Files
iperf3-gui/lib/ui/features/test_run/test_run_view_model.dart
T

152 lines
4.4 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:iperf3gui/domain/models/iperf3_event.dart';
import 'package:iperf3gui/domain/models/iperf3_result.dart';
import 'package:iperf3gui/domain/models/iperf3_session.dart';
import 'package:iperf3gui/domain/models/test_configuration.dart';
import 'package:iperf3gui/domain/services/iperf3_runner.dart';
import 'package:iperf3gui/ui/features/test_run/iperf3_event_formatter.dart';
/// State of the test run.
enum TestRunState { idle, running, completed, error }
/// ViewModel for managing test run state.
///
/// Presentation only. What happened during a run is accumulated by
/// [Iperf3Session] in the domain layer; this class subscribes to
/// [Iperf3Runner.events], feeds that session, keeps a formatted transcript for
/// display, and decides when to repaint.
class TestRunViewModel extends ChangeNotifier {
final Iperf3Runner iperf3Service;
/// How often to notify listeners while output is arriving. iperf3 can report
/// several times a second across many streams, and each rebuild re-renders
/// the whole transcript — so repaints are coalesced instead of one per event.
static const Duration displayInterval = Duration(milliseconds: 250);
TestRunViewModel({required this.iperf3Service}) {
_eventSubscription = iperf3Service.events.listen(_onEvent);
}
StreamSubscription<Iperf3Event>? _eventSubscription;
Timer? _repaintTimer;
bool _dirty = false;
TestRunState _state = TestRunState.idle;
TestRunState get state => _state;
Iperf3Result? _result;
Iperf3Result? get result => _result;
TestConfiguration? _currentConfig;
TestConfiguration? get currentConfig => _currentConfig;
final StringBuffer _transcript = StringBuffer();
/// Human-readable output accumulated so far. Display only — the structured
/// record of the run is [session].
String get output => _transcript.toString();
final Iperf3Session _session = Iperf3Session();
/// The accumulated run: interval series, totals, errors and their statistics.
Iperf3Session get session => _session;
/// Whether a test is currently in flight.
bool get isRunning => _state == TestRunState.running;
/// Whether the most recent finished run ended because the user stopped it.
bool get wasStopped => _result?.stopped ?? false;
/// Start a test with the given configuration.
Future<void> runTest(TestConfiguration config) async {
if (_state == TestRunState.running) return;
if (!config.isValid) {
_resetRunData();
_transcript
.writeln('Invalid configuration: check the server address and port.');
_state = TestRunState.error;
notifyListeners();
return;
}
_resetRunData();
_state = TestRunState.running;
_currentConfig = config;
notifyListeners();
try {
final result = await iperf3Service.run(config);
_result = result;
if (_transcript.isEmpty && result.rawOutput.isNotEmpty) {
_transcript.write(result.rawOutput);
}
_state =
result.exitCode == 0 ? TestRunState.completed : TestRunState.error;
} catch (e) {
_transcript.writeln('Test failed: $e');
_state = TestRunState.error;
} finally {
_stopRepaintTimer();
notifyListeners();
}
}
/// Request that a running test stop early.
Future<void> stopTest() async {
if (_state != TestRunState.running) return;
await iperf3Service.stop();
}
/// Reset to idle state.
void reset() {
_resetRunData();
_state = TestRunState.idle;
_currentConfig = null;
notifyListeners();
}
void _resetRunData() {
_result = null;
_transcript.clear();
_session.clear();
_dirty = false;
}
void _onEvent(Iperf3Event event) {
_session.add(event);
final line = formatIperf3Event(event);
if (line != null && line.isNotEmpty) {
_transcript.writeln(line);
}
_scheduleRepaint();
}
/// Coalesce repaints: mark dirty and let a timer flush at [displayInterval].
void _scheduleRepaint() {
_dirty = true;
_repaintTimer ??= Timer.periodic(displayInterval, (_) {
if (!_dirty) return;
_dirty = false;
notifyListeners();
});
}
void _stopRepaintTimer() {
_repaintTimer?.cancel();
_repaintTimer = null;
_dirty = false;
}
@override
void dispose() {
_stopRepaintTimer();
_eventSubscription?.cancel();
iperf3Service.dispose();
super.dispose();
}
}