Skip to content

Commit

Permalink
Add new fixture container_image
Browse files Browse the repository at this point in the history
  • Loading branch information
dcermak committed Aug 27, 2024
1 parent 6905b76 commit aebac9d
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 16 deletions.
12 changes: 6 additions & 6 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,12 @@ Improvements and new features:
Container Images exposing the same ports in parallel without marking them as
``singleton=True``.

- The attribute :py:attr:`~pytest_container.container.ContainerData.container`
was added to :py:class:`~pytest_container.container.ContainerData` (the
datastructure that is passed to test functions via the ``*container*``
fixtures). This attribute contains the
:py:class:`~pytest_container.container.ContainerBase` that was used to
parametrize this test run.
- The attribute ``ContainerData.container`` (is now
:py:attr:`~pytest_container.container.ContainerImageData.container`) was added
to :py:class:`~pytest_container.container.ContainerData` (the datastructure
that is passed to test functions via the ``*container*`` fixtures). This
attribute contains the :py:class:`~pytest_container.container.ContainerBase`
that was used to parametrize this test run.

- Add support to add tags to container images via
``DerivedContainer.add_build_tags`` (is now called
Expand Down
103 changes: 93 additions & 10 deletions pytest_container/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,20 +526,39 @@ def get_launch_cmd(
self,
container_runtime: OciRuntimeBase,
extra_run_args: Optional[List[str]] = None,
detach: bool = True,
interactive_tty: bool = True,
remove: bool = False,
) -> List[str]:
"""Returns the command to launch this container image.
Args:
container_runtime: The container runtime to be used to launch this
container
extra_run_args: optional list of arguments that are added to the
launch command directly after the ``run -d``.
detach: flag whether to launch the container with ``-d``
(i.e. in the background). Defaults to ``True``.
interactive: flag whether to launch the container with ``--it``
(i.e. in interactive mode and attach a pseudo TTY). Defaults to
``True``.
remove: flag whether to launch the container with the ``--rm`` flag
(i.e. it will be auto-removed on after stopping). Defaults to
``False``.
Returns:
The command to launch the container image described by this class
instance as a list of strings that can be fed directly to
:py:class:`subprocess.Popen` as the ``args`` parameter.
"""
cmd = (
[container_runtime.runner_binary, "run", "-d"]
[container_runtime.runner_binary, "run"]
+ (["-d"] if detach else [])
+ (["--rm"] if remove else [])
+ (extra_run_args or [])
+ self.extra_launch_args
+ (
Expand All @@ -558,7 +577,9 @@ def get_launch_cmd(
)

id_or_url = self.container_id or self.url
container_launch = ("-it", id_or_url)
container_launch: Tuple[str, ...] = (
("-it", id_or_url) if interactive_tty else (id_or_url,)
)
bash_launch_end = (
*_CONTAINER_STOPSIGNAL,
*container_launch,
Expand Down Expand Up @@ -990,7 +1011,47 @@ def prepare_container(


@dataclass(frozen=True)
class ContainerData:
class ContainerImageData:
"""Class returned by the ``container_image`` fixture to the test
function. It contains a reference to the container image that has been used
in the test and has properties that provide the full command to launch the
entrypoint of the container image under test.
"""

#: the container data class that has been used in this test
container: Union[Container, DerivedContainer, MultiStageContainer]

_container_runtime: OciRuntimeBase

@property
def run_command_list(self) -> List[str]:
"""The full command (including the container runtime) to launch this
container image's entrypoint in the foreground. A list of the individual
arguments is returned that can be passed directly into
:py:func:`subprocess.run`.
"""
return self.container.get_launch_cmd(
self._container_runtime,
detach=False,
remove=True,
# -it breaks testinfra.host.check_output() with docker
interactive_tty=self._container_runtime.runner_binary == "podman",
)

@property
def run_command(self) -> str:
"""The full command (including the container runtime) to launch this
container image's entrypoint in the foreground. The command is returned
as a single string that has to be invoked via a shell.
"""
return " ".join(self.run_command_list)


@dataclass(frozen=True)
class ContainerData(ContainerImageData):
"""Class returned by the ``*container*`` fixtures to the test function. It
contains information about the launched container and the testinfra
:py:attr:`connection` to the running container.
Expand All @@ -1004,13 +1065,9 @@ class ContainerData:
container_id: str
#: the testinfra connection to the running container
connection: Any
#: the container data class that has been used in this test
container: Union[Container, DerivedContainer, MultiStageContainer]
#: any ports that are exposed by this container
forwarded_ports: List[PortForwarding]

_container_runtime: OciRuntimeBase

@property
def inspect(self) -> ContainerInspect:
"""Inspect the launched container and return the result of
Expand Down Expand Up @@ -1201,11 +1258,13 @@ def from_pytestconfig(
def __enter__(self) -> "ContainerLauncher":
return self

def launch_container(self) -> None:
"""This function performs the actual heavy lifting of launching the
container, creating all the volumes, port bindings, etc.pp.
def prepare_container_image(self) -> None:
"""Prepares the container image for launching containers. This includes
building the container image and all its dependents and creating volume
mounts.
"""

# Lock guarding the container preparation, so that only one process
# tries to pull/build it at the same time.
# If this container is a singleton, then we use it as a lock until
Expand Down Expand Up @@ -1247,6 +1306,13 @@ def release_lock() -> None:
get_volume_creator(cont_vol, self.container_runtime)
)

def launch_container(self) -> None:
"""This function performs the actual heavy lifting of launching the
container, creating all the volumes, port bindings, etc.pp.
"""
self.prepare_container_image()

forwarded_ports = self.container.forwarded_ports

extra_run_args = self.extra_run_args
Expand Down Expand Up @@ -1286,6 +1352,23 @@ def release_lock() -> None:

self._wait_for_container_to_become_healthy()

@property
def container_image_data(self) -> ContainerImageData:
"""The :py:class:`ContainerImageData` belonging to this container
image.
.. warning::
This property will always be set, even if the container image has not
been prepared yet. Only use it after calling
:py:func:`ContainerLauncher.prepare_container_image`.
"""
# FIXME: check if container is prepared
return ContainerImageData(
container=self.container, _container_runtime=self.container_runtime
)

@property
def container_data(self) -> ContainerData:
"""The :py:class:`ContainerData` instance corresponding to the running
Expand Down
29 changes: 29 additions & 0 deletions pytest_container/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from pytest_container.container import container_and_marks_from_pytest_param
from pytest_container.container import ContainerData
from pytest_container.container import ContainerImageData
from pytest_container.container import ContainerLauncher
from pytest_container.logging import _logger
from pytest_container.pod import pod_from_pytest_param
Expand Down Expand Up @@ -173,3 +174,31 @@ def fixture_funct(

#: Same as :py:func:`pod`, except that it creates a pod for each test function.
pod_per_test = _create_auto_pod_fixture("function")


@fixture(scope="session")
def container_image(
request: SubRequest,
# we must call this parameter container runtime, so that pytest will
# treat it as a fixture, but that causes pylint to complain…
# pylint: disable=redefined-outer-name
container_runtime: OciRuntimeBase,
pytestconfig: Config,
) -> Generator[ContainerImageData, None, None]:
"""Fixture that has to be parametrized with an instance of
:py:class:`~pytest_container.container.Container`,
:py:class:`~pytest_container.container.DerivedContainer` or
:py:class:`~pytest_container.container.MultiStageContainer` with
``indirect=True``. It builds the container image passed as the parameter and
yields an instance of
:py:class:`~pytest_container.container.ContainerImageData` to the test
function.
"""

container, _ = container_and_marks_from_pytest_param(request.param)
with ContainerLauncher.from_pytestconfig(
container, container_runtime, pytestconfig
) as launcher:
launcher.prepare_container_image()
yield launcher.container_image_data
37 changes: 37 additions & 0 deletions tests/test_container_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# pylint: disable=missing-function-docstring,missing-module-docstring
import pytest
from pytest_container.container import ContainerImageData
from pytest_container.container import DerivedContainer

from tests.images import LEAP


@pytest.mark.parametrize("container_image", [LEAP], indirect=True)
def test_run_leap(container_image: ContainerImageData, host) -> None:
assert 'NAME="openSUSE Leap"' in host.check_output(
f"{container_image.run_command} cat /etc/os-release"
)


CTR_WITH_ENTRYPOINT_ADDING_PATH = DerivedContainer(
base=LEAP,
containerfile="""RUN mkdir -p /usr/test/; echo 'echo "foobar"' > /usr/test/foobar; chmod +x /usr/test/foobar
RUN echo '#!/bin/sh' > /entrypoint.sh; \
echo "export PATH=/usr/test/:$PATH" >> /entrypoint.sh; \
echo 'exec "$@"' >> /entrypoint.sh; \
chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
""",
)


@pytest.mark.parametrize(
"container_image", [CTR_WITH_ENTRYPOINT_ADDING_PATH], indirect=True
)
def test_entrypoint_respected_in_run(
container_image: ContainerImageData, host
) -> None:
assert "foobar" in host.check_output(
f"{container_image.run_command} foobar"
)

0 comments on commit aebac9d

Please sign in to comment.