diff --git a/pytest_container/container.py b/pytest_container/container.py index 4cf7aec..0102ad4 100644 --- a/pytest_container/container.py +++ b/pytest_container/container.py @@ -526,20 +526,38 @@ def get_launch_cmd( self, container_runtime: OciRuntimeBase, extra_run_args: Optional[List[str]] = None, + in_background: 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``. + in_background: 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) + 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 in_background else []) + + (["--rm"] if remove else []) + (extra_run_args or []) + self.extra_launch_args + ( @@ -558,7 +576,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, @@ -994,7 +1014,29 @@ def prepare_container( @dataclass(frozen=True) -class ContainerData: +class ContainerImageData: + #: 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]: + return self.container.get_launch_cmd( + self._container_runtime, + in_background=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: + 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. @@ -1008,13 +1050,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 @@ -1205,11 +1243,7 @@ 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(self) -> None: # 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 @@ -1251,6 +1285,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() + forwarded_ports = self.container.forwarded_ports extra_run_args = self.extra_run_args @@ -1290,6 +1331,13 @@ def release_lock() -> None: self._wait_for_container_to_become_healthy() + @property + def container_image_data(self) -> ContainerImageData: + # 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 diff --git a/pytest_container/plugin.py b/pytest_container/plugin.py index 186597c..13a89c7 100644 --- a/pytest_container/plugin.py +++ b/pytest_container/plugin.py @@ -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 @@ -173,3 +174,20 @@ 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]: + container, _ = container_and_marks_from_pytest_param(request.param) + with ContainerLauncher.from_pytestconfig( + container, container_runtime, pytestconfig + ) as launcher: + launcher.prepare_container() + yield launcher.container_image_data diff --git a/tests/test_container_run.py b/tests/test_container_run.py new file mode 100644 index 0000000..afa0884 --- /dev/null +++ b/tests/test_container_run.py @@ -0,0 +1,36 @@ +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" + )