todo-app/test/port_guard_parsing_test.dart
Krzysztof kuhy Rudnicki 10a546f6d6 Split nine oversized test files under the cap
Every split is by test-group/concern, with the shared setup moved to a
sibling helper the splits import. All 313 tests are preserved -- the count
is unchanged and `test('…')` names are byte-identical to the originals
(verified by diffing the sorted name lists).

  note_repository_test      677 -> 4 files + note_repository_helpers
  capture_screen_test       660 -> 4 files + capture_screen_harness
  note_editor_test          502 -> 3 files + note_editor_harness
  port_guard_test           405 -> 3 files + port_guard_fakes
  wrapper_server_test       402 -> 2 files + wrapper_server_harness
  settings_screen_test      391 -> 2 files + settings_screen_harness
  github_mirror_screen_test 365 -> 2 files + github_mirror_harness
  notes_list_screen_test    347 -> 2 files + notes_list_harness
  analytics_service_test    300 -> 2 files + analytics_helpers

Helpers are deliberately NOT named `*_test.dart`; the runner would collect
them and fail on the missing `main()`.

Three things did not survive a mechanical split and were handled explicitly:

  * wrapper_server's `enabledOrigin` closes over the `late root` that setUp
    assigns, so a top-level function cannot see it. It is now a
    WrapperHarness class with an install() that registers the setUp/tearDown
    pair -- the only structural change in this commit.
  * note_repository's splits each repeat `setUpAll(sqfliteFfiInit)`, and
    analytics' each repeat `@TestOn('vm')`. Omitting either fails quietly
    (uninitialised sqflite / wrong platform) rather than loudly.
  * capture_screen's `recordingMock` was declared mid-file between two
    tests, so it moved to the harness rather than being dropped.

Verified: 313 tests pass (same as before the refactor), 100% coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 16:42:13 +02:00

205 lines
5.4 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:todo/desktop/port_guard.dart';
import 'port_guard_fakes.dart';
/// Real `ss -tlnp` output, including the header row and the run-together
/// `Peer Address:PortProcess` column that iproute2 actually prints.
const _ssOutput = '''
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 4096 0.0.0.0:5355 0.0.0.0:*
LISTEN 0 128 127.0.0.1:8730 0.0.0.0:* users:(("dart:todo_deskt",pid=61687,fd=7))
LISTEN 0 511 127.0.0.1:9000 0.0.0.0:* users:(("node",pid=1234,fd=20))
''';
/// A [SystemProbe] whose every answer is scripted by the test.
class FakeProbe implements SystemProbe {
FakeProbe({
this.pid,
this.description,
this.cmdlines = const [],
this.inUse = const [true],
this.freeAfter,
});
int? pid;
ProcessDescription? description;
List<List<String>> cmdlines;
/// Answers for successive [portInUse] calls; the last one repeats.
List<bool> inUse;
/// When set, the port frees only once this signal has been delivered.
///
/// Keyed on the signal rather than on elapsed time so the escalation test
/// cannot flake on a slow machine.
ProcessSignal? freeAfter;
final List<ProcessSignal> signalsSent = [];
int portInUseCalls = 0;
@override
Future<int?> listenerPid(int port) async => pid;
@override
Future<ProcessDescription?> describe(int pid) async => description;
@override
Future<List<List<String>>> processCmdlines() async => cmdlines;
@override
Future<bool> portInUse(int port) async {
portInUseCalls++;
if (freeAfter != null) return !signalsSent.contains(freeAfter);
return inUse[(portInUseCalls - 1).clamp(0, inUse.length - 1)];
}
@override
bool signal(int pid, ProcessSignal signal) {
signalsSent.add(signal);
return true;
}
}
const _profile = '/home/kuchy/.local/share/todo-desktop/profile';
/// Fast timings so the reap tests do not slow the suite down.
Future<bool> _reap(PortGuard guard, int pid, int port) => guard.reap(
pid,
port,
termWait: const Duration(milliseconds: 60),
killWait: const Duration(milliseconds: 60),
pollInterval: const Duration(milliseconds: 5),
);
void main() {
group('parseListenerPid', () {
test('finds the pid holding the requested port', () {
expect(parseListenerPid(_ssOutput, 8730), 61687);
expect(parseListenerPid(_ssOutput, 9000), 1234);
});
test('returns null when no line listens on the port', () {
expect(parseListenerPid(_ssOutput, 7777), isNull);
});
test('returns null when ss hides the owning pid', () {
// ss omits the users:(...) column for another user's socket.
const hidden = 'LISTEN 0 128 127.0.0.1:8730 0.0.0.0:*';
expect(parseListenerPid(hidden, 8730), isNull);
});
test('ignores short and empty lines', () {
expect(parseListenerPid('\nLISTEN 0 128\n', 8730), isNull);
});
test('does not match a port that is only a suffix', () {
const other =
'LISTEN 0 128 127.0.0.1:18730 0.0.0.0:* '
'users:(("x",pid=5,fd=7))';
expect(parseListenerPid(other, 8730), isNull);
});
});
group('isTodoWrapper', () {
test('accepts the installed binary', () {
expect(isTodoWrapper('/opt/todo/bin/todo_desktop', []), isTrue);
});
test('accepts a binary replaced by an upgrade', () {
expect(
isTodoWrapper('/opt/todo/bin/todo_desktop (deleted)', []),
isTrue,
);
});
test('accepts a development run out of the repo', () {
expect(
isTodoWrapper('/usr/lib/dart/bin/dart', [
'dart',
'run',
'bin/todo_desktop.dart',
'--web-root',
'build/web',
]),
isTrue,
);
});
test('rejects an unrelated process', () {
expect(
isTodoWrapper('/usr/bin/python3.14', [
'python3',
'-m',
'http.server',
'8730',
]),
isFalse,
);
});
});
group('hasWindowAttached', () {
// Chrome rewrites argv into one space-joined string, so this is the shape
// that actually appears in /proc for a browser window.
const collapsed = [
'/opt/thorium-browser/thorium --app=http://localhost:8730 '
'--user-data-dir=$_profile --class=todo --no-first-run',
];
test('matches a browser whose argv collapsed into one token', () {
expect(hasWindowAttached([collapsed], _profile), isTrue);
});
test('matches a normally separated argument vector', () {
expect(
hasWindowAttached(
[
['thorium', '--user-data-dir=$_profile', '--class=todo'],
],
_profile,
),
isTrue,
);
});
test('matches when the flag is the final argument', () {
expect(
hasWindowAttached(
[
['thorium', '--user-data-dir=$_profile'],
],
_profile,
),
isTrue,
);
});
test('does not match a sibling profile directory', () {
expect(
hasWindowAttached(
[
['thorium', '--user-data-dir=${_profile}2'],
],
_profile,
),
isFalse,
);
});
test('does not match when no process mentions the profile', () {
expect(
hasWindowAttached(
[
['bash', '-c', 'sleep 1'],
],
_profile,
),
isFalse,
);
});
});
}