Skip to content

Commit 28f5034

Browse files
committed
Make test infra optional
1 parent 4a7314e commit 28f5034

File tree

5 files changed

+86
-26
lines changed

5 files changed

+86
-26
lines changed

README.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Find the latest documentation on `dcermak.github.io/pytest_container
2121
<https://dcermak.github.io/pytest_container/>`_.
2222

2323
``pytest_container`` is a `pytest <https://pytest.org>`_ plugin
24-
to test container images via pytest fixtures and `testinfra
24+
to test container images via pytest fixtures and optionally `testinfra
2525
<https://testinfra.readthedocs.io/en/latest/>`_. It takes care of all the boring
2626
tasks, like spinning up containers, finding free ports and cleaning up after
2727
tests, and allows you to focus on implementing the actual tests.
@@ -55,8 +55,7 @@ instantiating a ``Container`` and parametrizing a test function with the
5555
The fixture automatically pulls and spins up the container, stops it and removes
5656
it after the test is completed. Your test function receives an instance of
5757
``ContainerData`` with the ``ContainerData.connection`` attribute. The
58-
``ContainerData.connection`` attribute is a `testinfra
59-
<https://testinfra.readthedocs.io/en/latest/>`_ connection object. It can be
58+
``ContainerData.connection`` attribute is a shell connection object. It can be
6059
used to run basic tests inside the container itself. For example, you can check
6160
whether files are present, packages are installed, etc.
6261

pytest_container/container.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import operator
1212
import os
1313
import socket
14+
import subprocess
1415
import sys
1516
import tempfile
1617
import time
@@ -43,10 +44,10 @@
4344
import _pytest.mark
4445
import deprecation
4546
import pytest
46-
import testinfra
4747
from filelock import BaseFileLock
4848
from filelock import FileLock
4949

