From 9393211b8acf94ee83d8300549f8bd4ada5a595f Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 11 Mar 2024 11:21:11 +0100 Subject: [PATCH] testutil: tweak `mock_command` to write a call_log Instead of just mocking the binary also write a log of the way it got called so that tests can use this to check if the right options are passed. Note that the API should be improved here, instead of returning a "naked" path to the calllog file there should be a class wrapping it. And of course there should be tests. --- osbuild/testutil/__init__.py | 45 ++++++++++++++++++++++++-- test/mod/test_testutil_mock_command.py | 16 +++++++-- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/osbuild/testutil/__init__.py b/osbuild/testutil/__init__.py index f6c4eb6b..a5b69327 100644 --- a/osbuild/testutil/__init__.py +++ b/osbuild/testutil/__init__.py @@ -8,6 +8,7 @@ import re import shutil import subprocess import tempfile +import textwrap def has_executable(executable: str) -> bool: @@ -62,20 +63,58 @@ def assert_jsonschema_error_contains(res, expected_err, expected_num_errs=None): for err_msg in err_msgs), f"{expected_err} not found in {err_msgs}" +class MockCommandCallArgs: + """MockCommandCallArgs provides the arguments a mocked command + was called with. + + Use :call_args_list: to get a list of calls and each of these calls + will have the argv[1:] from the mocked binary. + """ + + def __init__(self, calllog_path): + self._calllog = pathlib.Path(calllog_path) + + @property + def call_args_list(self): + call_arg_list = [] + for acall in self._calllog.read_text(encoding="utf8").split("\n\n"): + if acall: + call_arg_list.append(acall.split("\n")) + return call_arg_list + + @contextlib.contextmanager def mock_command(cmd_name: str, script: str): """ mock_command creates a mocked binary with the given :cmd_name: and :script: - content. This is useful to e.g. mock errors from binaries. + content. This is useful to e.g. mock errors from binaries or validate that + external binaries are called in the right way. + + It returns a MockCommandCallArgs class that can be used to inspect the + way the binary was called. """ original_path = os.environ["PATH"] with tempfile.TemporaryDirectory() as tmpdir: cmd_path = pathlib.Path(tmpdir) / cmd_name - cmd_path.write_text(script, encoding="utf8") + cmd_calllog_path = pathlib.Path(os.fspath(cmd_path) + ".calllog") + # This is a little bit naive right now, if args contains \n things + # will break. easy enough to fix by using \0 as the separator but + # then \n in args is kinda rare + fake_cmd_content = textwrap.dedent(f"""\ + #!/bin/sh -e + + for arg in "$@"; do + echo "$arg" >> {cmd_calllog_path} + done + # extra separator to differenciate between calls + echo "" >> {cmd_calllog_path} + + """) + script + cmd_path.write_text(fake_cmd_content, encoding="utf8") cmd_path.chmod(0o755) os.environ["PATH"] = f"{tmpdir}:{original_path}" try: - yield + yield MockCommandCallArgs(cmd_calllog_path) finally: os.environ["PATH"] = original_path diff --git a/test/mod/test_testutil_mock_command.py b/test/mod/test_testutil_mock_command.py index 5e332512..5c1eb72b 100644 --- a/test/mod/test_testutil_mock_command.py +++ b/test/mod/test_testutil_mock_command.py @@ -12,16 +12,28 @@ def test_mock_command_integration(): output = subprocess.check_output(["echo", "hello"]) assert output == b"hello\n" fake_echo = textwrap.dedent("""\ - #!/bin/sh echo i-am-not-echo """) - with mock_command("echo", fake_echo): + with mock_command("echo", fake_echo) as mocked_cmd: output = subprocess.check_output(["echo", "hello"]) assert output == b"i-am-not-echo\n" + assert mocked_cmd.call_args_list == [ + ["hello"], + ] output = subprocess.check_output(["echo", "hello"]) assert output == b"hello\n" +def test_mock_command_multi(): + with mock_command("echo", "") as mocked_cmd: + subprocess.check_output(["echo", "call1-arg1", "call1-arg2"]) + subprocess.check_output(["echo", "call2-arg1", "call2-arg2"]) + assert mocked_cmd.call_args_list == [ + ["call1-arg1", "call1-arg2"], + ["call2-arg1", "call2-arg2"], + ] + + def test_mock_command_environ_is_modified_and_restored(): orig_path = os.environ["PATH"] with mock_command("something", "#!/bin/sh\ntrue\n"):