diff --git a/doc/source/Subprocess.rst b/doc/source/Subprocess.rst index 2eaebf4..c28506f 100644 --- a/doc/source/Subprocess.rst +++ b/doc/source/Subprocess.rst @@ -16,6 +16,7 @@ API: Subprocess :type log_mask_re: typing.Optional[str] .. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd + .. versionchanged:: 3.1.0 Not singleton anymore. Only lock is shared between all instances. .. py:attribute:: log_mask_re diff --git a/exec_helpers/__init__.py b/exec_helpers/__init__.py index a548d14..ad052b4 100644 --- a/exec_helpers/__init__.py +++ b/exec_helpers/__init__.py @@ -51,7 +51,7 @@ "async_api", ) -__version__ = "3.0.0" +__version__ = "3.1.0" __author__ = "Alexey Stepanov" __author_email__ = "penguinolog@gmail.com" __maintainers__ = { diff --git a/exec_helpers/async_api/subprocess_runner.py b/exec_helpers/async_api/subprocess_runner.py index 429ff61..2083f70 100644 --- a/exec_helpers/async_api/subprocess_runner.py +++ b/exec_helpers/async_api/subprocess_runner.py @@ -63,7 +63,7 @@ def stdout(self) -> typing.Optional[asyncio.StreamReader]: # type: ignore return super(SubprocessExecuteAsyncResult, self).stdout -class Subprocess(api.ExecHelper, metaclass=metaclasses.SingletonMeta): +class Subprocess(api.ExecHelper, metaclass=metaclasses.SingleLock): """Subprocess helper with timeouts and lock-free FIFO.""" __slots__ = () @@ -76,6 +76,8 @@ def __init__(self, logger: logging.Logger = logger, log_mask_re: typing.Optional :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + + .. versionchanged:: 3.1.0 Not singleton anymore. Only lock is shared between all instances. """ super(Subprocess, self).__init__(logger=logger, log_mask_re=log_mask_re) diff --git a/exec_helpers/metaclasses.py b/exec_helpers/metaclasses.py index df3cb62..89f428d 100644 --- a/exec_helpers/metaclasses.py +++ b/exec_helpers/metaclasses.py @@ -47,3 +47,34 @@ def __prepare__( # pylint: disable=unused-argument .. versionadded:: 1.2.0 """ return collections.OrderedDict() + + +class SingleLock(abc.ABCMeta): + """Metaclass for creating classes with single lock instance per class.""" + + def __init__(cls, name: str, bases: typing.Tuple[type, ...], namespace: typing.Dict[str, typing.Any]) -> None: + """Create lock object for class.""" + super(SingleLock, cls).__init__(name, bases, namespace) + cls.__lock = threading.RLock() + + def __new__( + mcs, name: str, bases: typing.Tuple[type, ...], namespace: typing.Dict[str, typing.Any], **kwargs: typing.Any + ) -> typing.Type: + """Create lock property for class instances.""" + namespace["lock"] = property(fget=lambda self: self.__class__.lock) + return super().__new__(mcs, name, bases, namespace, **kwargs) # type: ignore + + @property + def lock(cls) -> threading.RLock: + """Lock property for class.""" + return cls.__lock + + @classmethod + def __prepare__( # pylint: disable=unused-argument + mcs: typing.Type["SingleLock"], name: str, bases: typing.Iterable[typing.Type], **kwargs: typing.Any + ) -> collections.OrderedDict: + """Metaclass magic for object storage. + + .. versionadded:: 1.2.0 + """ + return collections.OrderedDict() diff --git a/exec_helpers/subprocess_runner.py b/exec_helpers/subprocess_runner.py index 25e29fe..475dfb4 100644 --- a/exec_helpers/subprocess_runner.py +++ b/exec_helpers/subprocess_runner.py @@ -61,7 +61,7 @@ def stdout(self) -> typing.Optional[typing.IO]: # type: ignore return super(SubprocessExecuteAsyncResult, self).stdout -class Subprocess(api.ExecHelper, metaclass=metaclasses.SingletonMeta): +class Subprocess(api.ExecHelper, metaclass=metaclasses.SingleLock): """Subprocess helper with timeouts and lock-free FIFO.""" def __init__(self, log_mask_re: typing.Optional[str] = None) -> None: @@ -74,6 +74,7 @@ def __init__(self, log_mask_re: typing.Optional[str] = None) -> None: :type log_mask_re: typing.Optional[str] .. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd + .. versionchanged:: 3.1.0 Not singleton anymore. Only lock is shared between all instances. """ super(Subprocess, self).__init__(logger=logger, log_mask_re=log_mask_re) diff --git a/test/async_api/test_subprocess.py b/test/async_api/test_subprocess.py index 7cf475a..32a463c 100644 --- a/test/async_api/test_subprocess.py +++ b/test/async_api/test_subprocess.py @@ -22,7 +22,6 @@ import pytest import exec_helpers -from exec_helpers import metaclasses from exec_helpers import _subprocess_helpers # All test coroutines will be treated as marked. @@ -256,7 +255,6 @@ async def test_002_execute(create_subprocess_shell, logger, exec_result, run_par async def test_003_context_manager(monkeypatch, create_subprocess_shell, logger, exec_result, run_parameters) -> None: - metaclasses.SingletonMeta._instances.clear() # prepare lock = asynctest.CoroutineMock() lock.attach_mock(asynctest.CoroutineMock("acquire"), "acquire") lock.attach_mock(mock.Mock("release"), "release") diff --git a/test/async_api/test_subprocess_special.py b/test/async_api/test_subprocess_special.py index 2bc8ab5..1e6f229 100644 --- a/test/async_api/test_subprocess_special.py +++ b/test/async_api/test_subprocess_special.py @@ -91,6 +91,16 @@ async def read_stream(stream: FakeFileStream): "stdin_error": dict(ec=(0xDEADBEEF,), stdout=(), stdin="Failed", write=OSError(), expect_exc=OSError), "stdin_close_closed": dict(stdout=(b" \n", b"2\n", b"3\n", b" \n"), stdin="Stdin", stdin_close=eshutdown_exc), "stdin_close_fail": dict(ec=(0xDEADBEEF,), stdout=(), stdin="Failed", stdin_close=OSError(), expect_exc=OSError), + "mask_global": dict( + command="USE='secret=secret_pass' do task", + init_log_mask_re=r"secret\s*=\s*([A-Z-a-z0-9_\-]+)", + masked_cmd="USE='secret=<*masked*>' do task", + ), + "mask_local": dict( + command="USE='secret=secret_pass' do task", + log_mask_re=r"secret\s*=\s*([A-Z-a-z0-9_\-]+)", + masked_cmd="USE='secret=<*masked*>' do task", + ), } @@ -112,6 +122,8 @@ def pytest_generate_tests(metafunc): "stdin_error", "stdin_close_closed", "stdin_close_fail", + "mask_global", + "mask_local", ], indirect=True, ) @@ -131,7 +143,7 @@ def exec_result(run_parameters): stdout_res = tuple([elem for elem in run_parameters["stdout"] if isinstance(elem, bytes)]) return exec_helpers.async_api.ExecResult( - cmd=command, + cmd=run_parameters.get("masked_cmd", command), stdin=run_parameters.get("stdin", None), stdout=stdout_res, stderr=(), @@ -190,16 +202,24 @@ def logger(mocker): async def test_special_cases(create_subprocess_shell, exec_result, logger, run_parameters) -> None: - runner = exec_helpers.async_api.Subprocess() + runner = exec_helpers.async_api.Subprocess(log_mask_re=run_parameters.get("init_log_mask_re", None)) if "expect_exc" not in run_parameters: res = await runner.execute( - command, stdin=run_parameters.get("stdin", None), verbose=run_parameters.get("verbose", None) + command=run_parameters.get("command", command), + stdin=run_parameters.get("stdin", None), + verbose=run_parameters.get("verbose", None), + log_mask_re=run_parameters.get("log_mask_re", None), ) level = logging.INFO if run_parameters.get("verbose", False) else logging.DEBUG - assert logger.mock_calls[0] == mock.call.log(level=level, msg=command_log) - assert logger.mock_calls[-1] == mock.call.log( - level=level, msg="Command {result.cmd!r} exit code: {result.exit_code!s}".format(result=res) + + command_for_log = run_parameters.get("masked_cmd", command) + command_log = "Executing command:\n{!r}\n".format(command_for_log.rstrip()) + result_log = "Command {command!r} exit code: {result.exit_code!s}".format( + command=command_for_log.rstrip(), result=res ) + + assert logger.mock_calls[0] == mock.call.log(level=level, msg=command_log) + assert logger.mock_calls[-1] == mock.call.log(level=level, msg=result_log) assert res == exec_result else: with pytest.raises(run_parameters["expect_exc"]): diff --git a/test/test_ssh_client_init.py b/test/test_ssh_client_init.py index 2ebb18f..3560155 100644 --- a/test/test_ssh_client_init.py +++ b/test/test_ssh_client_init.py @@ -53,7 +53,6 @@ def __iter__(self): port = 22 username = "user" password = "pass" -private_keys = [] # noinspection PyTypeChecker diff --git a/test/test_sshauth.py b/test/test_sshauth.py index 5a7f58c..3966718 100644 --- a/test/test_sshauth.py +++ b/test/test_sshauth.py @@ -16,29 +16,42 @@ # pylint: disable=no-self-use -import base64 import contextlib import copy import io import typing -import unittest -import mock import paramiko +import pytest import exec_helpers -def gen_private_keys(amount=1): +def gen_private_keys(amount: int = 1) -> typing.List[paramiko.RSAKey]: return [paramiko.RSAKey.generate(1024) for _ in range(amount)] -def gen_public_key(private_key=None): +def gen_public_key(private_key: typing.Optional[paramiko.RSAKey] = None) -> str: if private_key is None: private_key = paramiko.RSAKey.generate(1024) return "{0} {1}".format(private_key.get_name(), private_key.get_base64()) +def get_internal_keys( + key: typing.Optional[paramiko.RSAKey] = None, + keys: typing.Optional[typing.Iterable[paramiko.RSAKey]] = None, + **kwargs +): + int_keys = [None] + if key is not None: + int_keys.append(key) + if keys is not None: + for k in keys: + if k not in int_keys: + int_keys.append(k) + return int_keys + + class FakeStream: def __init__(self, *args): self.__src = list(args) @@ -50,113 +63,89 @@ def __iter__(self): yield self.__src.pop(0) -host = "127.0.0.1" -port = 22 username = "user" password = "pass" -private_keys = [] -command = "ls ~\nline 2\nline 3\nline с кирилицей" -command_log = "Executing command:\n{!s}\n".format(command.rstrip()) -stdout_list = [b" \n", b"2\n", b"3\n", b" \n"] -stdout_str = b"".join(stdout_list).strip().decode("utf-8") -stderr_list = [b" \n", b"0\n", b"1\n", b" \n"] -stderr_str = b"".join(stderr_list).strip().decode("utf-8") -encoded_cmd = base64.b64encode("{}\n".format(command).encode("utf-8")).decode("utf-8") - - -# noinspection PyTypeChecker -class TestSSHAuth(unittest.TestCase): - def tearDown(self): - with mock.patch("warnings.warn"): - exec_helpers.SSHClient._clear_cache() - - def init_checks( - self, - username=None, - password=None, - key=None, - keys=None, - key_filename: typing.Union[typing.List[str], str, None] = None, - passphrase: typing.Optional[str] = None, - ): - """shared positive init checks - - :type username: str - :type password: str - :type key: paramiko.RSAKey - :type keys: list - :type key_filename: typing.Union[typing.List[str], str, None] - :type passphrase: typing.Optional[str] - """ - auth = exec_helpers.SSHAuth( - username=username, password=password, key=key, keys=keys, key_filename=key_filename, passphrase=passphrase - ) - - int_keys = [None] - if key is not None: - int_keys.append(key) - if keys is not None: - for k in keys: - if k not in int_keys: - int_keys.append(k) - - self.assertEqual(auth.username, username) - with contextlib.closing(io.StringIO()) as tgt: - auth.enter_password(tgt) - self.assertEqual(tgt.getvalue(), "{}\n".format(password)) - self.assertEqual(auth.public_key, gen_public_key(key) if key is not None else None) - - _key = None if auth.public_key is None else "".format(auth.public_key) - _keys = [] - for k in int_keys: - if k == key: - continue - _keys.append("".format(gen_public_key(k)) if k is not None else None) - - self.assertEqual( - repr(auth), - "{cls}(" - "username={auth.username!r}, " - "password=<*masked*>, " - "key={key}, " - "keys={keys}, " - "key_filename={auth.key_filename!r}, " - "passphrase=<*masked*>," - ")".format(cls=exec_helpers.SSHAuth.__name__, auth=auth, key=_key, keys=_keys), - ) - self.assertEqual( - str(auth), "{cls} for {username}".format(cls=exec_helpers.SSHAuth.__name__, username=auth.username) - ) - - def test_init_username_only(self): - self.init_checks(username=username) - def test_init_username_password(self): - self.init_checks(username=username, password=password) - def test_init_username_key(self): - self.init_checks(username=username, key=gen_private_keys(1).pop()) - - def test_init_username_password_key(self): - self.init_checks(username=username, password=password, key=gen_private_keys(1).pop()) - - def test_init_username_password_keys(self): - self.init_checks(username=username, password=password, keys=gen_private_keys(2)) - - def test_init_username_password_key_keys(self): - self.init_checks(username=username, password=password, key=gen_private_keys(1).pop(), keys=gen_private_keys(2)) - - def test_equality_copy(self): - """Equality is calculated using hash, copy=deepcopy.""" - auth1 = exec_helpers.SSHAuth(username="username") - - auth2 = exec_helpers.SSHAuth(username="username") +configs = { + "username_only": dict(username=username), + "username_password": dict(username=username, password=password), + "username_key": dict(username=username, key=gen_private_keys(1).pop()), + "username_password_key": dict(username=username, password=password, key=gen_private_keys(1).pop()), + "username_password_keys": dict(username=username, password=password, keys=gen_private_keys(2)), + "username_password_key_keys": dict( + username=username, password=password, key=gen_private_keys(1).pop(), keys=gen_private_keys(2) + ), +} + + +def pytest_generate_tests(metafunc): + if "run_parameters" in metafunc.fixturenames: + metafunc.parametrize( + "run_parameters", + [ + "username_only", + "username_password", + "username_key", + "username_password_key", + "username_password_keys", + "username_password_key_keys", + ], + indirect=True, + ) - auth3 = exec_helpers.SSHAuth(username="username_differs") - self.assertEqual(auth1, auth2) - self.assertNotEqual(auth1, auth3) - self.assertEqual(auth3, copy.copy(auth3)) - self.assertIsNot(auth3, copy.copy(auth3)) - self.assertEqual(auth3, copy.deepcopy(auth3)) - self.assertIsNot(auth3, copy.deepcopy(auth3)) +@pytest.fixture +def run_parameters(request): + return configs[request.param] + + +def test_001_init_checks(run_parameters) -> None: + auth = exec_helpers.SSHAuth(**run_parameters) + int_keys = get_internal_keys(**run_parameters) + + assert auth.username == username + with contextlib.closing(io.StringIO()) as tgt: + auth.enter_password(tgt) + assert tgt.getvalue() == "{}\n".format(run_parameters.get("password", None)) + + key = run_parameters.get("key", None) + if key is not None: + assert auth.public_key == gen_public_key(key) + else: + assert auth.public_key is None + + _key = None if auth.public_key is None else "".format(auth.public_key) + _keys = [] + for k in int_keys: + if k == key: + continue + _keys.append("".format(gen_public_key(k)) if k is not None else None) + + assert repr(auth) == ( + "{cls}(" + "username={auth.username!r}, " + "password=<*masked*>, " + "key={key}, " + "keys={keys}, " + "key_filename={auth.key_filename!r}, " + "passphrase=<*masked*>," + ")".format(cls=exec_helpers.SSHAuth.__name__, auth=auth, key=_key, keys=_keys) + ) + assert str(auth) == "{cls} for {username}".format(cls=exec_helpers.SSHAuth.__name__, username=auth.username) + + +def test_002_equality_copy(): + """Equality is calculated using hash, copy=deepcopy.""" + auth1 = exec_helpers.SSHAuth(username="username") + + auth2 = exec_helpers.SSHAuth(username="username") + + auth3 = exec_helpers.SSHAuth(username="username_differs") + + assert auth1 == auth2 + assert auth1 != auth3 + assert auth3 == copy.copy(auth3) + assert auth3 is not copy.copy(auth3) + assert auth3 == copy.deepcopy(auth3) + assert auth3 is not copy.deepcopy(auth3) diff --git a/test/test_subprocess_runner.py b/test/test_subprocess_runner.py deleted file mode 100644 index 08cbeaa..0000000 --- a/test/test_subprocess_runner.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright 2018 Alexey Stepanov aka penguinolog. - -# Copyright 2016 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging -import subprocess -import typing -import unittest - -import mock - -import exec_helpers -from exec_helpers import metaclasses -from exec_helpers._subprocess_helpers import subprocess_kw - -command = "ls ~\nline 2\nline 3\nline с кирилицей" -command_log = "Executing command:\n{!r}\n".format(command.rstrip()) -stdout_list = [b" \n", b"2\n", b"3\n", b" \n"] -stderr_list = [b" \n", b"0\n", b"1\n", b" \n"] -print_stdin = 'read line; echo "$line"' -default_timeout = 60 * 60 # 1 hour - - -class FakeFileStream: - """Mock-like object for stream emulation.""" - - def __init__(self, *args): - self.__src = list(args) - self.closed = False - - def __iter__(self): - """Normally we iter over source.""" - for _ in range(len(self.__src)): - yield self.__src.pop(0) - - def fileno(self): - return hash(tuple(self.__src)) - - def close(self): - """We enforce close.""" - self.closed = True - - -@mock.patch("psutil.Process", autospec=True) -@mock.patch("exec_helpers.subprocess_runner.logger", autospec=True) -@mock.patch("subprocess.Popen", autospec=True, name="subprocess.Popen") -class TestSubprocessRunner(unittest.TestCase): - def setUp(self): - """Set up tests.""" - metaclasses.SingletonMeta._instances.clear() - - @staticmethod - def prepare_close( - popen: mock.MagicMock, - cmd: str = command, - stderr_val=None, - ec=0, - open_stdout=True, - stdout_override=None, - open_stderr=True, - cmd_in_result=None, - stdin=None, - ) -> typing.Tuple[mock.Mock, exec_helpers.ExecResult]: - if open_stdout: - stdout_lines = stdout_list if stdout_override is None else stdout_override - stdout = FakeFileStream(*stdout_lines) - else: - stdout = stdout_lines = None - if open_stderr: - stderr_lines = stderr_list if stderr_val is None else [] - stderr = FakeFileStream(*stderr_lines) - else: - stderr = stderr_lines = None - - popen_obj = mock.Mock() - if stdout: - popen_obj.attach_mock(stdout, "stdout") - else: - popen_obj.configure_mock(stdout=None) - if stderr: - popen_obj.attach_mock(stderr, "stderr") - else: - popen_obj.configure_mock(stderr=None) - popen_obj.attach_mock(mock.Mock(return_value=ec), "poll") - popen_obj.attach_mock(mock.Mock(return_value=ec), "wait") - popen_obj.configure_mock(returncode=ec) - - popen.return_value = popen_obj - - # noinspection PyTypeChecker - exp_result = exec_helpers.ExecResult( - cmd=cmd_in_result if cmd_in_result is not None else cmd, - stderr=stderr_lines, - stdout=stdout_lines, - exit_code=ec, - stdin=stdin, - ) - - return popen_obj, exp_result - - @staticmethod - def gen_cmd_result_log_message(result: exec_helpers.ExecResult) -> str: - """Exclude copy-pasting.""" - return "Command {result.cmd!r} exit code: {result.exit_code!s}".format(result=result) - - def test_008_execute_mask_global(self, popen: mock.MagicMock, logger: mock.MagicMock, *args): - cmd = "USE='secret=secret_pass' do task" - log_mask_re = r"secret\s*=\s*([A-Z-a-z0-9_\-]+)" - masked_cmd = "USE='secret=<*masked*>' do task" - cmd_log = "Executing command:\n{!r}\n".format(masked_cmd) - - popen_obj, exp_result = self.prepare_close(popen, cmd=cmd, cmd_in_result=masked_cmd) - - runner = exec_helpers.Subprocess(log_mask_re=log_mask_re) - - # noinspection PyTypeChecker - result = runner.execute(cmd) - self.assertEqual(result, exp_result) - popen.assert_has_calls( - ( - mock.call( - args=[cmd], - cwd=None, - env=None, - shell=True, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - universal_newlines=False, - **subprocess_kw - ), - ) - ) - - self.assertEqual(logger.mock_calls[0], mock.call.log(level=logging.DEBUG, msg=cmd_log)) - self.assertEqual( - logger.mock_calls[-1], mock.call.log(level=logging.DEBUG, msg=self.gen_cmd_result_log_message(result)) - ) - - self.assertIn(mock.call.wait(timeout=default_timeout), popen_obj.mock_calls) - - def test_009_execute_mask_local(self, popen: mock.MagicMock, logger: mock.MagicMock, *args): - cmd = "USE='secret=secret_pass' do task" - log_mask_re = r"secret\s*=\s*([A-Z-a-z0-9_\-]+)" - masked_cmd = "USE='secret=<*masked*>' do task" - cmd_log = "Executing command:\n{!r}\n".format(masked_cmd) - - popen_obj, exp_result = self.prepare_close(popen, cmd=cmd, cmd_in_result=masked_cmd) - - runner = exec_helpers.Subprocess() - - # noinspection PyTypeChecker - result = runner.execute(cmd, log_mask_re=log_mask_re) - self.assertEqual(result, exp_result) - popen.assert_has_calls( - ( - mock.call( - args=[cmd], - cwd=None, - env=None, - shell=True, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - universal_newlines=False, - **subprocess_kw - ), - ) - ) - self.assertEqual(logger.mock_calls[0], mock.call.log(level=logging.DEBUG, msg=cmd_log)) - self.assertEqual( - logger.mock_calls[-1], mock.call.log(level=logging.DEBUG, msg=self.gen_cmd_result_log_message(result)) - ) - self.assertIn(mock.call.wait(timeout=default_timeout), popen_obj.mock_calls) diff --git a/test/test_subprocess_special.py b/test/test_subprocess_special.py index b01a30c..b3ed7fc 100644 --- a/test/test_subprocess_special.py +++ b/test/test_subprocess_special.py @@ -26,7 +26,6 @@ # All test coroutines will be treated as marked. command = "ls ~\nline 2\nline 3\nline с кирилицей" -command_log = "Executing command:\n{!r}\n".format(command.rstrip()) print_stdin = 'read line; echo "$line"' default_timeout = 60 * 60 # 1 hour @@ -91,6 +90,16 @@ def read_stream(stream: FakeFileStream): "stdin_error": dict(ec=(0xDEADBEEF,), stdout=(), stdin="Failed", write=OSError(), expect_exc=OSError), "stdin_close_closed": dict(stdout=(b" \n", b"2\n", b"3\n", b" \n"), stdin="Stdin", stdin_close=eshutdown_exc), "stdin_close_fail": dict(ec=(0xDEADBEEF,), stdout=(), stdin="Failed", stdin_close=OSError(), expect_exc=OSError), + "mask_global": dict( + command="USE='secret=secret_pass' do task", + init_log_mask_re=r"secret\s*=\s*([A-Z-a-z0-9_\-]+)", + masked_cmd="USE='secret=<*masked*>' do task", + ), + "mask_local": dict( + command="USE='secret=secret_pass' do task", + log_mask_re=r"secret\s*=\s*([A-Z-a-z0-9_\-]+)", + masked_cmd="USE='secret=<*masked*>' do task", + ), } @@ -112,6 +121,8 @@ def pytest_generate_tests(metafunc): "stdin_error", "stdin_close_closed", "stdin_close_fail", + "mask_global", + "mask_local", ], indirect=True, ) @@ -131,7 +142,7 @@ def exec_result(run_parameters): stdout_res = tuple([elem for elem in run_parameters["stdout"] if isinstance(elem, bytes)]) return exec_helpers.ExecResult( - cmd=command, + cmd=run_parameters.get("masked_cmd", command), stdin=run_parameters.get("stdin", None), stdout=stdout_res, stderr=(), @@ -186,16 +197,24 @@ def logger(mocker): def test_special_cases(create_subprocess_shell, exec_result, logger, run_parameters) -> None: - runner = exec_helpers.Subprocess() + runner = exec_helpers.Subprocess(log_mask_re=run_parameters.get("init_log_mask_re", None)) if "expect_exc" not in run_parameters: res = runner.execute( - command, stdin=run_parameters.get("stdin", None), verbose=run_parameters.get("verbose", None) + command=run_parameters.get("command", command), + stdin=run_parameters.get("stdin", None), + verbose=run_parameters.get("verbose", None), + log_mask_re=run_parameters.get("log_mask_re", None), ) level = logging.INFO if run_parameters.get("verbose", False) else logging.DEBUG - assert logger.mock_calls[0] == mock.call.log(level=level, msg=command_log) - assert logger.mock_calls[-1] == mock.call.log( - level=level, msg="Command {result.cmd!r} exit code: {result.exit_code!s}".format(result=res) + + command_for_log = run_parameters.get("masked_cmd", command) + command_log = "Executing command:\n{!r}\n".format(command_for_log.rstrip()) + result_log = "Command {command!r} exit code: {result.exit_code!s}".format( + command=command_for_log.rstrip(), result=res ) + + assert logger.mock_calls[0] == mock.call.log(level=level, msg=command_log) + assert logger.mock_calls[-1] == mock.call.log(level=level, msg=result_log) assert res == exec_result else: with pytest.raises(run_parameters["expect_exc"]):