50+
from pytest_container import helpers
5051
from pytest_container.helpers import get_always_pull_option
5152
from pytest_container.helpers import get_extra_build_args
5253
from pytest_container.helpers import get_extra_run_args
@@ -877,7 +878,7 @@ def prepare_container(
877878
@dataclass(frozen=True)
878879
class ContainerData:
879880
"""Class returned by the ``*container*`` fixtures to the test function. It
880-
contains information about the launched container and the testinfra
881+
contains information about the launched container and its shell connection
881882
:py:attr:`connection` to the running container.
882883
883884
"""
@@ -887,7 +888,7 @@ class ContainerData:
887888
image_url_or_id: str
888889
#: ID of the started container
889890
container_id: str
890-
#: the testinfra connection to the running container
891+
#: the shell connection to the running container
891892
connection: Any
892893
#: the container data class that has been used in this test
893894
container: Union[Container, DerivedContainer]
@@ -1013,6 +1014,27 @@ def container_from_pytest_param(
10131014
raise ValueError(f"Invalid pytest.param values: {param.values}")
10141015

10151016

1017+
@dataclass(frozen=True)
1018+
class ContainerRemoteEndpoint:
1019+
_container_id: str
1020+
_runtime: OciRuntimeBase
1021+
1022+
def __post_init__(self) -> None:
1023+
assert self._container_id, "Container ID must not be empty"
1024+
1025+
def check_output(self, cmd: str, strip: bool = True) -> str:
1026+
"""Run a command in the container and return its output."""
1027+
return helpers.run_command(
1028+
[
1029+
self._runtime.runner_binary,
1030+
"exec",
1031+
self._container_id,
1032+
"/bin/sh",
1033+
"-c",
1034+
cmd,
1035+
],
1036+
)[1]
1037+
10161038
@dataclass
10171039
class ContainerLauncher:
10181040
"""Helper context manager to setup, start and teardown a container including
@@ -1174,12 +1196,22 @@ def container_data(self) -> ContainerData:
11741196
"""
11751197
if not self._container_id:
11761198
raise RuntimeError(f"Container {self.container} has not started")
1199+
connection: Any = None
1200+
try:
1201+
import testinfra
1202+
1203+
connection = testinfra.get_host(
1204+
f"{self.container_runtime.runner_binary}://{self._container_id}"
1205+
)
1206+
except ImportError:
1207+
connection = ContainerRemoteEndpoint(
1208+
self._container_id, self.container_runtime
1209+
)
1210+
11771211
return ContainerData(
11781212
image_url_or_id=self.container.url or self.container.container_id,
11791213
container_id=self._container_id,
1180-
connection=testinfra.get_host(
1181-
f"{self.container_runtime.runner_binary}://{self._container_id}"
1182-
),
1214+
connection=connection,
11831215
container=self.container,
11841216
forwarded_ports=self._new_port_forwards,
11851217
_container_runtime=self.container_runtime,

pytest_container/helpers.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
import logging
88
import os
9+
import subprocess
910
from typing import List
1011

1112
from _pytest.config import Config
1213
from _pytest.config.argparsing import Parser
1314
from _pytest.python import Metafunc
1415

16+
from pytest_container.logging import _logger
1517
from pytest_container.logging import set_internal_logging_level
1618

1719

@@ -157,3 +159,26 @@ def get_always_pull_option() -> bool:
157159
158160
"""
159161
return bool(int(os.getenv("PULL_ALWAYS", "1")))
162+
163+
164+
def run_command(
165+
cmd: List[str],
166+
ignore_errors=True,
167+
):
168+
try:
169+
result = subprocess.run(
170+
cmd,
171+
capture_output=True,
172+
text=True,
173+
check=not ignore_errors,
174+
)
175+
176+
_logger.debug(
177+
f"RUN CMD: {cmd} RC: {result.returncode} STDOUT: {result.stdout} STDERR:{result.stderr}"
178+
)
179+
return result.returncode, result.stdout, result.stderr
180+
except subprocess.CalledProcessError as exc:
181+
_logger.debug(
182+
f"RUN(Failed) CMD: {cmd} RC: {exc.returncode} STDOUT:{exc.stdout} STDERR:{exc.stderr}"
183+
)
184+
raise exc

pytest_container/runtime.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import json
88
import re
9+
import shutil
910
import sys
1011
from abc import ABC
1112
from abc import abstractmethod
@@ -20,10 +21,11 @@
2021
from typing import Optional
2122
from typing import Union
2223

23-
import testinfra
24+
2425
from _pytest.mark.structures import ParameterSet
2526
from pytest import param
2627

28+
from pytest_container import helpers
2729
from pytest_container.inspect import BindMount
2830
from pytest_container.inspect import Config
2931
from pytest_container.inspect import ContainerHealth
@@ -411,9 +413,6 @@ def __str__(self) -> str:
411413
return self.__class__.__name__
412414

413415

414-
LOCALHOST = testinfra.host.get_host("local://")
415-
416-
417416
def _get_podman_version(version_stdout: str) -> Version:
418417
if version_stdout[:15] != "podman version ":
419418
raise RuntimeError(
@@ -424,7 +423,7 @@ def _get_podman_version(version_stdout: str) -> Version:
424423

425424

426425
def _get_buildah_version() -> Version:
427-
version_stdout = LOCALHOST.check_output("buildah --version")
426+
version_stdout = helpers.run_command(["buildah", "--version"])[1]
428427
build_version_begin = "buildah version "
429428
if not version_stdout.startswith(build_version_begin):
430429
raise RuntimeError(
@@ -443,11 +442,11 @@ class PodmanRuntime(OciRuntimeBase):
443442
"""
444443

445444
def __init__(self) -> None:
446-
podman_ps = LOCALHOST.run("podman ps")
447-
if not podman_ps.succeeded:
448-
raise RuntimeError(f"`podman ps` failed with {podman_ps.stderr}")
445+
podman_ps = helpers.run_command(["podman", "ps"])
446+
if not podman_ps[0] == 0:
447+
raise RuntimeError(f"`podman ps` failed with {podman_ps[2]}")
449448

450-
self._buildah_functional = LOCALHOST.run("buildah").succeeded
449+
self._buildah_functional = helpers.run_command(["buildah"])[0] == 0
451450
super().__init__(
452451
build_command=(
453452
["buildah", "bud", "--layers", "--force-rm"]
@@ -461,8 +460,11 @@ def __init__(self) -> None:
461460
@cached_property
462461
def version(self) -> Version:
463462
"""Returns the version of podman installed on the system"""
463+
464464
return _get_podman_version(
465-
LOCALHOST.run_expect([0], "podman --version").stdout
465+
helpers.run_command(["podman", "--version"], ignore_errors=False)[
466+
1
467+
]
466468
)
467469

468470
@cached_property
@@ -539,9 +541,9 @@ class DockerRuntime(OciRuntimeBase):
539541
containers."""
540542

541543
def __init__(self) -> None:
542-
docker_ps = LOCALHOST.run("docker ps")
543-
if not docker_ps.succeeded:
544-
raise RuntimeError(f"`docker ps` failed with {docker_ps.stderr}")
544+
docker_ps = helpers.run_command(["docker", "ps"])
545+
if not docker_ps[0] == 0:
546+
raise RuntimeError(f"`docker ps` failed with {docker_ps[2]}")
545547

546548
super().__init__(
547549
build_command=["docker", "build", "--force-rm"],
@@ -552,7 +554,9 @@ def __init__(self) -> None:
552554
def version(self) -> Version:
553555
"""Returns the version of docker installed on this system"""
554556
return _get_docker_version(
555-
LOCALHOST.run_expect([0], "docker --version").stdout
557+
helpers.run_command(["docker", "--version"], ignore_errors=False)[
558+
1
559+
]
556560
)
557561

558562
@property
@@ -613,8 +617,8 @@ def get_selected_runtime() -> OciRuntimeBase:
613617
614618
If neither docker nor podman are available, then a ValueError is raised.
615619
"""
616-
podman_exists = LOCALHOST.exists("podman")
617-
docker_exists = LOCALHOST.exists("docker")
620+
podman_exists = shutil.which("podman")
621+
docker_exists = shutil.which("docker")
618622

619623
runtime_choice = getenv("CONTAINER_RUNTIME", "podman").lower()
620624
if runtime_choice not in ("podman", "docker"):

source/prerequisites.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Prerequisites
22
=============
33

4-
`pytest_container` works with Python 3.6 and later and requires `pytest
4+
`pytest_container` works with Python 3.6 and later and optionally requires `pytest
55
<https://pytest.org/>`_ and `pytest-testinfra
66
<https://testinfra.readthedocs.io/>`_. Additionally, for python 3.6, you'll need
77
the `dataclasses <https://pypi.org/project/dataclasses/>`_ module.

0 commit comments

Comments
 (0)