Skip to content
2 changes: 2 additions & 0 deletions tests/provision/mock/main.fmf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require+:
- tmt+provision-mock
tag+:
- provision-only
- provision-mock
2 changes: 1 addition & 1 deletion tmt/package_managers/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 47 additions & 23 deletions tmt/steps/provision/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,28 +185,35 @@ 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
assert self.mock_shell is not 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))
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(f'\necho {finished_keyword}\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 `finished_keyword`.
while True:
Comment thread
thrix marked this conversation as resolved.
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:
loop -= 1
self.mock_shell.stdout.read()
if fileno == self.mock_shell_stdout_fd and self.mock_shell.stdout.read().endswith(
f'{finished_keyword}\n'
):
break
else:
continue
break
for line in self.mock_shell.stderr.readlines():
self.parent.debug('mock', line.rstrip(), color='blue', level=2)

Expand Down Expand Up @@ -416,23 +423,44 @@ 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:
# TODO
# 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 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)
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:
Comment thread
LecrisUT marked this conversation as resolved.
# 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:
# 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
# the stderr so just discard it.
self.mock_shell.stderr.read()
elif fileno == stdout_fd:
content = os.read(stdout_fd, 128)
Expand All @@ -444,12 +472,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())

Comment thread
LecrisUT marked this conversation as resolved.
stdout = stream_out.string
stderr = stream_err.string
Expand Down Expand Up @@ -677,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(
Expand Down
Loading