Files
iperf3-gui/test/domain/models/iperf3_event_test.dart
T
mikkeli 7d8d06e2e5 feat(app): parse json-stream reports into structured events
The native layer now emits one JSON object per report, so the service parses
each into a sealed Iperf3Event — start, interval, end, error — and publishes
those instead of opaque lines. Interval samples carry bytes, bits per second,
retransmits and per-stream socket/cwnd/rtt detail; the end event carries sent
and received totals and CPU utilisation. None of that was available while the
output was scraped text.

Anything unrecognised — non-JSON, malformed JSON, an event name a future
iperf3 adds — becomes an Iperf3LogLine rather than being dropped, so
unexpected output still reaches the user.

Failures arrive as an `error` event rather than a non-zero return from
iperf_run_client, so that event is what now marks a run as failed. The
result's rawOutput keeps the JSON lines verbatim, since that is what the
native layer actually produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:35:10 +09:00

108 lines
4.4 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:iperf3gui/domain/models/iperf3_event.dart';
void main() {
group('Iperf3Event.parse', () {
test('parses a start event', () {
const line = '{"event":"start","data":{'
'"connecting_to":{"host":"192.168.2.182","port":5201},'
'"test_start":{"protocol":"TCP","num_streams":4,"duration":10}}}';
final event = Iperf3Event.parse(line);
expect(event, isA<Iperf3Start>());
final start = event as Iperf3Start;
expect(start.targetHost, equals('192.168.2.182'));
expect(start.targetPort, equals(5201));
expect(start.streamCount, equals(4));
expect(start.durationSeconds, equals(10));
expect(start.protocol, equals('TCP'));
});
test('parses an interval event with per-stream detail', () {
const line = '{"event":"interval","data":{'
'"streams":[{"socket":143,"bytes":49283072,'
'"bits_per_second":394264576.0,"retransmits":0,"snd_cwnd":2023424,'
'"rtt":3521}],'
'"sum":{"start":0.0,"end":1.000151,"seconds":1.000151,'
'"bytes":98566144,"bits_per_second":788410352.0,"retransmits":0,'
'"omitted":false,"sender":true}}}';
final event = Iperf3Event.parse(line);
expect(event, isA<Iperf3Interval>());
final interval = event as Iperf3Interval;
expect(interval.bytes, equals(98566144));
expect(interval.bitsPerSecond, closeTo(788410352.0, 0.1));
expect(interval.retransmits, equals(0));
expect(interval.omitted, isFalse);
expect(interval.streams, hasLength(1));
expect(interval.streams.single.socket, equals(143));
expect(interval.streams.single.rtt, equals(3521));
});
test('marks omitted intervals', () {
const line = '{"event":"interval","data":{"streams":[],'
'"sum":{"start":0.0,"end":1.0,"bytes":1,"bits_per_second":8.0,'
'"omitted":true}}}';
final interval = Iperf3Event.parse(line) as Iperf3Interval;
expect(interval.omitted, isTrue);
});
test('parses an end event with sent and received totals', () {
const line = '{"event":"end","data":{'
'"sum_sent":{"seconds":10.0,"bytes":987654321,'
'"bits_per_second":790123456.0,"retransmits":7},'
'"sum_received":{"seconds":10.01,"bytes":987000000,'
'"bits_per_second":789000000.0},'
'"cpu_utilization_percent":{"host_total":12.5,"remote_total":3.25}}}';
final end = Iperf3Event.parse(line) as Iperf3End;
expect(end.sent!.bytes, equals(987654321));
expect(end.sent!.retransmits, equals(7));
expect(end.received!.bitsPerSecond, closeTo(789000000.0, 0.1));
expect(end.hostCpuPercent, closeTo(12.5, 0.01));
expect(end.remoteCpuPercent, closeTo(3.25, 0.01));
});
test('falls back to bidir-reverse keys when present', () {
const line = '{"event":"interval","data":{"streams":[],'
'"sum_bidir_reverse":{"start":0.0,"end":1.0,"bytes":42,'
'"bits_per_second":336.0}}}';
final interval = Iperf3Event.parse(line) as Iperf3Interval;
expect(interval.bytes, equals(42));
});
test('parses an error event', () {
const line = '{"event":"error","data":"unable to connect to server"}';
final event = Iperf3Event.parse(line);
expect(event, isA<Iperf3Error>());
expect((event as Iperf3Error).message,
equals('unable to connect to server'));
});
test('parses the bridge stop marker', () {
final event = Iperf3Event.parse('{"event":"jni_stopped","data":{}}');
expect(event, isA<Iperf3Stopped>());
});
test('surfaces non-JSON output as a log line rather than dropping it', () {
final event = Iperf3Event.parse('iperf3: something unexpected');
expect(event, isA<Iperf3LogLine>());
expect((event as Iperf3LogLine).text,
equals('iperf3: something unexpected'));
});
test('surfaces malformed JSON as a log line', () {
final event = Iperf3Event.parse('{"event":"interval",');
expect(event, isA<Iperf3LogLine>());
});
test('treats an unknown event name as a log line', () {
final event = Iperf3Event.parse('{"event":"future_thing","data":{}}');
expect(event, isA<Iperf3LogLine>());
});
test('tolerates a missing data object', () {
final event = Iperf3Event.parse('{"event":"interval"}');
expect(event, isA<Iperf3Interval>());
expect((event as Iperf3Interval).bytes, equals(0));
});
});
}