diff --git a/test/features/meshtastic/mesh_chat_controller_test.dart b/test/features/meshtastic/mesh_chat_controller_test.dart index 88a3936aa..977c934f0 100644 --- a/test/features/meshtastic/mesh_chat_controller_test.dart +++ b/test/features/meshtastic/mesh_chat_controller_test.dart @@ -61,6 +61,23 @@ void main() { Future settle() => Future.delayed(const Duration(milliseconds: 150)); + /// Waits until [store] holds [expected] messages. + /// + /// `_add` writes fire-and-forget and only adopts a message into the in-memory + /// list once the insert answers (mesh_chat_controller.dart:322-328), so both + /// halves of what these tests assert land *after* the controller has been + /// handed the packet. A fixed delay is therefore a guess about how fast the + /// machine is: 150 ms was enough on a laptop and not on CI, where the last + /// dozen inserts arrived after the test had already closed the database and + /// the failure read as `This database has already been closed`. + Future drained(MeshStore store, int expected) async { + final deadline = DateTime.now().add(const Duration(seconds: 10)); + while (DateTime.now().isBefore(deadline)) { + if ((await store.messages(limit: 100000)).length >= expected) return; + await Future.delayed(const Duration(milliseconds: 10)); + } + } + test('keeps the newest messages first', () async { final (controller, service, _) = await makeController(); for (var i = 0; i < 10; i++) { @@ -78,7 +95,7 @@ void main() { for (var i = 0; i < MeshChatController.windowSize + 20; i++) { service.messages.add(message('m$i', seconds: i)); } - await settle(); + await drained(store, MeshChatController.windowSize + 20); expect(controller.messages, hasLength(MeshChatController.windowSize)); // The store keeps everything — the window is a view, not a retention cap. @@ -91,7 +108,7 @@ void main() { test('persists the log and reloads it after a restart', () async { final (controller, service, store) = await makeController(); service.messages.add(message('hello')); - await settle(); + await drained(store, 1); controller.dispose(); final (restored, _, _) = await makeController(store); diff --git a/test/tool/colorize_logs_test.dart b/test/tool/colorize_logs_test.dart index 103fcaeae..5d97bc6a7 100644 --- a/test/tool/colorize_logs_test.dart +++ b/test/tool/colorize_logs_test.dart @@ -15,17 +15,21 @@ const _esc = 27; /// Runs the script over [input]. Without a pty, stdout is not a terminal. String run(String input, {bool tty = false}) { final script = '${Directory.current.path}/tool/internal/colorize_logs.sh'; + final piped = 'printf %s ${_quote(input)} | $script'; + // `script` lends the pipeline a pty, which is the only way to exercise the + // branch that decides whether to emit anything at all — and its argument + // order is the opposite on the two platforms this repository builds on. + // BSD takes the typescript file first and the command after it; util-linux + // wants the command behind `-c` and the file last. Written for BSD only, it + // passed on a laptop and hung Linux CI into a failure with no message. final result = tty - // `script` lends the pipeline a pty, which is the only way to exercise - // the branch that decides whether to emit anything at all. - ? Process.runSync('script', [ - '-q', - '/dev/null', - 'bash', - '-c', - 'printf %s ${_quote(input)} | $script', - ]) - : Process.runSync('bash', ['-c', 'printf %s ${_quote(input)} | $script']); + ? Process.runSync( + 'script', + Platform.isMacOS + ? ['-q', '/dev/null', 'bash', '-c', piped] + : ['-q', '-c', piped, '/dev/null'], + ) + : Process.runSync('bash', ['-c', piped]); expect(result.exitCode, 0, reason: result.stderr.toString()); return result.stdout.toString(); } diff --git a/tool/commit.sh b/tool/commit.sh index dbcac3837..46f7820f0 100755 --- a/tool/commit.sh +++ b/tool/commit.sh @@ -301,7 +301,13 @@ if ((run_gates)); then note 'inputs, so an unchanged tree costs seconds instead of a minute.' printf '\n' if tool/check.sh; then - ok 'every gate passed — this is what CI will do' + ok "every gate passed, on $(uname -s)" + # Said, because the difference has already cost a red CI: this runs CI's + # command list on *this* machine, and the runner is Linux. A test that + # shells out meets a different sed, a different awk and a different + # `script`, and passes here while failing there. + [[ "$(uname -s)" == Linux ]] || + note 'CI runs Linux — a test that shells out can still differ there' else block 'a gate failed — CI will fail the same way' fi