From 570971aab116f8af2a7019a852d01bf88ca11ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Kon=C4=8Dek?= Date: Wed, 17 Dec 2025 14:13:25 +0100 Subject: [PATCH 1/8] mock provision: make detection of finished processes more deterministic --- tmt/steps/provision/mock.py | 46 +++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/tmt/steps/provision/mock.py b/tmt/steps/provision/mock.py index 1c4465cfeb..c435c8ebe6 100644 --- a/tmt/steps/provision/mock.py +++ b/tmt/steps/provision/mock.py @@ -196,16 +196,21 @@ def _simple_execute(self, *commands: str) -> None: assert self.mock_shell.stderr is not None self.mock_shell.stdin.write(''.join(command + '\n' for command in commands)) + # Issue a command writing a binary zero on the standard output after all + # the previous commands are finished. + self.mock_shell.stdin.write('echo -e \\\\x00\n') self.mock_shell.stdin.flush() - # Wait until the previous commands finished. - loop = len(commands) - while loop != 0 and self.mock_shell.poll() is None: + # Wait until we read the binary zero from stdout. + loop = True + while loop and self.mock_shell.poll() is None: events = self.epoll.poll() for fileno, _ in events: - if fileno == self.mock_shell_stdout_fd: - loop -= 1 - self.mock_shell.stdout.read() + if ( + fileno == self.mock_shell_stdout_fd + and self.mock_shell.stdout.read() == '\x00\n' + ): + loop = False break for line in self.mock_shell.stderr.readlines(): self.parent.debug('mock', line.rstrip(), color='blue', level=2) @@ -423,16 +428,23 @@ def _spawn_command( # kill the process spawned inside the mock shell pass - # The command is finished when mock shell prints a newline on its - # stdout. We want to break loop after we handled all the other - # epoll events because the event ordering is not guaranteed. - if len(events) == 1 and events[0][0] == self.mock_shell_stdout_fd: - self.mock_shell.stdout.read() + # The command is finished when the returncode is written to its + # file. + # We want to break loop after we handled all the other epoll + # events because the event ordering is not guaranteed. + if len(events) == 1 and events[0][0] == returncode_fd: + content = os.read(returncode_fd, 16) + returncode = int(content.decode('utf-8').strip()) + returncode_io.try_unregister() break for fileno, _ in events: - # Whatever we sent on mock shell's input it prints on the stderr - # so just discard it. - if fileno == self.mock_shell_stderr_fd: + if fileno == self.mock_shell_stdout_fd: + # Various environments may print variable number of + # times here. + self.mock_shell.stdout.read() + elif fileno == self.mock_shell_stderr_fd: + # Whatever we sent on mock shell's input it prints on + # the stderr so just discard it. self.mock_shell.stderr.read() elif fileno == stdout_fd: content = os.read(stdout_fd, 128) @@ -444,12 +456,6 @@ def _spawn_command( stream_err += content if not content: stderr_io.try_unregister() - elif fileno == returncode_fd: - content = os.read(returncode_fd, 16) - if not content: - returncode_io.try_unregister() - else: - returncode = int(content.decode('utf-8').strip()) stdout = stream_out.string stderr = stream_err.string From 9dbec15de08db5ace19a91b93dadd5fe584b04a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Kon=C4=8Dek?= Date: Thu, 26 Feb 2026 10:39:51 +0100 Subject: [PATCH 2/8] mock provision: Refactor --- tmt/steps/provision/mock.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tmt/steps/provision/mock.py b/tmt/steps/provision/mock.py index c435c8ebe6..168f35f3a2 100644 --- a/tmt/steps/provision/mock.py +++ b/tmt/steps/provision/mock.py @@ -185,8 +185,6 @@ def _simple_execute(self, *commands: str) -> None: Invoke commands in the mock shell without checking for errors. This is done by writing the commands to the shell, ending each with a newline. - Each command invocation causes the shell to print a newline when the - command has finished. """ assert self.epoll is not None @@ -194,24 +192,29 @@ def _simple_execute(self, *commands: str) -> None: assert self.mock_shell.stdin is not None assert self.mock_shell.stdout is not None assert self.mock_shell.stderr is not None + finished_keyword = 'TMT_FINISHED_EXEC' - self.mock_shell.stdin.write(''.join(command + '\n' for command in commands)) - # Issue a command writing a binary zero on the standard output after all + self.mock_shell.stdin.write('\n'.join(commands)) + # Issue a command writing a terminator on the standard output after all # the previous commands are finished. - self.mock_shell.stdin.write('echo -e \\\\x00\n') + self.mock_shell.stdin.write(f'\necho {finished_keyword}\n') self.mock_shell.stdin.flush() # Wait until we read the binary zero from stdout. - loop = True - while loop and self.mock_shell.poll() is None: + while True: + mock_shell_result = self.mock_shell.poll() + if mock_shell_result is not None: + raise tmt.utils.ProvisionError(f'Mock shell abruptly exited: {mock_shell_result}.') events = self.epoll.poll() for fileno, _ in events: if ( fileno == self.mock_shell_stdout_fd - and self.mock_shell.stdout.read() == '\x00\n' + and self.mock_shell.stdout.read() == f'{finished_keyword}\n' ): - loop = False break + else: + continue + break for line in self.mock_shell.stderr.readlines(): self.parent.debug('mock', line.rstrip(), color='blue', level=2) @@ -439,8 +442,7 @@ def _spawn_command( break for fileno, _ in events: if fileno == self.mock_shell_stdout_fd: - # Various environments may print variable number of - # times here. + # Mock prints newlines on stdout. self.mock_shell.stdout.read() elif fileno == self.mock_shell_stderr_fd: # Whatever we sent on mock shell's input it prints on From 4189ebe99ddab44b2238a0c3a0ec81f9051c1d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Kon=C4=8Dek?= Date: Mon, 16 Mar 2026 09:24:53 +0100 Subject: [PATCH 3/8] mock provision: Make command finish detection more reliable --- tmt/steps/provision/mock.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tmt/steps/provision/mock.py b/tmt/steps/provision/mock.py index 168f35f3a2..629b5025cc 100644 --- a/tmt/steps/provision/mock.py +++ b/tmt/steps/provision/mock.py @@ -200,16 +200,15 @@ def _simple_execute(self, *commands: str) -> None: self.mock_shell.stdin.write(f'\necho {finished_keyword}\n') self.mock_shell.stdin.flush() - # Wait until we read the binary zero from stdout. + # Wait until we read the `finished_keyword`. while True: mock_shell_result = self.mock_shell.poll() if mock_shell_result is not None: raise tmt.utils.ProvisionError(f'Mock shell abruptly exited: {mock_shell_result}.') events = self.epoll.poll() for fileno, _ in events: - if ( - fileno == self.mock_shell_stdout_fd - and self.mock_shell.stdout.read() == f'{finished_keyword}\n' + if fileno == self.mock_shell_stdout_fd and self.mock_shell.stdout.read().endswith( + f'{finished_keyword}\n' ): break else: From c4f3dc19b6c2dbe2b68be0ec50b6df996c0efd0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Kon=C4=8Dek?= Date: Mon, 16 Mar 2026 14:13:21 +0100 Subject: [PATCH 4/8] mock provision: Improve code-level documentation --- tmt/steps/provision/mock.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tmt/steps/provision/mock.py b/tmt/steps/provision/mock.py index 629b5025cc..c424229da1 100644 --- a/tmt/steps/provision/mock.py +++ b/tmt/steps/provision/mock.py @@ -423,6 +423,15 @@ def _spawn_command( yield # type: ignore[misc] while self.mock_shell.poll() is None: + # The `epoll.poll` call returns a list of pairs of all the + # events, that are currently ready. The pair consists of the + # file descriptor and the event type (we only registered + # select.EPOLLIN - ready for reading). + # + # `epoll.poll` is level-based: each time it is invoked, it + # returns a list of descriptors which are ready and it will + # contain those descriptors until we actually read from the + # descriptor. events = self.epoll.poll(timeout=timeout) if len(events) == 0: @@ -432,8 +441,9 @@ def _spawn_command( # The command is finished when the returncode is written to its # file. - # We want to break loop after we handled all the other epoll - # events because the event ordering is not guaranteed. + # We want to break loop after we handled all the epoll events + # other than `returncode` because the event ordering is not + # guaranteed. if len(events) == 1 and events[0][0] == returncode_fd: content = os.read(returncode_fd, 16) returncode = int(content.decode('utf-8').strip()) From 5205f491a08edd9886fa3a3c8a90885d317281cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Kon=C4=8Dek?= Date: Mon, 16 Mar 2026 14:16:32 +0100 Subject: [PATCH 5/8] mock provision: Improve mock shell missing returncode handling --- tmt/steps/provision/mock.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tmt/steps/provision/mock.py b/tmt/steps/provision/mock.py index c424229da1..8d1458076c 100644 --- a/tmt/steps/provision/mock.py +++ b/tmt/steps/provision/mock.py @@ -446,8 +446,13 @@ def _spawn_command( # guaranteed. if len(events) == 1 and events[0][0] == returncode_fd: content = os.read(returncode_fd, 16) - returncode = int(content.decode('utf-8').strip()) - returncode_io.try_unregister() + try: + returncode = int(content.decode('ascii').strip()) + except ValueError: + # Missing `returncode` is handled outside of the loop. + break + finally: + returncode_io.try_unregister() break for fileno, _ in events: if fileno == self.mock_shell_stdout_fd: From 7c7e86189f2bad31e77d5a6cc04d9e719c8589f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Kon=C4=8Dek?= Date: Fri, 20 Mar 2026 12:10:06 +0100 Subject: [PATCH 6/8] mock provision: Implement delete option on file push --- tmt/steps/provision/mock.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tmt/steps/provision/mock.py b/tmt/steps/provision/mock.py index 8d1458076c..bfa6ed9361 100644 --- a/tmt/steps/provision/mock.py +++ b/tmt/steps/provision/mock.py @@ -699,6 +699,8 @@ def push( source = source or self.plan_workdir destination = destination or source + if options.delete: + self.mock_shell.execute(Command('rm', '-rf', str(destination)), logger=self._logger) if source.is_dir(): self.mock_shell.execute(Command('mkdir', '-p', str(destination)), logger=self._logger) p = self.mock_shell._spawn_command( From 2bd720009e75664afb5dd5e70fe5bde423124814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Kon=C4=8Dek?= Date: Fri, 27 Mar 2026 11:03:45 +0100 Subject: [PATCH 7/8] mock provision: Add tmt-mock test requirement --- tests/provision/mock/main.fmf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/provision/mock/main.fmf b/tests/provision/mock/main.fmf index 0cb4e636c6..d7a030b928 100644 --- a/tests/provision/mock/main.fmf +++ b/tests/provision/mock/main.fmf @@ -1,3 +1,5 @@ +require+: + - tmt+provision-mock tag+: - provision-only - provision-mock From ad0bd1f134df657e75d7485511c6840edc7b6284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Kon=C4=8Dek?= Date: Fri, 27 Mar 2026 11:04:15 +0100 Subject: [PATCH 8/8] mock provision: Raise mock package manager probe priority over bootc --- tmt/package_managers/mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmt/package_managers/mock.py b/tmt/package_managers/mock.py index 28a947c051..e2cfeb7b53 100644 --- a/tmt/package_managers/mock.py +++ b/tmt/package_managers/mock.py @@ -92,7 +92,7 @@ class _MockPackageManager(PackageManager[MockEngine]): """ probe_command = Command('/usr/bin/false') - probe_priority = 130 + probe_priority = 140 _engine_class = MockEngine # Implementation "stolen" from the dnf package manager family. It should