Files
dmc/app/test/ui/providers/telemetry_provider_test.dart
T
Constantin LeueandClaude Sonnet 5 e5c87fba5f Drone/Netzwerk-Connect-Disconnect wirklich repariert (Riverpods Auto-Retry verschluckte den Fehlerzustand)
Der vorherige Fix (msp_telemetry_poller.dart, addError nach 3 fehl-
geschlagenen Zyklen) hat die Erkennung allein nicht repariert - aus zwei
Gruenden, beide jetzt behoben:

1. telemetryProvider brach bei WLAN-Verbindungsverlust (wifiLinkStateProvider
   != connected) den Strom bisher mit einem stillen `return;` ab, statt
   ueberhaupt subscribeTelemetry() zu abonnieren. Damit blieb der Provider
   nach einem zuvor erfolgreichen Verbindungsaufbau unbegrenzt auf dem
   letzten AsyncData(...) haengen, sobald das WLAN-Netz verloren ging -
   komplett unabhaengig vom MSP-Poller-Fix, der in diesem Fall nie erreicht
   wird. Ersetzt durch eine neue WifiLinkNotConnectedException.

2. Der eigentliche Grund, warum ich das beim ersten Fix nicht bemerkt habe:
   Riverpod 3.x wiederholt einen fehlgeschlagenen Provider standardmaessig
   automatisch (ProviderContainer.defaultRetry) und haelt ihn dabei in
   AsyncLoading(error: ..., retrying: true) statt sofort auf AsyncError zu
   wechseln - AsyncValue.when()s error:-Zweig (systemMessageAutoLogProvider)
   feuert dafuer nicht, nur der loading:-Zweig (No-Op). Das betraf sowohl
   die neue WifiLinkNotConnectedException als auch das per yield*
   durchgereichte addError aus dem MSP-Poller - beide blieben dadurch
   unbegrenzt "am Wiederholen haengen", nie als AsyncError sichtbar. Mit
   retry: (retryCount, error) => null gezielt fuer telemetryProvider
   deaktiviert - die eigentliche Wiederherstellung passiert ohnehin
   reaktiv (ref.watch(wifiLinkStateProvider) bzw. der Poller-Takt selbst),
   nicht ueber Riverpods Backoff.

Neuer Test in telemetry_provider_test.dart deckt jetzt die komplette
Kette end-to-end ab (echter MspFlightControllerLink + LoopbackTransport +
ueberschriebener wifiLinkStateProvider, keine der bisherigen Tests in
system_message_log_provider_test.dart haette diesen Fehler auffangen
koennen, da sie telemetryProvider selbst immer ueberschreiben statt seine
eigene Generatorfunktion zu durchlaufen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 17:17:13 +02:00

165 lines
6.4 KiB
Dart

import 'dart:async';
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dmc_app/domain/telemetry/system_message.dart';
import 'package:dmc_app/transport/connection_type.dart';
import 'package:dmc_app/transport/link_transport.dart';
import 'package:dmc_app/transport/loopback/loopback_transport.dart';
import 'package:dmc_app/transport/msp/msp_commands.dart';
import 'package:dmc_app/transport/msp/msp_flight_controller_link.dart';
import 'package:dmc_app/transport/msp/msp_frame.dart';
import 'package:dmc_app/transport/msp/msp_frame_decoder.dart';
import 'package:dmc_app/ui/providers/connection_settings_provider.dart';
import 'package:dmc_app/ui/providers/system_message_log_provider.dart';
import 'package:dmc_app/ui/providers/telemetry_provider.dart';
import 'package:dmc_app/ui/providers/wifi_connection_provider.dart';
/// Minimaler Fake-FC (analog msp_flight_controller_link_test.dart/
/// msp_telemetry_poller_test.dart) - beantwortet jede Anfrage sofort mit
/// einer gueltigen Antwort.
class _FakeResponder {
_FakeResponder(this.transport) {
_timer = Timer.periodic(const Duration(milliseconds: 2), _tick);
}
final LoopbackTransport transport;
late final Timer _timer;
var _answered = 0;
void _tick(Timer _) {
while (_answered < transport.sentData.length) {
final request =
MspFrameDecoder().addBytes(transport.sentData[_answered]).single;
_answered++;
final Uint8List payload;
switch (request.function) {
case MspCommands.rawGps:
final d = ByteData(18);
d.setUint8(0, 3);
d.setUint8(1, 10);
d.setInt32(2, 525200000, Endian.little);
d.setInt32(6, 134050000, Endian.little);
d.setUint16(10, 100, Endian.little);
d.setUint16(12, 500, Endian.little);
d.setUint16(16, 150, Endian.little);
payload = d.buffer.asUint8List();
case MspCommands.altitude:
final d = ByteData(10);
d.setInt32(0, 5000, Endian.little);
payload = d.buffer.asUint8List();
case MspCommands.inavStatus:
payload = Uint8List(22);
case MspCommands.navStatus:
payload = Uint8List.fromList([0, 0, 0, 0, 0, 0, 0]);
case MspCommands.inavAnalog:
payload = Uint8List(24);
case MspCommands.inavLinkStats:
payload = Uint8List(3);
case MspCommands.inavTemperatures:
payload = Uint8List(16);
default:
payload = Uint8List(0);
}
transport.feed(
encodeMspV2Frame(MspDirection.response, request.function, payload),
);
}
}
void cancel() => _timer.cancel();
}
typedef _ConnectionSettings = ({
int? activeDroneProfileId,
int? currentMissionId,
String? connectionDeviceAddress,
bool connectionAutoConnect,
ConnectionType connectionType,
String? connectionWifiHost,
int connectionWifiPort,
String connectionWifiSsidPrefix,
String? connectionWifiLastSsid,
});
_ConnectionSettings _wifiSettings() => (
activeDroneProfileId: null,
currentMissionId: null,
connectionDeviceAddress: null,
connectionAutoConnect: true,
connectionType: ConnectionType.wifi,
connectionWifiHost: null,
connectionWifiPort: 14550,
connectionWifiSsidPrefix: 'mLRS-',
connectionWifiLastSsid: null,
);
void main() {
// Reproduziert den Nutzerbericht "Netzwerk Connect und Disconnect wurde
// noch nie erkannt": alle Tests in system_message_log_provider_test.dart
// ueberschreiben telemetryProvider direkt und pruefen deshalb nie dessen
// eigene Generatorfunktion - genau dort sass der Fehler (stilles `return;`
// statt eines Fehlers, siehe telemetry_provider.dart).
test(
'telemetryProvider haengt bei Verlust der WLAN-Verbindung nicht mehr '
'unbegrenzt in AsyncLoading, sondern wird zu AsyncError - end-to-end '
'bis "Drone connected"/"Drone disconnected" im System-Messages-Log',
() async {
final transport = LoopbackTransport();
final responder = _FakeResponder(transport);
addTearDown(responder.cancel);
final link = MspFlightControllerLink(transport: transport);
addTearDown(link.disconnect);
final wifiState = StreamController<LinkState>.broadcast();
addTearDown(wifiState.close);
final container = ProviderContainer(overrides: [
connectionSettingsProvider
.overrideWith((ref) async => _wifiSettings()),
flightControllerLinkProvider.overrideWithValue(link),
wifiLinkStateProvider.overrideWith((ref) => wifiState.stream),
]);
addTearDown(container.dispose);
container.listen(systemMessageAutoLogProvider, (previous, next) {});
container.listen(telemetryProvider, (previous, next) {});
// Noch nie verbunden: telemetryProvider darf nicht auf AsyncData
// stehen, und es darf noch nichts geloggt werden (kein hadData).
wifiState.add(LinkState.disconnected);
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(container.read(telemetryProvider).hasValue, isFalse);
expect(container.read(systemMessageLogProvider), isEmpty);
// "WLAN verbindet sich".
wifiState.add(LinkState.connected);
await Future<void>.delayed(const Duration(milliseconds: 400));
expect(container.read(telemetryProvider).hasValue, isTrue);
final afterConnect = container.read(systemMessageLogProvider);
expect(afterConnect, hasLength(1));
expect(afterConnect.single.message, 'Drone connected');
// "WLAN-Netz verloren" (UdpTransport meldet LinkState.error bei
// MlrsNetworkLost, siehe udp_transport.dart).
wifiState.add(LinkState.error);
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(container.read(telemetryProvider).hasError, isTrue);
final afterLost = container.read(systemMessageLogProvider);
expect(afterLost.first.message, 'Drone disconnected');
expect(afterLost.first.severity, SystemMessageSeverity.critical);
// "WLAN wiederhergestellt".
wifiState.add(LinkState.connected);
await Future<void>.delayed(const Duration(milliseconds: 400));
expect(container.read(telemetryProvider).hasValue, isTrue);
final afterReconnect = container.read(systemMessageLogProvider);
expect(afterReconnect.first.message, 'Drone connected');
},
timeout: const Timeout(Duration(seconds: 10)),
);
}