From b9fc3ef550f92d188e9769e13ad2661badbec4a9 Mon Sep 17 00:00:00 2001 From: j4n Date: Thu, 27 Aug 2026 14:32:19 +0200 Subject: [PATCH] tests: start the journalctl tail before it is needed iter_output() waited until its caller's first next() call to spawn the journalctl tail, which could lead to log traffic being missed, hanging next() forever: start the tail right away instead. --- .../src/cmdeploy/tests/online/test_1_basic.py | 3 +- cmdeploy/src/cmdeploy/tests/plugin.py | 5 +++ .../cmdeploy/tests/test_remote_iter_output.py | 34 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 cmdeploy/src/cmdeploy/tests/test_remote_iter_output.py diff --git a/cmdeploy/src/cmdeploy/tests/online/test_1_basic.py b/cmdeploy/src/cmdeploy/tests/online/test_1_basic.py index 9a32ef7a..60c3987d 100644 --- a/cmdeploy/src/cmdeploy/tests/online/test_1_basic.py +++ b/cmdeploy/src/cmdeploy/tests/online/test_1_basic.py @@ -13,7 +13,8 @@ from cmdeploy.cmdeploy import get_sshexec class TestSSHExecutor: @pytest.fixture(scope="class") - def sshexec(self, sshdomain): + @classmethod + def sshexec(cls, sshdomain): return get_sshexec(sshdomain) def test_ls(self, sshexec): diff --git a/cmdeploy/src/cmdeploy/tests/plugin.py b/cmdeploy/src/cmdeploy/tests/plugin.py index e2350284..031abf30 100644 --- a/cmdeploy/src/cmdeploy/tests/plugin.py +++ b/cmdeploy/src/cmdeploy/tests/plugin.py @@ -411,6 +411,7 @@ class Remote: self._procs = [] def iter_output(self, logcmd="", ready=None): + # run popen run here so the tail is live before caller triggers getjournal = "journalctl -f" if not logcmd else logcmd print(self.sshdomain) if self.sshdomain in ("@local", "localhost"): @@ -425,6 +426,10 @@ class Remote: stderr=subprocess.DEVNULL, ) self._procs.append(popen) + return self._read_lines(popen, ready) + + @staticmethod + def _read_lines(popen, ready): try: while 1: line = popen.stdout.readline() diff --git a/cmdeploy/src/cmdeploy/tests/test_remote_iter_output.py b/cmdeploy/src/cmdeploy/tests/test_remote_iter_output.py new file mode 100644 index 00000000..aeb865ac --- /dev/null +++ b/cmdeploy/src/cmdeploy/tests/test_remote_iter_output.py @@ -0,0 +1,34 @@ +from cmdeploy.tests.plugin import Remote + + +def _script(tmp_path, name, body): + # iter_output splits its command string on whitespace, so any command + # with embedded spaces has to live in a file, not an inline string. + path = tmp_path / name + path.write_text(f"#!/bin/sh\n{body}\n") + path.chmod(0o755) + return str(path) + + +def test_iter_output_spawns_before_first_next(tmp_path): + """The tail process must be live before the caller triggers or fast-moving logs can be missed""" + script = _script(tmp_path, "late.sh", "sleep 0.2\necho late-line") + remote = Remote("@local") + lineproducer = remote.iter_output(script) + assert remote._procs, "Popen must run on iter_output() itself, not on first next()" + assert remote._procs[0].poll() is None + assert next(lineproducer) == "late-line" + remote.close() + + +def test_iter_output_ready_fires_after_first_line(tmp_path): + """ready() must wait for the first real line, not fire at spawn time""" + script = _script(tmp_path, "trigger.sh", "echo backlog\nsleep 0.2\necho trigger-fired") + remote = Remote("@local") + calls = [] + lineproducer = remote.iter_output(script, ready=lambda: calls.append(1)) + assert calls == [] + assert next(lineproducer) == "backlog" + assert calls == [1] + assert next(lineproducer) == "trigger-fired" + remote.close()