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>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user