CH-J SM Mobilespráva serverů přes SSH
Menu
Vydané zdroje

CH-J SM Mobile

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J / SM Mobile 175 souborů
sm-mobile-20261016T185235Z-8e93ecc1b59a Přidáno: · Sestaveno:
monitor_diagnostics_test.dart Přidáno: Datum není evidováno 18.1 KB · 519 řádků
Kód Stáhnout soubor
1import 'dart:async';
2import 'dart:io';
3import 'package:flutter/material.dart';
4import 'package:flutter_test/flutter_test.dart';
5import 'package:intl/intl.dart';
6import 'package:sm_mobile/core/app_controller.dart';
7import 'package:sm_mobile/l10n/strings.dart';
8import 'package:sm_mobile/services/diagnostics.dart';
9import 'package:sm_mobile/services/metrics_format.dart';
10import 'package:sm_mobile/services/net/icmp_probe.dart';
11import 'package:sm_mobile/services/net/measure.dart';
12import 'package:sm_mobile/services/net/tcp_ping.dart';
13import 'package:sm_mobile/ui/diagnostics_page.dart';
14import 'support.dart';
16String decimal(double v, int digits) => NumberFormat.decimalPatternDigits(
17 locale: 'cs',
18 decimalDigits: digits,
19).format(v);
21void main() {
22 group('metric formatting', () {
23 // Values as LinuxMonitor reports them (observed on Ubuntu 24.04.5).
24 const sample = {
25 'cpu': '1.0%',
26 'ram': '15.7% (308 / 1967 MiB)',
27 'disk': '17% (4521312 / 30298176 KiB)',
28 'uptime': '166790.65',
29 'load': '0.19 0.11 0.04',
30 'rtt': '12 ms',
31 };
33 test('parses the monitor output into numbers', () {
34 final v = MetricValues.parse(sample);
35 expect(v.cpu, 1.0);
36 expect(v.ramPercent, 15.7);
37 expect(v.ramUsedBytes, 308 * 1024 * 1024);
38 expect(v.ramTotalBytes, 1967 * 1024 * 1024);
39 expect(v.diskPercent, 17);
40 expect(v.diskUsedBytes, 4521312 * 1024);
41 expect(v.diskTotalBytes, 30298176 * 1024);
42 expect(v.uptimeSeconds, 166790.65);
43 expect(v.load, [0.19, 0.11, 0.04]);
44 });
46 test('unexpected output stays unparsed instead of guessed', () {
47 final v = MetricValues.parse(const {
48 'cpu': 'n/a',
49 'ram': 'weird',
50 'uptime': '-5',
51 'load': '0.1 x 0.2',
52 });
53 expect(v.cpu, isNull);
54 expect(v.ramPercent, isNull);
55 expect(v.ramUsedBytes, isNull);
56 expect(v.uptimeSeconds, isNull);
57 expect(v.load, isNull);
58 expect(MetricValues.parse(const {}).diskPercent, isNull);
59 });
61 test('uptime splits into days, hours and minutes', () {
62 expect(splitUptime(166790.65), (days: 1, hours: 22, minutes: 19));
63 expect(splitUptime(59), (days: 0, hours: 0, minutes: 0));
64 expect(splitUptime(3600), (days: 0, hours: 1, minutes: 0));
65 expect(splitUptime(86400 * 400 + 61), (days: 400, hours: 0, minutes: 1));
66 });
68 test('uptime is localized with correct plural forms', () async {
69 final cs = await AppLocalizations.delegate.load(const Locale('cs'));
70 final de = await AppLocalizations.delegate.load(const Locale('de'));
71 final en = await AppLocalizations.delegate.load(const Locale('en'));
72 String text(AppLocalizations l, double s) {
73 final u = splitUptime(s);
74 return [
75 if (u.days > 0) l.uptimeDays(u.days),
76 if (u.days > 0 || u.hours > 0) l.uptimeHours(u.hours),
77 l.uptimeMinutes(u.minutes),
78 ].join(' ');
79 }
81 expect(text(cs, 166790.65), '1 den 22 hodin 19 minut');
82 expect(
83 text(cs, 3 * 86400 + 2 * 3600 + 4 * 60),
84 '3 dny 2 hodiny 4 minuty',
85 );
86 expect(text(cs, 5 * 86400 + 60), '5 dní 0 hodin 1 minuta');
87 expect(text(en, 166790.65), '1 day 22 hours 19 minutes');
88 expect(text(de, 166790.65), '1 Tag 22 Stunden 19 Minuten');
89 });
91 test('sizes use binary units consistently', () {
92 expect(formatBytes(512, decimal), '512 B');
93 expect(formatBytes(308 * 1024 * 1024, decimal), '308 MiB');
94 expect(formatBytes(1967 * 1024 * 1024, decimal), '1,9 GiB');
95 expect(formatBytes(4521312 * 1024, decimal), '4,3 GiB');
96 expect(formatBytes(30298176 * 1024, decimal), '28,9 GiB');
97 expect(formatBytes(3 * 1024 * 1024 * 1024 * 1024, decimal), '3,0 TiB');
98 });
99 });
101 group('diagnostics page (simulated network, no sockets)', () {
102 late List<Object> started;
103 late Map<InternetAddress, StreamController<Object>> streams;
104 late bool cancelled;
106 NetTools tools({
107 List<String> a = const ['192.0.2.5'],
108 List<String> aaaa = const ['2001:db8::5'],
109 Object? lookupError,
110 String? Function(TraceMethod)? unsupported,
111 }) {
112 started = [];
113 streams = {};
114 cancelled = false;
115 return NetTools(
116 lookup: (host, type) async {
117 if (lookupError != null) throw lookupError;
118 final v4 = [for (final x in a) InternetAddress(x)];
119 final v6 = [for (final x in aaaa) InternetAddress(x)];
120 return switch (type) {
121 InternetAddressType.IPv4 => v4,
122 InternetAddressType.IPv6 => v6,
123 _ => [...v6, ...v4],
124 };
125 },
126 tcp: (address, settings) {
127 started.add(settings);
128 final c = StreamController<Sample>();
129 streams[address] = c;
130 return PingRun(c.stream, () async {
131 cancelled = true;
132 unawaited(c.close());
133 });
134 },
135 ping: (address, settings) {
136 started.add(settings);
137 final c = StreamController<ProbeEvent>();
138 streams[address] = c;
139 return IcmpRun(c.stream, () async {
140 cancelled = true;
141 unawaited(c.close());
142 });
143 },
144 traceroute: (address, settings) {
145 started.add(settings);
146 final c = StreamController<ProbeEvent>();
147 streams[address] = c;
148 return IcmpRun(c.stream, () async {
149 cancelled = true;
150 unawaited(c.close());
151 });
152 },
153 reverse: (a) async =>
154 a.address == '198.51.100.1' ? 'router.example' : null,
155 unsupported: unsupported ?? (_) => null,
156 );
157 }
159 Future<AppController> pump(WidgetTester tester, NetTools tools) async {
160 tester.view.physicalSize = const Size(900, 4000);
161 tester.view.devicePixelRatio = 1;
162 addTearDown(tester.view.reset);
163 final c = AppController(MemoryStore());
164 await tester.runAsync(() async {
165 await c.initialize();
166 await c.acceptLicense();
167 await c.openVault('diagnostics ui test', create: true);
168 });
169 await tester.pumpWidget(
170 MaterialApp(
171 locale: const Locale('en'),
172 localizationsDelegates: AppLocalizations.localizationsDelegates,
173 supportedLocales: AppLocalizations.supportedLocales,
174 home: Scaffold(body: DiagnosticsPage(c, tools: tools)),
175 ),
176 );
177 await tester.pumpAndSettle();
178 return c;
179 }
181 Sample tcpSample(int seq, String address, double? rtt, Outcome outcome) =>
182 Sample(
183 seq: seq,
184 time: DateTime(2026, 10, 16, 12, 0, seq),
185 family: address.contains(':') ? Family.v6 : Family.v4,
186 address: address,
187 port: 443,
188 rttMs: rtt,
189 outcome: outcome,
190 );
192 // Measurements animate a progress bar, so the page never settles while
193 // one runs; pump a few frames instead.
194 Future<void> step(WidgetTester tester) async {
195 for (var i = 0; i < 5; i++) {
196 await tester.pump(const Duration(milliseconds: 50));
197 }
198 }
200 Future<void> finish(WidgetTester tester, AppController c) async {
201 await tester.pumpWidget(const SizedBox());
202 c.dispose();
203 }
205 testWidgets('TCP ping: count presets, every attempt listed, statistics', (
206 tester,
207 ) async {
208 final c = await pump(tester, tools());
209 await tester.enterText(find.byType(TextField).at(0), 'server.test');
210 await tester.enterText(find.byType(TextField).at(1), '443');
211 await tester.tap(find.text('IPv4'));
212 await tester.tap(find.byType(DropdownButtonFormField<String>));
213 await tester.pumpAndSettle();
214 await tester.tap(find.text('10').last);
215 await tester.pumpAndSettle();
216 await tester.tap(find.text('Run test'));
217 await step(tester);
218 final s = started.single as TcpPingSettings;
219 expect((s.port, s.count), (443, 10));
220 final c4 =
221 streams[InternetAddress('192.0.2.5')]! as StreamController<Sample>;
222 c4
223 ..add(tcpSample(1, '192.0.2.5', 10, Outcome.ok))
224 ..add(tcpSample(2, '192.0.2.5', null, Outcome.timeout))
225 ..add(tcpSample(3, '192.0.2.5', 1.5, Outcome.refused))
226 ..add(tcpSample(4, '192.0.2.5', 30, Outcome.ok));
227 await step(tester);
228 expect(find.text('Timeout'), findsOneWidget);
229 expect(find.textContaining('Refused (TCP reset)'), findsOneWidget);
230 expect(find.text('10.0 ms'), findsWidgets);
231 Text value(String label) => tester.widget<Text>(
232 find
233 .descendant(
234 of: find
235 .ancestor(of: find.text(label), matching: find.byType(Column))
236 .first,
237 matching: find.byType(Text),
238 )
239 .last,
240 );
241 expect(value('Attempts').data, '4');
242 expect(value('Successful').data, '2');
243 expect(value('Failed').data, '2');
244 expect(value('Success').data, '50.0 %');
245 expect(value('Average').data, '20.0 ms');
246 expect(value('Jitter').data, '20.0 ms');
247 expect(value('P95').data, '—');
248 await tester.tap(find.text('Stop'));
249 await step(tester);
250 expect(cancelled, true);
251 expect(find.text('Run test'), findsOneWidget);
252 await finish(tester, c);
253 });
255 testWidgets('both IP versions run independently; a missing AAAA is shown', (
256 tester,
257 ) async {
258 var c = await pump(tester, tools());
259 await tester.enterText(find.byType(TextField).at(0), 'dual.test');
260 await tester.tap(find.text('4 + 6'));
261 await tester.tap(find.text('Run test'));
262 await step(tester);
263 expect(started, hasLength(2));
264 expect(streams.keys.map((a) => a.address).toSet(), {
265 '192.0.2.5',
266 '2001:db8::5',
267 });
268 (streams[InternetAddress('2001:db8::5')]! as StreamController<Sample>)
269 .add(tcpSample(1, '2001:db8::5', null, Outcome.timeout));
270 (streams[InternetAddress('192.0.2.5')]! as StreamController<Sample>).add(
271 tcpSample(1, '192.0.2.5', 12, Outcome.ok),
272 );
273 await tester.pump();
274 expect(find.text('IPv4 and IPv6 overview'), findsOneWidget);
275 expect(
276 find.text('0.0 % (0/1)'),
277 findsOneWidget,
278 reason: 'IPv6 failure is visible',
279 );
280 expect(find.text('100.0 % (1/1)'), findsOneWidget);
281 await finish(tester, c);
283 c = await pump(tester, tools(aaaa: const []));
284 await tester.enterText(find.byType(TextField).at(0), 'only-a.test');
285 await tester.tap(find.text('4 + 6'));
286 await tester.tap(find.text('Run test'));
287 await step(tester);
288 expect(started, hasLength(1));
289 expect(
290 find.textContaining('No address of this IP version'),
291 findsOneWidget,
292 );
293 await finish(tester, c);
294 });
296 testWidgets('ICMP ping options, address actually used, stop on demand', (
297 tester,
298 ) async {
299 final c = await pump(tester, tools());
300 await tester.tap(find.text('ICMP ping'));
301 await tester.pump();
302 expect(find.text('TCP port'), findsNothing, reason: 'ICMP has no port');
303 await tester.enterText(find.byType(TextField).first, 'server.test');
304 await tester.tap(find.byType(DropdownButtonFormField<String>));
305 await tester.pumpAndSettle();
306 await tester.tap(find.text('Continuous until stopped').last);
307 await tester.pumpAndSettle();
308 await tester.tap(find.text('Test options'));
309 await tester.pumpAndSettle();
310 await tester.enterText(
311 find.widgetWithText(TextField, 'Payload size (B)'),
312 '100',
313 );
314 await tester.enterText(
315 find.widgetWithText(TextField, 'TTL / hop limit'),
316 '7',
317 );
318 await tester.tap(find.text("Don't fragment (DF)"));
319 await tester.tap(find.text('Run test'));
320 await step(tester);
321 final s = started.single as PingSettings;
322 expect((s.count, s.size, s.ttl, s.dontFragment), (0, 100, 7, true));
323 // Automatic mode: the resolver preferred IPv6, and the page says so.
324 expect(find.textContaining('IPv6 / ICMPv6 echo'), findsOneWidget);
325 final from = InternetAddress('2001:db8::5');
326 (streams[from]! as StreamController<ProbeEvent>)
327 ..add(ProbeEvent(1, ProbeStatus.reply, from: from, rttMs: 10))
328 ..add(const ProbeEvent(2, ProbeStatus.timeout))
329 ..add(ProbeEvent(3, ProbeStatus.reply, from: from, rttMs: 30));
330 await step(tester);
331 expect(find.text('Timeout'), findsOneWidget);
332 await tester.tap(find.text('Stop'));
333 await step(tester);
334 expect(cancelled, true);
335 expect(find.textContaining('Stopped by user'), findsOneWidget);
336 await finish(tester, c);
337 });
339 testWidgets('platform errors are explained, not hidden', (tester) async {
340 final c = await pump(
341 tester,
342 tools(a: const ['192.0.2.9'], aaaa: const []),
343 );
344 await tester.tap(find.text('ICMP ping'));
345 await tester.pump();
346 await tester.enterText(find.byType(TextField).first, '192.0.2.9');
347 await tester.tap(find.text('Run test'));
348 await step(tester);
349 final s =
350 streams[InternetAddress('192.0.2.9')]!
351 as StreamController<ProbeEvent>;
352 s.addError(const IcmpUnavailable('icmpNotPermitted', 13));
353 await s.close();
354 await step(tester);
355 expect(
356 find.textContaining('does not allow an unprivileged ICMP'),
357 findsWidgets,
358 );
359 await finish(tester, c);
360 });
362 testWidgets('traceroute: hops, *, names, statistics, compare and export', (
363 tester,
364 ) async {
365 final c = await pump(
366 tester,
367 tools(a: const ['192.0.2.9'], aaaa: const []),
368 );
369 await tester.tap(find.text('Traceroute'));
370 await tester.pump();
371 await tester.enterText(find.byType(TextField).first, '192.0.2.9');
372 await tester.tap(find.text('UDP'));
373 await tester.tap(find.text('Run test'));
374 await step(tester);
375 final settings = started.single as TraceSettings;
376 expect(
377 (settings.method, settings.maxHops, settings.probes),
378 (TraceMethod.udp, 30, 3),
379 );
380 final router = InternetAddress('198.51.100.1');
381 final target = InternetAddress('192.0.2.9');
382 final s = streams[target]! as StreamController<ProbeEvent>;
383 s
384 ..add(
385 ProbeEvent(
386 1,
387 ProbeStatus.timeExceeded,
388 ttl: 1,
389 from: router,
390 rttMs: 1.5,
391 ),
392 )
393 ..add(const ProbeEvent(2, ProbeStatus.timeout, ttl: 1))
394 ..add(
395 ProbeEvent(
396 3,
397 ProbeStatus.timeExceeded,
398 ttl: 1,
399 from: router,
400 rttMs: 2.5,
401 ),
402 )
403 ..add(const ProbeEvent(1, ProbeStatus.timeout, ttl: 2))
404 ..add(
405 ProbeEvent(
406 1,
407 ProbeStatus.reply,
408 ttl: 3,
409 from: target,
410 rttMs: 12,
411 icmpCode: 3,
412 note: 'portUnreachable',
413 ),
414 );
415 await s.close();
416 await step(tester);
417 expect(find.text('router.example (198.51.100.1)'), findsOneWidget);
418 expect(find.text('1.50 ms * 2.50 ms'), findsOneWidget);
419 expect(
420 find.textContaining('min / avg / max: 1.50 / 2.00 / 2.50 ms'),
421 findsOneWidget,
422 );
423 expect(find.textContaining('Destination reached'), findsOneWidget);
424 expect(find.textContaining('Protocol: UDP'), findsOneWidget);
425 // A second run can be compared with the first.
426 await tester.tap(find.text('Run test'));
427 await step(tester);
428 final again = streams[target]! as StreamController<ProbeEvent>;
429 again.add(
430 ProbeEvent(
431 1,
432 ProbeStatus.timeExceeded,
433 ttl: 1,
434 from: InternetAddress('198.51.100.7'),
435 rttMs: 3,
436 ),
437 );
438 await again.close();
439 await step(tester);
440 await tester.tap(find.text('Compare with the previous measurement'));
441 await tester.pumpAndSettle();
442 expect(find.textContaining('Route changed'), findsOneWidget);
443 await finish(tester, c);
444 });
446 testWidgets('an unsupported traceroute method cannot be chosen', (
447 tester,
448 ) async {
449 final c = await pump(
450 tester,
451 tools(
452 unsupported: (m) =>
453 m == TraceMethod.tcp ? 'tcpTraceUnsupported' : null,
454 ),
455 );
456 await tester.tap(find.text('Traceroute'));
457 await tester.pump();
458 expect(
459 find.textContaining('TCP: Not available on Android'),
460 findsOneWidget,
461 );
462 await tester.tap(find.text('TCP'));
463 await tester.pump();
464 final selected = tester
465 .widget<SegmentedButton<TraceMethod>>(
466 find.byType(SegmentedButton<TraceMethod>),
467 )
468 .selected;
469 expect(selected, {TraceMethod.icmp});
470 await finish(tester, c);
471 });
473 testWidgets('DNS failure and invalid options never start a measurement', (
474 tester,
475 ) async {
476 final c = await pump(
477 tester,
478 tools(lookupError: const SocketException('Failed host lookup')),
479 );
480 await tester.enterText(
481 find.byType(TextField).first,
482 'nonexistent.invalid',
483 );
484 await tester.tap(find.text('Run test'));
485 await step(tester);
486 expect(started, isEmpty);
487 expect(find.textContaining('DNS lookup failed'), findsOneWidget);
488 await tester.enterText(find.byType(TextField).at(1), '70000');
489 await tester.tap(find.text('Run test'));
490 await step(tester);
491 expect(started, isEmpty);
492 expect(find.text('Invalid value in the test options.'), findsOneWidget);
493 await finish(tester, c);
494 });
495 });
497 test('real sockets: TCP failure is reported as the TCP stage', () async {
498 final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
499 final port = server.port;
500 final ok = await Diagnostics().probe(
501 '127.0.0.1',
502 port,
503 type: InternetAddressType.IPv4,
504 );
505 expect(ok.connected!.address, '127.0.0.1');
506 expect(ok.dnsMs, isNotNull);
507 await server.close();
508 await expectLater(
509 Diagnostics().probe('127.0.0.1', port, type: InternetAddressType.IPv4),
510 throwsA(
511 isA<ProbeFailure>().having((e) => e.stage, 'stage', 'tcp').having(
512 (e) => e.addresses,
513 'addresses',
514 ['127.0.0.1'],
515 ),
516 ),
517 );
518 });
SHA-256d17d7645a7a7bcd3c3537b3154817096619f02a4cc3551434ec7501d2f1beb5b

CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

SHA-256 archivu46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Soubory