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:
@@ -195,29 +195,31 @@ class _TestRunContentState extends State<_TestRunContent> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Live throughput figures, derived from the parsed interval series
|
||||
// Live throughput figures. Every number here is read off the
|
||||
// session; none are computed in the widget tree.
|
||||
Consumer<TestRunViewModel>(
|
||||
builder: (context, vm, _) {
|
||||
final intervals = vm.intervals;
|
||||
if (intervals.isEmpty) return const SizedBox.shrink();
|
||||
final rates =
|
||||
intervals.map((i) => i.bitsPerSecond).toList(growable: false);
|
||||
final latest = rates.last;
|
||||
final peak = rates.reduce((a, b) => a > b ? a : b);
|
||||
final mean = rates.reduce((a, b) => a + b) / rates.length;
|
||||
final summary = vm.summary?.sent;
|
||||
final session = vm.session;
|
||||
final latest = session.latestBitsPerSecond;
|
||||
final mean = session.meanBitsPerSecond;
|
||||
final peak = session.peakBitsPerSecond;
|
||||
if (latest == null || mean == null || peak == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final total = session.summary?.sent;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_Stat(
|
||||
label: summary != null ? 'total' : 'now',
|
||||
value: formatBitrate(summary?.bitsPerSecond ?? latest),
|
||||
label: total != null ? 'total' : 'now',
|
||||
value: formatBitrate(total?.bitsPerSecond ?? latest),
|
||||
),
|
||||
_Stat(label: 'avg', value: formatBitrate(mean)),
|
||||
_Stat(label: 'peak', value: formatBitrate(peak)),
|
||||
_Stat(label: 'samples', value: '${rates.length}'),
|
||||
_Stat(
|
||||
label: 'samples', value: '${session.sampleCount}'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user