From 271b4f8b3275a2b0fcd06ac8aa51b20c8aca404d Mon Sep 17 00:00:00 2001 From: Carsten Grohmann Date: Fri, 25 Aug 2023 09:51:32 +0200 Subject: [PATCH 01/15] Fix output of error message cmp is a string already. Therefore, the join() statement is not necessary and only introduces additional spaces. --- testinfra/utils/ansible_runner.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/testinfra/utils/ansible_runner.py b/testinfra/utils/ansible_runner.py index c65fec31..78a6df22 100644 --- a/testinfra/utils/ansible_runner.py +++ b/testinfra/utils/ansible_runner.py @@ -393,9 +393,7 @@ def run_module( "msg": "Skipped. You might want to try check=False", } if not files: - raise RuntimeError( - "Error while running {}: {}".format(" ".join(cmd), out) - ) + raise RuntimeError(f"{out}") fpath = os.path.join(d, files[0]) try: with open(fpath, "r", encoding="ascii") as f: From 01db77fbe8df63bdfe08e708bf2b425c46cd6782 Mon Sep 17 00:00:00 2001 From: marcandre-larochelle-bell <79320471+marcandre-larochelle-bell@users.noreply.github.com> Date: Mon, 28 Aug 2023 07:47:24 -0400 Subject: [PATCH 02/15] Ansible: Fix for missing group names in get_variables() (#724) --- test/test_backends.py | 4 ++-- test/test_modules.py | 2 +- testinfra/utils/ansible_runner.py | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test_backends.py b/test/test_backends.py index c49b2e71..b805eeb4 100644 --- a/test/test_backends.py +++ b/test/test_backends.py @@ -183,14 +183,14 @@ def get_vars(host): "c": "d", "x": "z", "inventory_hostname": "debian", - "group_names": ["g"], + "group_names": ["all", "g"], "groups": groups, } assert get_vars("rockylinux") == { "a": "a", "e": "f", "inventory_hostname": "rockylinux", - "group_names": ["ungrouped"], + "group_names": ["all", "ungrouped"], "groups": groups, } diff --git a/test/test_modules.py b/test/test_modules.py index e55a23d1..3106479f 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -348,7 +348,7 @@ def test_ansible_module(host): assert variables["myhostvar"] == "bar" assert variables["mygroupvar"] == "qux" assert variables["inventory_hostname"] == "debian_bookworm" - assert variables["group_names"] == ["testgroup"] + assert variables["group_names"] == ["all", "testgroup"] assert variables["groups"] == { "all": ["debian_bookworm"], "testgroup": ["debian_bookworm"], diff --git a/testinfra/utils/ansible_runner.py b/testinfra/utils/ansible_runner.py index 78a6df22..084c03b6 100644 --- a/testinfra/utils/ansible_runner.py +++ b/testinfra/utils/ansible_runner.py @@ -311,12 +311,14 @@ def get_variables(self, host: str) -> dict[str, Any]: hostvars.setdefault("inventory_hostname", host) group_names = [] groups = {} + for group in sorted(inventory): if group == "_meta": continue groups[group] = sorted(itergroup(inventory, group)) - if group != "all" and host in inventory[group].get("hosts", []): + if host in groups[group]: group_names.append(group) + hostvars.setdefault("group_names", group_names) hostvars.setdefault("groups", groups) return hostvars From 1cb19cce7b6fed503b79284af5e147e2910add9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= Date: Mon, 13 Nov 2023 17:16:36 +0100 Subject: [PATCH 03/15] testinfra/modules/blockdevice: Don't fail on stderr (#745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `blockdevice` module raises a `RuntimeError` if `blockdev` returns with a non-zero exit code, or if stderr is not empty. But the latter can happen even when there's no failure at all, for instance if `sshd` has a banner message configured (which gets output to stderr whenever someone connects to the host). Signed-off-by: BenoƮt Knecht --- testinfra/modules/blockdevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testinfra/modules/blockdevice.py b/testinfra/modules/blockdevice.py index 5f129065..fdfd9e0c 100644 --- a/testinfra/modules/blockdevice.py +++ b/testinfra/modules/blockdevice.py @@ -130,7 +130,7 @@ def _data(self): header = ["RO", "RA", "SSZ", "BSZ", "StartSec", "Size", "Device"] command = "blockdev --report %s" blockdev = self.run(command, self.device) - if blockdev.rc != 0 or blockdev.stderr: + if blockdev.rc != 0: raise RuntimeError("Failed to gather data: {}".format(blockdev.stderr)) output = blockdev.stdout.splitlines() if len(output) < 2: From 4807473a5b54f82488ef386c7ad17f91a4a0d83d Mon Sep 17 00:00:00 2001 From: Carsten Grohmann Date: Tue, 24 Oct 2023 21:38:11 +0200 Subject: [PATCH 04/15] Add CommandResult documenation --- doc/source/modules.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/source/modules.rst b/doc/source/modules.rst index ebbb3dc2..84756c8d 100644 --- a/doc/source/modules.rst +++ b/doc/source/modules.rst @@ -284,4 +284,11 @@ User :exclude-members: get_module_class +CommandResult +~~~~~~~~~~~~~ + +.. autoclass:: testinfra.backend.base.CommandResult + :members: + + .. _fixture: https://docs.pytest.org/en/latest/fixture.html#fixture From 2f78038025d292946e2b97c9bde38a522f398265 Mon Sep 17 00:00:00 2001 From: Carsten Grohmann Date: Tue, 24 Oct 2023 21:40:18 +0200 Subject: [PATCH 05/15] Extend CommandResult documentation --- testinfra/backend/base.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/testinfra/backend/base.py b/testinfra/backend/base.py index f62b69e2..6258cdbb 100644 --- a/testinfra/backend/base.py +++ b/testinfra/backend/base.py @@ -34,6 +34,23 @@ class HostSpec: class CommandResult: + """Object that encapsulates all returned details of the command execution. + + Example: + + >>> cmd = host.run("ls -l /etc/passwd") + >>> cmd.rc + 0 + >>> cmd.stdout + '-rw-r--r-- 1 root root 1790 Feb 11 00:28 /etc/passwd\\n' + >>> cmd.stderr + '' + >>> cmd.succeeded + True + >>> cmd.failed + False + """ + def __init__( self, backend: "BaseBackend", @@ -82,24 +99,44 @@ def rc(self) -> int: @property def stdout(self) -> str: + """Gets standard output (stdout) stream of an executed command + + >>> host.run("mkdir -v new_directory").stdout + mkdir: created directory 'new_directory' + """ if self._stdout is None: self._stdout = self._backend.decode(self._stdout_bytes) return self._stdout @property def stderr(self) -> str: + """Gets standard error (stderr) stream of an executed command + + >>> host.run("mkdir new_directory").stderr + mkdir: cannot create directory 'new_directory': File exists + """ if self._stderr is None: self._stderr = self._backend.decode(self._stderr_bytes) return self._stderr @property def stdout_bytes(self) -> bytes: + """Gets standard output (stdout) stream of an executed command as bytes + + >>> host.run("mkdir -v new_directory").stdout_bytes + b"mkdir: created directory 'new_directory'" + """ if self._stdout_bytes is None: self._stdout_bytes = self._backend.encode(self._stdout) return self._stdout_bytes @property def stderr_bytes(self) -> bytes: + """Gets standard error (stderr) stream of an executed command as bytes + + >>> host.run("mkdir new_directory").stderr_bytes + b"mkdir: cannot create directory 'new_directory': File exists" + """ if self._stderr_bytes is None: self._stderr_bytes = self._backend.encode(self._stderr) return self._stderr_bytes From 5d5c51f4c1bc8d63465c55e56866b600d71642b2 Mon Sep 17 00:00:00 2001 From: Carsten Grohmann Date: Thu, 5 Oct 2023 09:31:26 +0200 Subject: [PATCH 06/15] Extend list of valid suffixes for systemd units The old implementation limits the usage of SystemdService.is_valid() to ".service" units. This implementation allows checking all types of unit files with is_valid() (systemd-analyze verify ). --- testinfra/modules/service.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/testinfra/modules/service.py b/testinfra/modules/service.py index 830d066d..47e3e7d0 100644 --- a/testinfra/modules/service.py +++ b/testinfra/modules/service.py @@ -143,6 +143,32 @@ def is_enabled(self): class SystemdService(SysvService): + suffix_list = [ + "service", + "socket", + "device", + "mount", + "automount", + "swap", + "target", + "path", + "timer", + "slice", + "scope", + ] + """ + List of valid suffixes for systemd unit files + + See systemd.unit(5) for more details + """ + + def _has_systemd_suffix(self): + """ + Check if service name has a known systemd unit suffix + """ + unit_suffix = self.name.split(".")[-1] + return unit_suffix in self.suffix_list + @property def is_running(self): out = self.run_expect([0, 1, 3], "systemctl is-active %s", self.name) @@ -164,8 +190,8 @@ def is_enabled(self): @property def is_valid(self): - # systemd-analyze requires a full path. - if self.name.endswith(".service"): + # systemd-analyze requires a full unit name. + if self._has_systemd_suffix(): name = self.name else: name = self.name + ".service" From b54a79ef519094c86a703856442f1fdc461fa124 Mon Sep 17 00:00:00 2001 From: Carsten Grohmann Date: Mon, 9 Oct 2023 07:48:38 +0200 Subject: [PATCH 07/15] Don't execute SysV fallback for systemd units --- testinfra/modules/service.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/testinfra/modules/service.py b/testinfra/modules/service.py index 47e3e7d0..592558b5 100644 --- a/testinfra/modules/service.py +++ b/testinfra/modules/service.py @@ -184,9 +184,15 @@ def is_enabled(self): return True if cmd.stdout.strip() == "disabled": return False - # Fallback on SysV + # Fallback on SysV - only for non-systemd units # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=760616 - return super().is_enabled + if not self._has_systemd_suffix(): + return super().is_enabled + raise RuntimeError( + "Unable to determine state of {0}. Does this service exist?".format( + self.name + ) + ) @property def is_valid(self): @@ -290,9 +296,7 @@ def is_enabled(self): if self.name in self.check_output("rcctl ls off").splitlines(): return False raise RuntimeError( - "Unable to determine state of {0}. Does this service exist?".format( - self.name - ) + f"Unable to determine state of {self.name}. Does this service exist?" ) From 48729032f87c109dcbcf3ae4e7f771689d283a4d Mon Sep 17 00:00:00 2001 From: Andrey Maslennikov Date: Mon, 25 Sep 2023 15:39:00 +0200 Subject: [PATCH 08/15] Add missing Environment doc section Fixes #736. --- doc/source/modules.rst | 6 ++++++ testinfra/modules/environment.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/source/modules.rst b/doc/source/modules.rst index 84756c8d..756cae7c 100644 --- a/doc/source/modules.rst +++ b/doc/source/modules.rst @@ -145,6 +145,12 @@ Docker :members: +Environment +~~~~~~ + +.. autoclass:: testinfra.modules.environment.Environment(name) + :members: + File ~~~~ diff --git a/testinfra/modules/environment.py b/testinfra/modules/environment.py index a762d74e..847127a5 100644 --- a/testinfra/modules/environment.py +++ b/testinfra/modules/environment.py @@ -18,7 +18,7 @@ class Environment(InstanceModule): Example: - >>> host.environment() + >>> host.environment() { "EDITOR": "vim", "SHELL": "/bin/bash", From a1e4c246c2cb7a2212436da4f03f2722550282d6 Mon Sep 17 00:00:00 2001 From: Andrey Maslennikov Date: Mon, 25 Sep 2023 15:52:29 +0200 Subject: [PATCH 09/15] Fix doc title syntax --- doc/source/modules.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/modules.rst b/doc/source/modules.rst index 756cae7c..2a5b001f 100644 --- a/doc/source/modules.rst +++ b/doc/source/modules.rst @@ -146,7 +146,7 @@ Docker Environment -~~~~~~ +~~~~~~~~~~~ .. autoclass:: testinfra.modules.environment.Environment(name) :members: From 0c6eacae6dde8c3c94c539d41c08358f9297068a Mon Sep 17 00:00:00 2001 From: Andrey Maslennikov Date: Fri, 22 Sep 2023 14:58:00 +0000 Subject: [PATCH 10/15] Define types for plugin.py --- testinfra/plugin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/testinfra/plugin.py b/testinfra/plugin.py index 707ff505..d1371dbb 100644 --- a/testinfra/plugin.py +++ b/testinfra/plugin.py @@ -15,7 +15,7 @@ import sys import tempfile import time -from typing import AnyStr +from typing import AnyStr, cast import pytest @@ -25,19 +25,19 @@ @pytest.fixture(scope="module") -def _testinfra_host(request): - return request.param +def _testinfra_host(request: pytest.FixtureRequest) -> testinfra.host.Host: + return cast(testinfra.host.Host, request.param) @pytest.fixture(scope="module") -def host(_testinfra_host): +def host(_testinfra_host: testinfra.host.Host) -> testinfra.host.Host: return _testinfra_host host.__doc__ = testinfra.host.Host.__doc__ -def pytest_addoption(parser): +def pytest_addoption(parser: pytest.Parser) -> None: group = parser.getgroup("testinfra") group.addoption( "--connection", @@ -107,7 +107,7 @@ def pytest_addoption(parser): ) -def pytest_generate_tests(metafunc): +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: if "_testinfra_host" in metafunc.fixturenames: if metafunc.config.option.hosts is not None: hosts = metafunc.config.option.hosts.split(",") @@ -141,7 +141,7 @@ def __init__(self, out): self.total_time = None self.out = out - def pytest_runtest_logreport(self, report): + def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: if report.passed: if report.when == "call": # ignore setup/teardown self.passed += 1 @@ -150,7 +150,7 @@ def pytest_runtest_logreport(self, report): elif report.skipped: self.skipped += 1 - def report(self): + def report(self) -> int: if self.failed: status = b"CRITICAL" ret = 2 From c99c289ceab08bded0bb740a643bec4e9b9ab12c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 20:27:51 +0000 Subject: [PATCH 11/15] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- .github/workflows/tox.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a6c22e0..4b64a3ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: PY_COLORS: 1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # needed by setuptools-scm - name: Switch to using Python 3.9 by default diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index f6543111..5a7526fb 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -26,7 +26,7 @@ jobs: matrix: python-version: ["3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -45,7 +45,7 @@ jobs: matrix: toxenv: [docs, packaging, py39] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.9 uses: actions/setup-python@v4 with: From 5af81c85a3ebfbe16d256008424ced355dd2cfc8 Mon Sep 17 00:00:00 2001 From: 700grm <42538950+700grm@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:38:26 +0000 Subject: [PATCH 12/15] Missing RHEL distribution in package module (#731) Added "ol" Oracle Linux and moved "rhel" accordingly --------- Co-authored-by: 100gr --- testinfra/modules/package.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testinfra/modules/package.py b/testinfra/modules/package.py index 9eb19759..ed113d81 100644 --- a/testinfra/modules/package.py +++ b/testinfra/modules/package.py @@ -77,8 +77,10 @@ def get_module_class(cls, host): "centos", "cloudlinux", "fedora", + "ol", "opensuse-leap", "opensuse-tumbleweed", + "rhel", "rocky", ) ): From 70554d35ee120bd905a780ed5863777330d85d9b Mon Sep 17 00:00:00 2001 From: Soof Golan Date: Sat, 28 Oct 2023 23:39:32 +0200 Subject: [PATCH 13/15] feat: brew support --- testinfra/modules/package.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/testinfra/modules/package.py b/testinfra/modules/package.py index ed113d81..39ef248d 100644 --- a/testinfra/modules/package.py +++ b/testinfra/modules/package.py @@ -9,6 +9,7 @@ # 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 json from testinfra.modules.base import Module @@ -31,6 +32,7 @@ def is_installed(self): - apk (Alpine) - apt (Debian, Ubuntu, ...) + - brew (macOS) - pacman (Arch, Manjaro ) - pkg (FreeBSD) - pkg_info (NetBSD) @@ -94,6 +96,8 @@ def get_module_class(cls, host): return DebianPackage if host.exists("rpm"): return RpmPackage + if host.exists("brew"): + return HomebrewPackage raise NotImplementedError @@ -216,3 +220,20 @@ def version(self): @property def release(self): raise NotImplementedError + + +class HomebrewPackage(Package): + @property + def is_installed(self): + info = self.check_output("brew info --formula --json %s", self.name) + return len(json.loads(info)[0]["installed"]) > 0 + + @property + def version(self): + info = self.check_output("brew info --formula --json %s", self.name) + version = json.loads(info)[0]["installed"][0]["version"] + return version + + @property + def release(self): + raise NotImplementedError From 76170a4f30a4ba6f5843bf1ccaacbd5d6f0be25d Mon Sep 17 00:00:00 2001 From: Carsten Grohmann Date: Mon, 9 Oct 2023 10:05:02 +0200 Subject: [PATCH 14/15] Add Service.exists --- testinfra/modules/service.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/testinfra/modules/service.py b/testinfra/modules/service.py index 592558b5..413b2433 100644 --- a/testinfra/modules/service.py +++ b/testinfra/modules/service.py @@ -33,6 +33,11 @@ def __init__(self, name): self.name = name super().__init__() + @property + def exists(self): + """Test if service is exists""" + raise NotImplementedError + @property def is_running(self): """Test if service is running""" @@ -169,6 +174,11 @@ def _has_systemd_suffix(self): unit_suffix = self.name.split(".")[-1] return unit_suffix in self.suffix_list + @property + def exists(self): + cmd = self.run_test('systemctl list-unit-files | grep -q"^%s"', self.name) + return cmd.rc == 0 + @property def is_running(self): out = self.run_expect([0, 1, 3], "systemctl is-active %s", self.name) @@ -227,6 +237,10 @@ def systemd_properties(self): class UpstartService(SysvService): + @property + def exists(self): + return self._host.file(f"/etc/init/{self.name}.conf").exists + @property def is_enabled(self): if ( @@ -269,6 +283,10 @@ def is_enabled(self): class FreeBSDService(Service): + @property + def exists(self): + return self._host.file(f"/etc/rc.d/{self.name}").exists + @property def is_running(self): return self.run_test("service %s onestatus", self.name).rc == 0 @@ -285,6 +303,10 @@ def is_enabled(self): class OpenBSDService(Service): + @property + def exists(self): + return self._host.file(f"/etc/rc.d/{self.name}").exists + @property def is_running(self): return self.run_test("/etc/rc.d/%s check", self.name).rc == 0 @@ -301,6 +323,10 @@ def is_enabled(self): class NetBSDService(Service): + @property + def exists(self): + return self._host.file(f"/etc/rc.d/{self.name}").exists + @property def is_running(self): return self.run_test("/etc/rc.d/%s onestatus", self.name).rc == 0 @@ -311,6 +337,13 @@ def is_enabled(self): class WindowsService(Service): + @property + def exists(self): + out = self.check_output( + f"Get-Service -Name {self.name} -ErrorAction SilentlyContinue" + ) + return self.name in out + @property def is_running(self): return ( From dc48cd98f3b1970ecbad63f866a94f8283b79ce4 Mon Sep 17 00:00:00 2001 From: Andrey Maslennikov Date: Mon, 13 Nov 2023 18:13:13 +0100 Subject: [PATCH 15/15] Make CommandResult a dataclass (#722) --- testinfra/backend/ansible.py | 8 ++--- testinfra/backend/base.py | 68 ++++++++++++++---------------------- testinfra/backend/salt.py | 4 +-- testinfra/backend/winrm.py | 2 +- 4 files changed, 32 insertions(+), 50 deletions(-) diff --git a/testinfra/backend/ansible.py b/testinfra/backend/ansible.py index 3e202df2..61bf25f8 100644 --- a/testinfra/backend/ansible.py +++ b/testinfra/backend/ansible.py @@ -58,11 +58,9 @@ def run(self, command: str, *args: str, **kwargs: Any) -> base.CommandResult: out = self.run_ansible("shell", module_args=command, check=False) return self.result( out["rc"], - command, - stdout_bytes=None, - stderr_bytes=None, - stdout=out["stdout"], - stderr=out["stderr"], + self.encode(command), + out["stdout"], + out["stderr"], ) def run_ansible( diff --git a/testinfra/backend/base.py b/testinfra/backend/base.py index 6258cdbb..2aefab94 100644 --- a/testinfra/backend/base.py +++ b/testinfra/backend/base.py @@ -17,7 +17,7 @@ import shlex import subprocess import urllib.parse -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Optional, Union if TYPE_CHECKING: import testinfra.host @@ -33,6 +33,7 @@ class HostSpec: password: Optional[str] +@dataclasses.dataclass class CommandResult: """Object that encapsulates all returned details of the command execution. @@ -51,24 +52,11 @@ class CommandResult: False """ - def __init__( - self, - backend: "BaseBackend", - exit_status: int, - command: bytes, - stdout_bytes: bytes, - stderr_bytes: bytes, - stdout: Optional[str] = None, - stderr: Optional[str] = None, - ): - self.exit_status = exit_status - self._stdout_bytes = stdout_bytes - self._stderr_bytes = stderr_bytes - self._stdout = stdout - self._stderr = stderr - self.command = command - self._backend = backend - super().__init__() + backend: "BaseBackend" + exit_status: int + command: bytes + _stdout: Union[str, bytes] + _stderr: Union[str, bytes] @property def succeeded(self) -> bool: @@ -104,8 +92,8 @@ def stdout(self) -> str: >>> host.run("mkdir -v new_directory").stdout mkdir: created directory 'new_directory' """ - if self._stdout is None: - self._stdout = self._backend.decode(self._stdout_bytes) + if isinstance(self._stdout, bytes): + return self.backend.decode(self._stdout) return self._stdout @property @@ -115,8 +103,8 @@ def stderr(self) -> str: >>> host.run("mkdir new_directory").stderr mkdir: cannot create directory 'new_directory': File exists """ - if self._stderr is None: - self._stderr = self._backend.decode(self._stderr_bytes) + if isinstance(self._stderr, bytes): + return self.backend.decode(self._stderr) return self._stderr @property @@ -126,9 +114,9 @@ def stdout_bytes(self) -> bytes: >>> host.run("mkdir -v new_directory").stdout_bytes b"mkdir: created directory 'new_directory'" """ - if self._stdout_bytes is None: - self._stdout_bytes = self._backend.encode(self._stdout) - return self._stdout_bytes + if isinstance(self._stdout, str): + return self.backend.encode(self._stdout) + return self._stdout @property def stderr_bytes(self) -> bytes: @@ -137,19 +125,9 @@ def stderr_bytes(self) -> bytes: >>> host.run("mkdir new_directory").stderr_bytes b"mkdir: cannot create directory 'new_directory': File exists" """ - if self._stderr_bytes is None: - self._stderr_bytes = self._backend.encode(self._stderr) - return self._stderr_bytes - - def __repr__(self) -> str: - return ( - "CommandResult(command={!r}, exit_status={}, stdout={!r}, " "stderr={!r})" - ).format( - self.command, - self.exit_status, - self._stdout_bytes or self._stdout, - self._stderr_bytes or self._stderr, - ) + if isinstance(self._stderr, str): + return self.backend.encode(self._stderr) + return self._stderr class BaseBackend(metaclass=abc.ABCMeta): @@ -337,7 +315,15 @@ def encode(self, data: str) -> bytes: except UnicodeEncodeError: return data.encode(self.encoding) - def result(self, *args: Any, **kwargs: Any) -> CommandResult: - result = CommandResult(self, *args, **kwargs) + def result( + self, rc: int, cmd: bytes, stdout: Union[str, bytes], stderr: Union[str, bytes] + ) -> CommandResult: + result = CommandResult( + backend=self, + exit_status=rc, + command=cmd, + _stdout=stdout, + _stderr=stderr, + ) logger.debug("RUN %s", result) return result diff --git a/testinfra/backend/salt.py b/testinfra/backend/salt.py index 985cc334..6fadf210 100644 --- a/testinfra/backend/salt.py +++ b/testinfra/backend/salt.py @@ -40,9 +40,7 @@ def run(self, command: str, *args: str, **kwargs: Any) -> base.CommandResult: out = self.run_salt("cmd.run_all", [command]) return self.result( out["retcode"], - command, - out["stdout"].encode("utf8"), - out["stderr"].encode("utf8"), + self.encode(command), stdout=out["stdout"], stderr=out["stderr"], ) diff --git a/testinfra/backend/winrm.py b/testinfra/backend/winrm.py index f801f894..c6ce3bf6 100644 --- a/testinfra/backend/winrm.py +++ b/testinfra/backend/winrm.py @@ -88,7 +88,7 @@ def run_winrm(self, command: str, *args: str) -> base.CommandResult: stdout, stderr, rc = p.get_command_output(shell_id, command_id) p.cleanup_command(shell_id, command_id) p.close_shell(shell_id) - return self.result(rc, command, stdout, stderr) + return self.result(rc, self.encode(command), stdout, stderr) @staticmethod def quote(command: str, *args: str) -> str: