refactor: move run accumulation out of the view model into the domain
The view model was doing two jobs. Alongside presentation state it held what happened during a run — the interval series, the rule that iperf3's omitted warm-up intervals do not count, the retained end summary — and the screen computed mean and peak inline in a widget builder. All of that is the part that has to be correct, and all of it would have been rewritten by anyone replacing the UI. Iperf3Session accumulates a run in the domain layer: samples, totals, errors, whether it was stopped, and the statistics over them. The view model now feeds it events and keeps only a formatted transcript and the repaint schedule; the screen reads every displayed number off the session and derives none. Two details the session now owns rather than leaving to a caller. Omitted intervals are excluded, so a warm-up cannot drag the mean down. And totalRetransmits stays null when no interval reported a count — UDP does not measure retransmits, and "not measured" must not render as zero. The aggregation tests move with the logic, into a pure-Dart test with no widgets or bindings involved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// Pure-Dart tests for the run aggregate. No widgets, no bindings, no channel —
|
||||
// this is the part that has to be correct regardless of what draws it.
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:iperf3gui/domain/models/iperf3_event.dart';
|
||||
import 'package:iperf3gui/domain/models/iperf3_session.dart';
|
||||
|
||||
void main() {
|
||||
Iperf3Interval sample(double start, double bits,
|
||||
{int bytes = 1000, int? retransmits, bool omitted = false}) =>
|
||||
Iperf3Interval(
|
||||
start: start,
|
||||
end: start + 1,
|
||||
bytes: bytes,
|
||||
bitsPerSecond: bits,
|
||||
retransmits: retransmits,
|
||||
omitted: omitted,
|
||||
);
|
||||
|
||||
group('Iperf3Session', () {
|
||||
test('starts empty', () {
|
||||
final session = Iperf3Session();
|
||||
expect(session.isEmpty, isTrue);
|
||||
expect(session.sampleCount, isZero);
|
||||
expect(session.intervals, isEmpty);
|
||||
expect(session.start, isNull);
|
||||
expect(session.summary, isNull);
|
||||
expect(session.errors, isEmpty);
|
||||
expect(session.stopped, isFalse);
|
||||
expect(session.hasError, isFalse);
|
||||
});
|
||||
|
||||
test('statistics are null before any sample', () {
|
||||
final session = Iperf3Session();
|
||||
expect(session.latestBitsPerSecond, isNull);
|
||||
expect(session.meanBitsPerSecond, isNull);
|
||||
expect(session.peakBitsPerSecond, isNull);
|
||||
expect(session.totalRetransmits, isNull);
|
||||
});
|
||||
|
||||
test('records start, intervals and summary', () {
|
||||
final session = Iperf3Session()
|
||||
..add(const Iperf3Start(targetHost: 'host', targetPort: 5201))
|
||||
..add(sample(0, 100))
|
||||
..add(const Iperf3End(
|
||||
sent: Iperf3Total(seconds: 1, bytes: 10, bitsPerSecond: 80),
|
||||
));
|
||||
expect(session.isEmpty, isFalse);
|
||||
expect(session.start!.targetHost, equals('host'));
|
||||
expect(session.sampleCount, equals(1));
|
||||
expect(session.summary!.sent!.bitsPerSecond, equals(80));
|
||||
});
|
||||
|
||||
test('excludes omitted warm-up intervals from the series', () {
|
||||
final session = Iperf3Session()
|
||||
..add(sample(0, 10, omitted: true))
|
||||
..add(sample(1, 100))
|
||||
..add(sample(2, 200));
|
||||
expect(session.sampleCount, equals(2));
|
||||
expect(session.meanBitsPerSecond, equals(150),
|
||||
reason: 'the omitted 10 would have pulled the mean to ~103');
|
||||
});
|
||||
|
||||
test('computes mean, peak and latest over the samples', () {
|
||||
final session = Iperf3Session()
|
||||
..add(sample(0, 100))
|
||||
..add(sample(1, 400))
|
||||
..add(sample(2, 250));
|
||||
expect(session.latestBitsPerSecond, equals(250));
|
||||
expect(session.peakBitsPerSecond, equals(400));
|
||||
expect(session.meanBitsPerSecond, equals(250));
|
||||
});
|
||||
|
||||
test('sums sampled bytes', () {
|
||||
final session = Iperf3Session()
|
||||
..add(sample(0, 1, bytes: 1500))
|
||||
..add(sample(1, 1, bytes: 2500));
|
||||
expect(session.sampledBytes, equals(4000));
|
||||
});
|
||||
|
||||
test('sums retransmits when reported', () {
|
||||
final session = Iperf3Session()
|
||||
..add(sample(0, 1, retransmits: 2))
|
||||
..add(sample(1, 1, retransmits: 5));
|
||||
expect(session.totalRetransmits, equals(7));
|
||||
});
|
||||
|
||||
test('retransmits stay null when never reported, not zero', () {
|
||||
// UDP reports no retransmit counts; "not measured" must not read as 0.
|
||||
final session = Iperf3Session()
|
||||
..add(sample(0, 1))
|
||||
..add(sample(1, 1));
|
||||
expect(session.totalRetransmits, isNull);
|
||||
});
|
||||
|
||||
test('collects errors in order and flags them', () {
|
||||
final session = Iperf3Session()
|
||||
..add(const Iperf3Error('first'))
|
||||
..add(const Iperf3Error('second'));
|
||||
expect(session.hasError, isTrue);
|
||||
expect(session.errors, equals(['first', 'second']));
|
||||
expect(session.isEmpty, isFalse);
|
||||
});
|
||||
|
||||
test('records a stop', () {
|
||||
final session = Iperf3Session()..add(const Iperf3Stopped());
|
||||
expect(session.stopped, isTrue);
|
||||
});
|
||||
|
||||
test('ignores log lines', () {
|
||||
final session = Iperf3Session()..add(const Iperf3LogLine('noise'));
|
||||
expect(session.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('clear returns it to the empty state', () {
|
||||
final session = Iperf3Session()
|
||||
..add(const Iperf3Start(targetHost: 'host'))
|
||||
..add(sample(0, 100))
|
||||
..add(const Iperf3Error('boom'))
|
||||
..add(const Iperf3Stopped())
|
||||
..add(const Iperf3End())
|
||||
..clear();
|
||||
expect(session.isEmpty, isTrue);
|
||||
expect(session.stopped, isFalse);
|
||||
expect(session.hasError, isFalse);
|
||||
expect(session.peakBitsPerSecond, isNull);
|
||||
});
|
||||
|
||||
test('exposed collections are unmodifiable', () {
|
||||
final session = Iperf3Session()..add(sample(0, 1));
|
||||
expect(() => session.intervals.add(sample(1, 1)), throwsUnsupportedError);
|
||||
expect(() => session.errors.add('x'), throwsUnsupportedError);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user