diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 09a980b2..df9a489c 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -202,10 +202,9 @@ def start(self) -> Self: else {} ) - self._container = docker_client.run( + self._container = docker_client.create( self.image, command=self._command, - detach=True, environment=self.env, ports=cast("dict[int, Optional[int]]", self.ports), name=self._name, @@ -214,14 +213,16 @@ def start(self) -> Self: **{**network_kwargs, **self._kwargs}, ) + for t in self._transferable_specs: + self._transfer_into_container(*t) + + docker_client.start(self._container) + if self._wait_strategy is not None: self._wait_strategy.wait_until_ready(self) logger.info("Container started: %s", self._container.short_id) - for t in self._transferable_specs: - self._transfer_into_container(*t) - return self def stop(self, force: bool = True, delete_volume: bool = True) -> None: @@ -328,6 +329,13 @@ def exec(self, command: Union[str, list[str]]) -> ExecResult: raise ContainerStartException("Container should be started before executing a command") return self._container.exec_run(command) + def wait(self) -> int: + """Wait for the container to stop and return its exit code.""" + if not self._container: + raise ContainerStartException("Container should be started before waiting") + result = self._container.wait() + return int(result["StatusCode"]) + def _configure(self) -> None: # placeholder if subclasses want to define this and use the default start method pass diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 12384c94..45b9bed9 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -114,6 +114,43 @@ def run( ) return container + @_wrapped_container_collection + def create( + self, + image: str, + command: Optional[Union[str, list[str]]] = None, + environment: Optional[dict[str, str]] = None, + ports: Optional[dict[int, Optional[int]]] = None, + labels: Optional[dict[str, str]] = None, + **kwargs: Any, + ) -> Container: + """Create a container without starting it, pulling the image first if not present locally.""" + if "network" not in kwargs and not get_docker_host(): + host_network = self.find_host_network() + if host_network: + kwargs["network"] = host_network + + try: + # This is more or less a replication of what the self.client.containers.start does internally + self.client.images.get(image) + except docker.errors.ImageNotFound: + self.client.images.pull(image) + + container = self.client.containers.create( + image, + command=command, + environment=environment, + ports=ports, + labels=create_labels(image, labels), + **kwargs, + ) + return container + + @_wrapped_container_collection + def start(self, container: Container) -> None: + """Start a previously created container.""" + container.start() + @_wrapped_image_collection def build( self, path: str, tag: Optional[str], rm: bool = True, **kwargs: Any diff --git a/core/tests/test_transferable.py b/core/tests/test_transferable.py index 992f163a..592ad87d 100644 --- a/core/tests/test_transferable.py +++ b/core/tests/test_transferable.py @@ -104,6 +104,19 @@ def test_copy_into_container_at_startup(transferable: Transferable): assert result.output == b"hello world" +def test_copy_into_startup_file(transferable: Transferable): + destination_in_container = "/tmp/my_file" + + container = DockerContainer("bash", command=f"cat {destination_in_container}") + container.with_copy_into_container(transferable, destination_in_container) + + with container: + exit_code = container.wait() + stdout, _ = container.get_logs() + assert exit_code == 0 + assert stdout.decode() == "hello world" + + def test_copy_into_container_via_initializer(transferable: Transferable): destination_in_container = "/tmp/my_file" transferables: list[TransferSpec] = [(transferable, destination_in_container, 0o644)]