diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 461ef9ef..ce050f20 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -52,6 +52,8 @@ jobs: restore-keys: ${{ runner.os }}-mix- - name: Install dependencies run: mix deps.get + - name: Run credo + run: mix credo --strict - name: Run tests run: MIX_ENV=test mix citest - name: Verify version diff --git a/lib/connection/connection.ex b/lib/connection/connection.ex index 966c8d3d..5f1ae6e7 100644 --- a/lib/connection/connection.ex +++ b/lib/connection/connection.ex @@ -4,13 +4,13 @@ defmodule Testcontainers.Connection do require Logger + alias DockerEngineAPI.Connection alias Testcontainers.Constants - alias Testcontainers.DockerUrl + alias Testcontainers.DockerHostFromEnvStrategy + alias Testcontainers.DockerHostFromPropertiesStrategy alias Testcontainers.DockerHostStrategyEvaluator alias Testcontainers.DockerSocketPathStrategy - alias Testcontainers.DockerHostFromPropertiesStrategy - alias Testcontainers.DockerHostFromEnvStrategy - alias DockerEngineAPI.Connection + alias Testcontainers.DockerUrl @timeout 300_000 @@ -29,9 +29,10 @@ defmodule Testcontainers.Connection do end defp get_docker_host_url do - with {:ok, docker_host} <- get_docker_host() do - {DockerUrl.construct(docker_host), docker_host} - else + case get_docker_host() do + {:ok, docker_host} -> + {DockerUrl.construct(docker_host), docker_host} + {:error, error} -> exit(error) end diff --git a/lib/connection/docker_host_strategy/docker_socket_path.ex b/lib/connection/docker_host_strategy/docker_socket_path.ex index 17e9e085..e81d2e6d 100644 --- a/lib/connection/docker_host_strategy/docker_socket_path.ex +++ b/lib/connection/docker_host_strategy/docker_socket_path.ex @@ -28,30 +28,34 @@ defmodule Testcontainers.DockerSocketPathStrategy do end def execute(strategy, _input) do - Enum.reduce_while( - if length(strategy.socket_paths) == 0 do - default_socket_paths() - else - strategy.socket_paths - end, - {:error, {:docker_socket_not_found, []}}, - fn path, {:error, {:docker_socket_not_found, tried_paths}} -> - if path != nil && File.exists?(path) do - path_with_scheme = "unix://" <> path - - case DockerUrl.test_docker_host(path_with_scheme) do - :ok -> - {:halt, {:ok, path_with_scheme}} - - {:error, reason} -> - Logger.debug("Docker socket path #{path} failed: #{reason}") - {:cont, {:error, {:docker_socket_not_found, tried_paths ++ [path]}}} - end - else - {:cont, {:error, {:docker_socket_not_found, tried_paths ++ [path]}}} - end + paths = + case strategy.socket_paths do + [] -> default_socket_paths() + paths -> paths end - ) + + Enum.reduce_while(paths, {:error, {:docker_socket_not_found, []}}, &try_socket_path/2) + end + + defp try_socket_path(path, {:error, {:docker_socket_not_found, tried_paths}}) do + if path != nil && File.exists?(path) do + probe_socket(path, tried_paths) + else + {:cont, {:error, {:docker_socket_not_found, tried_paths ++ [path]}}} + end + end + + defp probe_socket(path, tried_paths) do + path_with_scheme = "unix://" <> path + + case DockerUrl.test_docker_host(path_with_scheme) do + :ok -> + {:halt, {:ok, path_with_scheme}} + + {:error, reason} -> + Logger.debug("Docker socket path #{path} failed: #{reason}") + {:cont, {:error, {:docker_socket_not_found, tried_paths ++ [path]}}} + end end end end diff --git a/lib/connection/docker_host_strategy_evaluator.ex b/lib/connection/docker_host_strategy_evaluator.ex index d88315c1..eafa1c22 100644 --- a/lib/connection/docker_host_strategy_evaluator.ex +++ b/lib/connection/docker_host_strategy_evaluator.ex @@ -24,7 +24,6 @@ defmodule Testcontainers.DockerHostStrategyEvaluator do defp format_errors(errors) do errors |> Enum.reverse() - |> Enum.map(fn {:error, error} -> inspect(error) end) - |> Enum.join(", ") + |> Enum.map_join(", ", fn {:error, error} -> inspect(error) end) end end diff --git a/lib/container.ex b/lib/container.ex index 34a1c75e..6c605c73 100644 --- a/lib/container.ex +++ b/lib/container.ex @@ -8,6 +8,8 @@ defmodule Testcontainers.Container do require Logger + @type t :: %__MODULE__{} + @enforce_keys [:image] defstruct [ :image, @@ -51,7 +53,7 @@ defmodule Testcontainers.Container do when is_atom(name) and name == @os_type @dialyzer {:nowarn_function, os_type: 0} - def os_type() do + def os_type do cond do is_os(:linux) -> :linux is_os(:macos) -> :macos @@ -335,7 +337,8 @@ defmodule Testcontainers.Container do Available options: - * `TestContainers.PullPolicy.always_pull()` - default, always pulls image from the remote repository + * `TestContainers.PullPolicy.pull_if_missing()` - default, pulls image only if not already present locally (avoids registry rate limits) + * `TestContainers.PullPolicy.always_pull()` - always pulls image from the remote repository * `TestContainers.PullPolicy.never_pull()` - does not pull images, use when working with local images * `TestContainers.PullPolicy.pull_condition(expr)` - pulls image if expression returns `true` """ @@ -353,7 +356,7 @@ defmodule Testcontainers.Container do Do stuff after container has started. """ @impl true - @spec after_start(%Testcontainers.Container{}, %Testcontainers.Container{}, %Tesla.Env{}) :: + @spec after_start(Testcontainers.Container.t(), Testcontainers.Container.t(), Tesla.Env.t()) :: :ok def after_start(_config, _container, _conn), do: :ok end diff --git a/lib/container/cassandra_container.ex b/lib/container/cassandra_container.ex index 787322d0..92f48251 100644 --- a/lib/container/cassandra_container.ex +++ b/lib/container/cassandra_container.ex @@ -6,8 +6,8 @@ defmodule Testcontainers.CassandraContainer do alias Testcontainers.CassandraContainer alias Testcontainers.CommandWaitStrategy - alias Testcontainers.ContainerBuilder alias Testcontainers.Container + alias Testcontainers.ContainerBuilder import Testcontainers.Container, only: [is_valid_image: 1] @@ -19,6 +19,8 @@ defmodule Testcontainers.CassandraContainer do @default_port 9042 @default_wait_timeout 60_000 + @type t :: %__MODULE__{} + @enforce_keys [:image, :wait_timeout] defstruct [ :image, @@ -75,7 +77,7 @@ defmodule Testcontainers.CassandraContainer do import Container @impl true - @spec build(%CassandraContainer{}) :: %Container{} + @spec build(CassandraContainer.t()) :: Container.t() def build(%CassandraContainer{} = config) do new(config.image) |> with_exposed_port(CassandraContainer.default_port()) diff --git a/lib/container/ceph_container.ex b/lib/container/ceph_container.ex index b301e83a..a4c57a35 100644 --- a/lib/container/ceph_container.ex +++ b/lib/container/ceph_container.ex @@ -4,10 +4,10 @@ defmodule Testcontainers.CephContainer do Provides functionality for creating and managing Ceph container configurations. """ - alias Testcontainers.LogWaitStrategy alias Testcontainers.CephContainer - alias Testcontainers.ContainerBuilder alias Testcontainers.Container + alias Testcontainers.ContainerBuilder + alias Testcontainers.LogWaitStrategy import Testcontainers.Container, only: [is_valid_image: 1] @@ -20,6 +20,8 @@ defmodule Testcontainers.CephContainer do @default_port 8080 @default_wait_timeout 300_000 + @type t :: %__MODULE__{} + @enforce_keys [:image, :access_key, :secret_key, :bucket, :port, :wait_timeout] defstruct [ :image, @@ -230,7 +232,7 @@ defmodule Testcontainers.CephContainer do - Raises `ArgumentError` if the provided image is not compatible with the default Ceph image. """ - @spec build(%CephContainer{}) :: %Container{} + @spec build(CephContainer.t()) :: Container.t() @impl true def build(%CephContainer{} = config) do new(config.image) diff --git a/lib/container/emqx_container.ex b/lib/container/emqx_container.ex index 58803d2d..49e50291 100644 --- a/lib/container/emqx_container.ex +++ b/lib/container/emqx_container.ex @@ -3,10 +3,10 @@ defmodule Testcontainers.EmqxContainer do Provides functionality for creating and managing EMQX container configurations. """ - alias Testcontainers.ContainerBuilder alias Testcontainers.Container - alias Testcontainers.PortWaitStrategy + alias Testcontainers.ContainerBuilder alias Testcontainers.EmqxContainer + alias Testcontainers.PortWaitStrategy import Testcontainers.Container, only: [is_valid_image: 1] @@ -17,7 +17,7 @@ defmodule Testcontainers.EmqxContainer do @default_mqtts_port 8883 @default_mqtt_over_ws_port 8083 @default_mqtt_over_wss_port 8084 - @default_dashboard_port 18083 + @default_dashboard_port 18_083 @default_wait_timeout 60_000 @enforce_keys [:image, :mqtt_port, :wait_timeout] @@ -160,7 +160,6 @@ defmodule Testcontainers.EmqxContainer do ] @impl true - # TODO Implement the `after_start/3` function for the `ContainerBuilder` protocol. def after_start(_config, _container, _conn), do: :ok end end diff --git a/lib/container/kafka_container.ex b/lib/container/kafka_container.ex index 48e935e8..d7d2363d 100644 --- a/lib/container/kafka_container.ex +++ b/lib/container/kafka_container.ex @@ -45,6 +45,8 @@ defmodule Testcontainers.KafkaContainer do @default_wait_timeout 60_000 @default_cluster_id "4L6g3nShT-eMCtK--X86sw" + @type t :: %__MODULE__{} + @enforce_keys [ :image, :kafka_port, @@ -73,7 +75,7 @@ defmodule Testcontainers.KafkaContainer do """ def new do # Select a random port in a high range to minimize conflicts - kafka_port = Enum.random(29000..29999) + kafka_port = Enum.random(29_000..29_999) %__MODULE__{ image: @default_image_with_tag, @@ -170,7 +172,7 @@ defmodule Testcontainers.KafkaContainer do import Container @impl true - @spec build(%KafkaContainer{}) :: %Container{} + @spec build(KafkaContainer.t()) :: Container.t() def build(%KafkaContainer{} = config) do host = Testcontainers.get_host() diff --git a/lib/container/minio_container.ex b/lib/container/minio_container.ex index cc594922..67b9409a 100644 --- a/lib/container/minio_container.ex +++ b/lib/container/minio_container.ex @@ -4,9 +4,9 @@ defmodule Testcontainers.MinioContainer do """ alias Testcontainers.Container - alias Testcontainers.MinioContainer alias Testcontainers.ContainerBuilder alias Testcontainers.LogWaitStrategy + alias Testcontainers.MinioContainer @default_image "minio/minio" @default_tag "RELEASE.2023-11-11T08-14-41Z" @@ -17,6 +17,8 @@ defmodule Testcontainers.MinioContainer do @default_ui_port 9001 @default_wait_timeout 60_000 + @type t :: %__MODULE__{} + @enforce_keys [:image, :username, :password, :wait_timeout] defstruct [ :image, @@ -75,7 +77,7 @@ defmodule Testcontainers.MinioContainer do defimpl ContainerBuilder do import Container - @spec build(%MinioContainer{}) :: %Container{} + @spec build(MinioContainer.t()) :: Container.t() @impl true def build(%MinioContainer{} = config) do new(config.image) diff --git a/lib/container/mongo_container.ex b/lib/container/mongo_container.ex index dadc7914..84eedf61 100644 --- a/lib/container/mongo_container.ex +++ b/lib/container/mongo_container.ex @@ -18,9 +18,11 @@ defmodule Testcontainers.MongoContainer do @default_user "test" @default_password "test" @default_database "test" - @default_port 27017 + @default_port 27_017 @default_wait_timeout 180_000 + @type t :: %__MODULE__{} + @enforce_keys [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume] defstruct [ :image, @@ -251,7 +253,7 @@ defmodule Testcontainers.MongoContainer do - Raises `ArgumentError` if the provided image is not compatible with the default Mongo image. """ - @spec build(%MongoContainer{}) :: %Container{} + @spec build(MongoContainer.t()) :: Container.t() @impl true def build(%MongoContainer{} = config) do new(config.image) diff --git a/lib/container/mysql_container.ex b/lib/container/mysql_container.ex index 876856dd..47b00851 100644 --- a/lib/container/mysql_container.ex +++ b/lib/container/mysql_container.ex @@ -9,8 +9,8 @@ defmodule Testcontainers.MySqlContainer do alias Testcontainers.Container alias Testcontainers.ContainerBuilder - alias Testcontainers.MySqlContainer alias Testcontainers.LogWaitStrategy + alias Testcontainers.MySqlContainer import Testcontainers.Container, only: [is_valid_image: 1] @@ -23,6 +23,8 @@ defmodule Testcontainers.MySqlContainer do @default_port 3306 @default_wait_timeout 180_000 + @type t :: %__MODULE__{} + @enforce_keys [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume] defstruct [ :image, @@ -211,7 +213,7 @@ defmodule Testcontainers.MySqlContainer do - Raises `ArgumentError` if the provided image is not compatible with the default MySql image. """ - @spec build(%MySqlContainer{}) :: %Container{} + @spec build(MySqlContainer.t()) :: Container.t() @impl true def build(%MySqlContainer{} = config) do new(config.image) diff --git a/lib/container/postgres_container.ex b/lib/container/postgres_container.ex index 7b54e315..ff71319c 100644 --- a/lib/container/postgres_container.ex +++ b/lib/container/postgres_container.ex @@ -8,9 +8,9 @@ defmodule Testcontainers.PostgresContainer do """ alias Testcontainers.CommandWaitStrategy - alias Testcontainers.PostgresContainer alias Testcontainers.Container alias Testcontainers.ContainerBuilder + alias Testcontainers.PostgresContainer import Testcontainers.Container, only: [is_valid_image: 1] @@ -23,6 +23,8 @@ defmodule Testcontainers.PostgresContainer do @default_port 5432 @default_wait_timeout 60_000 + @type t :: %__MODULE__{} + @enforce_keys [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume] defstruct [ :image, @@ -211,7 +213,7 @@ defmodule Testcontainers.PostgresContainer do - Raises `ArgumentError` if the provided image is not compatible with the default Postgres image. """ - @spec build(%PostgresContainer{}) :: %Container{} + @spec build(PostgresContainer.t()) :: Container.t() @impl true def build(%PostgresContainer{} = config) do new(config.image) diff --git a/lib/container/protocols/container_builder.ex b/lib/container/protocols/container_builder.ex index 26ace0e2..04b6e032 100644 --- a/lib/container/protocols/container_builder.ex +++ b/lib/container/protocols/container_builder.ex @@ -2,12 +2,12 @@ defprotocol Testcontainers.ContainerBuilder do @moduledoc """ All types of predefined containers must implement this protocol. """ - @spec build(t()) :: %Testcontainers.Container{} + @spec build(t()) :: Testcontainers.Container.t() def build(builder) @doc """ Do stuff after container has started. """ - @spec after_start(t(), %Testcontainers.Container{}, %Tesla.Env{}) :: :ok | {:error, term()} + @spec after_start(t(), Testcontainers.Container.t(), Tesla.Env.t()) :: :ok | {:error, term()} def after_start(builder, container, connection) end diff --git a/lib/container/protocols/container_builder_helper.ex b/lib/container/protocols/container_builder_helper.ex index cd1b610c..c30a8a11 100644 --- a/lib/container/protocols/container_builder_helper.ex +++ b/lib/container/protocols/container_builder_helper.ex @@ -1,8 +1,9 @@ defmodule Testcontainers.ContainerBuilderHelper do + @moduledoc false import Testcontainers.Constants - alias Testcontainers.Util.Hash alias Testcontainers.Container alias Testcontainers.ContainerBuilder + alias Testcontainers.Util.Hash def build(builder, state) when is_map(state) and is_struct(builder) do config = @@ -18,13 +19,13 @@ defmodule Testcontainers.ContainerBuilderHelper do config |> Container.with_label(container_reuse(), "true") |> Container.with_label(container_reuse_hash_label(), hash) - |> Container.with_label(container_sessionId_label(), state.session_id) + |> Container.with_label(container_session_id_label(), state.session_id) |> Container.with_label(container_version_label(), library_version()) |> Kernel.then(&{:reuse, &1, hash}) else config |> Container.with_label(container_reuse(), "false") - |> Container.with_label(container_sessionId_label(), state.session_id) + |> Container.with_label(container_session_id_label(), state.session_id) |> Container.with_label(container_version_label(), library_version()) |> Kernel.then(&{:noreuse, &1, nil}) end diff --git a/lib/container/rabbitmq_container.ex b/lib/container/rabbitmq_container.ex index ffe93de1..ee283b35 100644 --- a/lib/container/rabbitmq_container.ex +++ b/lib/container/rabbitmq_container.ex @@ -7,9 +7,9 @@ defmodule Testcontainers.RabbitMQContainer do NOTE: Currently untested, any developer who tries to add improvements on this container should consider moving this container implementation to a separate Elixir package. """ - alias Testcontainers.ContainerBuilder - alias Testcontainers.Container alias Testcontainers.CommandWaitStrategy + alias Testcontainers.Container + alias Testcontainers.ContainerBuilder alias Testcontainers.RabbitMQContainer import Testcontainers.Container, only: [is_valid_image: 1] @@ -28,6 +28,8 @@ defmodule Testcontainers.RabbitMQContainer do ] @default_wait_timeout 60_000 + @type t :: %__MODULE__{} + @enforce_keys [:image, :port, :wait_timeout] defstruct [ :image, @@ -241,7 +243,8 @@ defmodule Testcontainers.RabbitMQContainer do ] end - # Provides the virtual host segment used in the AMQP URI specification defined in the AMQP 0-9-1, and interprets the virtual host for the connection URL based on the default value. + # Provides the virtual host segment used in the AMQP URI specification defined in the AMQP 0-9-1, + # and interprets the virtual host for the connection URL based on the default value. defp virtual_host_segment(container) do case container.environment[:RABBITMQ_DEFAULT_VHOST] do "/" -> "" @@ -271,7 +274,7 @@ defmodule Testcontainers.RabbitMQContainer do - Raises `ArgumentError` if the provided image is not compatible with the default RabbitMQ image. """ @impl true - @spec build(%RabbitMQContainer{}) :: %Container{} + @spec build(RabbitMQContainer.t()) :: Container.t() def build(%RabbitMQContainer{} = config) do new(config.image) |> with_exposed_port(config.port) diff --git a/lib/container/redis_container.ex b/lib/container/redis_container.ex index 0487394f..254ce63a 100644 --- a/lib/container/redis_container.ex +++ b/lib/container/redis_container.ex @@ -6,9 +6,9 @@ defmodule Testcontainers.RedisContainer do Provides functionality for creating and managing Redis container configurations. """ - alias Testcontainers.ContainerBuilder - alias Testcontainers.Container alias Testcontainers.CommandWaitStrategy + alias Testcontainers.Container + alias Testcontainers.ContainerBuilder alias Testcontainers.RedisContainer import Testcontainers.Container, only: [is_valid_image: 1] @@ -19,6 +19,8 @@ defmodule Testcontainers.RedisContainer do @default_port 6379 @default_wait_timeout 60_000 + @type t :: %__MODULE__{} + @enforce_keys [:image, :port, :wait_timeout] defstruct [ :image, @@ -165,7 +167,7 @@ defmodule Testcontainers.RedisContainer do - Raises `ArgumentError` if the provided image is not compatible with the default Redis image. """ - @spec build(%RedisContainer{}) :: %Container{} + @spec build(RedisContainer.t()) :: Container.t() @impl true def build(%RedisContainer{} = config) do container = diff --git a/lib/container/selenium_container.ex b/lib/container/selenium_container.ex index 083972d7..1caab9ac 100644 --- a/lib/container/selenium_container.ex +++ b/lib/container/selenium_container.ex @@ -4,11 +4,11 @@ defmodule Testcontainers.SeleniumContainer do Work in progress. Not stable for use yet. Not yet documented for this very reason. Can use https://github.com/stuart/elixir-webdriver for client in tests """ - alias Testcontainers.ContainerBuilder alias Testcontainers.Container - alias Testcontainers.SeleniumContainer - alias Testcontainers.PortWaitStrategy + alias Testcontainers.ContainerBuilder alias Testcontainers.LogWaitStrategy + alias Testcontainers.PortWaitStrategy + alias Testcontainers.SeleniumContainer import Testcontainers.Container, only: [is_valid_image: 1] @@ -19,6 +19,8 @@ defmodule Testcontainers.SeleniumContainer do @default_port2 4400 @default_wait_timeout 120_000 + @type t :: %__MODULE__{} + @enforce_keys [:image, :port1, :port2, :wait_timeout] defstruct [ :image, @@ -72,7 +74,7 @@ defmodule Testcontainers.SeleniumContainer do defimpl ContainerBuilder do import Container - @spec build(%SeleniumContainer{}) :: %Container{} + @spec build(SeleniumContainer.t()) :: Container.t() @impl true def build(%SeleniumContainer{} = config) do new(config.image) diff --git a/lib/copy_to.ex b/lib/copy_to.ex index 00a811a1..5b9e3552 100644 --- a/lib/copy_to.ex +++ b/lib/copy_to.ex @@ -1,4 +1,5 @@ defmodule Testcontainers.CopyTo do + @moduledoc false alias Testcontainers.Docker @doc """ diff --git a/lib/docker/api.ex b/lib/docker/api.ex index db699455..1b1ee63a 100644 --- a/lib/docker/api.ex +++ b/lib/docker/api.ex @@ -4,9 +4,9 @@ defmodule Testcontainers.Docker.Api do Internal docker api. Only for direct use by `Testcontainers` """ + alias DockerEngineAPI.Api alias DockerEngineAPI.Model.ExecConfig alias DockerEngineAPI.Model.HostConfig - alias DockerEngineAPI.Api alias Testcontainers.Container def get_container(container_id, conn) @@ -64,6 +64,22 @@ defmodule Testcontainers.Docker.Api do end end + def image_exists?(image, conn) when is_binary(image) do + case Api.Image.image_inspect(conn, image) do + {:ok, %DockerEngineAPI.Model.ImageInspect{}} -> + {:ok, true} + + {:ok, %DockerEngineAPI.Model.ErrorResponse{}} -> + {:ok, false} + + {:error, %Tesla.Env{status: 404}} -> + {:ok, false} + + {:error, %Tesla.Env{status: other}} -> + {:error, {:http_error, other}} + end + end + def delete_image(image, conn) when is_binary(image) do case Api.Image.image_delete(conn, image, force: true) do {:ok, _} -> :ok @@ -177,11 +193,9 @@ defmodule Testcontainers.Docker.Api do config |> Enum.filter(fn cfg -> Map.get(cfg, :Gateway, nil) != nil end) - if length(with_gateway) > 0 do - gateway = with_gateway |> Kernel.hd() |> Map.get(:Gateway) - {:ok, gateway} - else - {:error, :no_gateway} + case with_gateway do + [] -> {:error, :no_gateway} + [first | _] -> {:ok, Map.get(first, :Gateway)} end {:error, reason} -> diff --git a/lib/mix/tasks/testcontainers/run.ex b/lib/mix/tasks/testcontainers/run.ex index 1810629d..9773994a 100644 --- a/lib/mix/tasks/testcontainers/run.ex +++ b/lib/mix/tasks/testcontainers/run.ex @@ -1,7 +1,7 @@ defmodule Mix.Tasks.Testcontainers.Run do use Mix.Task - alias Testcontainers.PostgresContainer alias Testcontainers.MySqlContainer + alias Testcontainers.PostgresContainer @shortdoc "Runs a Mix sub-task (test, phx.server, etc) with a database container" @moduledoc """ diff --git a/lib/mix/tasks/testcontainers/test.ex b/lib/mix/tasks/testcontainers/test.ex index ddc95362..4c826674 100644 --- a/lib/mix/tasks/testcontainers/test.ex +++ b/lib/mix/tasks/testcontainers/test.ex @@ -1,4 +1,5 @@ defmodule Mix.Tasks.Testcontainers.Test do + @moduledoc false use Mix.Task @shortdoc "Runs mix test with Testcontainers (backward compatibility)" diff --git a/lib/pull_policy.ex b/lib/pull_policy.ex index b1e389df..030f4cfe 100644 --- a/lib/pull_policy.ex +++ b/lib/pull_policy.ex @@ -1,23 +1,39 @@ defmodule Testcontainers.PullPolicy do + @moduledoc """ + Pull policies that control whether an image is fetched from a remote registry + before starting a container. + """ + alias Testcontainers.PullPolicy - defstruct [:always_pull, :pull_condition] + @type t :: %__MODULE__{ + always_pull: boolean() | nil, + pull_if_missing: boolean() | nil, + pull_condition: (struct(), Tesla.Env.t() -> boolean()) | nil + } + + defstruct [:always_pull, :pull_if_missing, :pull_condition] - @spec always_pull() :: %PullPolicy{} + @spec always_pull() :: PullPolicy.t() def always_pull do %__MODULE__{always_pull: true} end - @spec never_pull() :: %PullPolicy{} + @spec never_pull() :: PullPolicy.t() def never_pull do %__MODULE__{} end + @spec pull_if_missing() :: PullPolicy.t() + def pull_if_missing do + %__MODULE__{pull_if_missing: true} + end + @spec pull_condition( expr :: (config :: struct(), conn :: Tesla.Env.t() -> true | false) ) :: - %PullPolicy{} + PullPolicy.t() def pull_condition(expr) do %__MODULE__{pull_condition: expr} end diff --git a/lib/testcontainers.ex b/lib/testcontainers.ex index 0f9d17d9..31c2d578 100644 --- a/lib/testcontainers.ex +++ b/lib/testcontainers.ex @@ -9,19 +9,19 @@ defmodule Testcontainers do require Logger - alias Testcontainers.CopyTo - alias Testcontainers.Constants - alias Testcontainers.WaitStrategy - alias Testcontainers.Docker.Api + alias Testcontainers.Compose.Cli, as: ComposeCli + alias Testcontainers.Compose.ComposeEnvironment + alias Testcontainers.Compose.ComposeService alias Testcontainers.Connection + alias Testcontainers.Constants alias Testcontainers.Container alias Testcontainers.ContainerBuilder + alias Testcontainers.CopyTo + alias Testcontainers.Docker.Api + alias Testcontainers.DockerCompose alias Testcontainers.PullPolicy alias Testcontainers.Util.PropertiesParser - alias Testcontainers.DockerCompose - alias Testcontainers.Compose.Cli, as: ComposeCli - alias Testcontainers.Compose.ComposeService - alias Testcontainers.Compose.ComposeEnvironment + alias Testcontainers.WaitStrategy import Testcontainers.Constants import Testcontainers.Container, only: [os_type: 0] @@ -94,7 +94,7 @@ defmodule Testcontainers do end @doc false - def get_host(), do: wait_for_call(:get_host, __MODULE__) + def get_host, do: wait_for_call(:get_host, __MODULE__) @doc """ Returns the host to use for connecting to the given container. @@ -334,22 +334,7 @@ defmodule Testcontainers do Task.async(fn -> result = start_and_wait(config_builder, state) - - case result do - {:ok, container} -> - GenServer.cast(self_pid, {:track_container, container.container_id, container.image}) - - _ -> - # Track the image even on failure so it gets cleaned up on terminate - case config_builder do - %Container{image: image} when is_binary(image) -> - GenServer.cast(self_pid, {:track_image, image}) - - _ -> - :ok - end - end - + track_result(self_pid, config_builder, result) GenServer.reply(from, result) end) @@ -376,7 +361,7 @@ defmodule Testcontainers do @impl true def handle_call({:create_network, network_name}, from, state) do labels = %{ - Constants.container_sessionId_label() => state.session_id, + Constants.container_session_id_label() => state.session_id, Constants.container_version_label() => Constants.library_version(), Constants.container_lang_label() => Constants.container_lang_value(), Constants.container_label() => "true", @@ -551,31 +536,40 @@ defmodule Testcontainers do defp run_compose_wait_strategies(%DockerCompose{} = compose, services, state) do Enum.reduce_while(compose.wait_strategies, :ok, fn {service_name, strategies}, :ok -> - case Map.get(services, service_name) do - nil -> - {:halt, {:error, {:service_not_found, service_name}}} - - %ComposeService{container_id: container_id} -> - case Api.get_container(container_id, state.conn) do - {:ok, container} -> - result = - Enum.reduce(strategies, :ok, fn - strategy, :ok -> - WaitStrategy.wait_until_container_is_ready(strategy, container, state.conn) - - _, error -> - error - end) - - case result do - :ok -> {:cont, :ok} - error -> {:halt, error} - end - - {:error, _} = error -> - {:halt, error} - end - end + run_service_wait_strategies(service_name, strategies, services, state) + end) + end + + defp run_service_wait_strategies(service_name, strategies, services, state) do + case Map.get(services, service_name) do + nil -> + {:halt, {:error, {:service_not_found, service_name}}} + + %ComposeService{container_id: container_id} -> + apply_wait_strategies_to_container(container_id, strategies, state) + end + end + + defp apply_wait_strategies_to_container(container_id, strategies, state) do + case Api.get_container(container_id, state.conn) do + {:ok, container} -> + case reduce_wait_strategies(strategies, container, state) do + :ok -> {:cont, :ok} + error -> {:halt, error} + end + + {:error, _} = error -> + {:halt, error} + end + end + + defp reduce_wait_strategies(strategies, container, state) do + Enum.reduce(strategies, :ok, fn + strategy, :ok -> + WaitStrategy.wait_until_container_is_ready(strategy, container, state.conn) + + _, error -> + error end) end @@ -599,38 +593,51 @@ defmodule Testcontainers do {:ok, uri.host} uri when uri.scheme == "http+unix" -> - if running_in_container?() do - Logger.debug("Running in docker environment, trying to get bridge network gateway") - - with {:ok, gateway} <- Api.get_bridge_gateway(conn) do - {:ok, gateway} - else - {:error, reason} -> - Logger.debug( - "Failed to get bridge gateway: #{inspect(reason)}. Trying /proc/net/route" - ) - - case File.read("/proc/net/route") do - {:ok, content} -> - case parse_gateway_from_proc_route(content) do - {:ok, gateway} -> - Logger.debug("Found gateway from /proc/net/route: #{gateway}") - {:ok, gateway} - - {:error, _} -> - Logger.debug("Failed to parse /proc/net/route. Using localhost") - {:ok, "localhost"} - end - - {:error, _} -> - Logger.debug("Cannot read /proc/net/route. Using localhost") - {:ok, "localhost"} - end - end - else - Logger.debug("Not running in docker environment, using localhost") - {:ok, "localhost"} - end + resolve_unix_docker_hostname(conn) + end + end + + defp resolve_unix_docker_hostname(conn) do + if running_in_container?() do + Logger.debug("Running in docker environment, trying to get bridge network gateway") + resolve_bridge_gateway(conn) + else + Logger.debug("Not running in docker environment, using localhost") + {:ok, "localhost"} + end + end + + defp resolve_bridge_gateway(conn) do + case Api.get_bridge_gateway(conn) do + {:ok, gateway} -> + {:ok, gateway} + + {:error, reason} -> + Logger.debug("Failed to get bridge gateway: #{inspect(reason)}. Trying /proc/net/route") + resolve_gateway_from_proc_route() + end + end + + defp resolve_gateway_from_proc_route do + case File.read("/proc/net/route") do + {:ok, content} -> + resolve_gateway_from_content(content) + + {:error, _} -> + Logger.debug("Cannot read /proc/net/route. Using localhost") + {:ok, "localhost"} + end + end + + defp resolve_gateway_from_content(content) do + case parse_gateway_from_proc_route(content) do + {:ok, gateway} -> + Logger.debug("Found gateway from /proc/net/route: #{gateway}") + {:ok, gateway} + + {:error, _} -> + Logger.debug("Failed to parse /proc/net/route. Using localhost") + {:ok, "localhost"} end end @@ -643,14 +650,14 @@ defmodule Testcontainers do case ryuk_disabled do true -> - ryukDisabledMessage = + ryuk_disabled_message = """ ******************************************************************************** Ryuk has been disabled. This can cause unexpected behavior in your environment. ******************************************************************************** """ - IO.puts(ryukDisabledMessage) + IO.puts(ryuk_disabled_message) {:ok} @@ -741,7 +748,7 @@ defmodule Testcontainers do :binary, active: false, packet: :line, - send_timeout: 10000 + send_timeout: 10_000 ], 5000) end @@ -766,7 +773,7 @@ defmodule Testcontainers do defp register_ryuk_filter(value, socket) do :gen_tcp.send( socket, - "label=#{container_sessionId_label()}=#{value}&" <> + "label=#{container_session_id_label()}=#{value}&" <> "label=#{container_version_label()}=#{library_version()}&" <> "label=#{container_lang_label()}=#{container_lang_value()}&" <> "label=#{container_label()}=#{true}&" <> @@ -782,6 +789,16 @@ defmodule Testcontainers do end end + defp track_result(self_pid, _config_builder, {:ok, container}) do + GenServer.cast(self_pid, {:track_container, container.container_id, container.image}) + end + + defp track_result(self_pid, %Container{image: image}, _result) when is_binary(image) do + GenServer.cast(self_pid, {:track_image, image}) + end + + defp track_result(_self_pid, _config_builder, _result), do: :ok + defp start_and_wait(config_builder, state) do case Testcontainers.ContainerBuilderHelper.build(config_builder, state) do {:reuse, config, hash} -> @@ -825,10 +842,10 @@ defmodule Testcontainers do defp resolve_pull_policy(%{pull_policy: nil} = config, properties) do pull_policy = - case Map.get(properties, "pull.policy", "always") do - "missing" -> PullPolicy.never_pull() + case Map.get(properties, "pull.policy", "missing") do + "always" -> PullPolicy.always_pull() "never" -> PullPolicy.never_pull() - _ -> PullPolicy.always_pull() + _ -> PullPolicy.pull_if_missing() end %{config | pull_policy: pull_policy} @@ -850,14 +867,31 @@ defmodule Testcontainers do end end - defp maybe_pull_image(config = %{pull_policy: %{always_pull: true}}, conn) do + defp maybe_pull_image(%{pull_policy: %{always_pull: true}} = config, conn) do case Api.pull_image(config.image, conn, auth: config.auth) do {:ok, _nil} -> :ok error -> error end end - defp maybe_pull_image(config = %{pull_policy: %{pull_condition: expr}}, conn) + defp maybe_pull_image(%{pull_policy: %{pull_if_missing: true}} = config, conn) do + case Api.image_exists?(config.image, conn) do + {:ok, true} -> + Logger.debug("Image #{config.image} already present locally, skipping pull") + :ok + + {:ok, false} -> + case Api.pull_image(config.image, conn, auth: config.auth) do + {:ok, _nil} -> :ok + error -> error + end + + error -> + error + end + end + + defp maybe_pull_image(%{pull_policy: %{pull_condition: expr}} = config, conn) when is_function(expr) do with {:eval, true} <- {:eval, expr.(config, conn)}, {:ok, _nil} <- Api.pull_image(config.image, conn, auth: config.auth) do diff --git a/lib/util/constants.ex b/lib/util/constants.ex index 201241f4..1746375f 100644 --- a/lib/util/constants.ex +++ b/lib/util/constants.ex @@ -9,7 +9,7 @@ defmodule Testcontainers.Constants do def container_reuse_hash_label, do: "org.testcontainers.reuse-hash" def container_reuse, do: "org.testcontainers.reuse" def container_lang_value, do: Elixir |> Atom.to_string() |> String.downcase() - def container_sessionId_label, do: "org.testcontainers.session-id" + def container_session_id_label, do: "org.testcontainers.session-id" def container_version_label, do: "org.testcontainers.version" def user_agent, do: "tc-elixir/" <> __MODULE__.library_version() end diff --git a/lib/util/hash.ex b/lib/util/hash.ex index 39576b21..10fbf444 100644 --- a/lib/util/hash.ex +++ b/lib/util/hash.ex @@ -1,4 +1,5 @@ defmodule Testcontainers.Util.Hash do + @moduledoc false alias Testcontainers.Util.ListFromDeepStruct def struct_to_hash(struct) when is_struct(struct) do diff --git a/lib/util/struct.ex b/lib/util/struct.ex index 6a841dc2..6a8d3f1e 100644 --- a/lib/util/struct.ex +++ b/lib/util/struct.ex @@ -1,4 +1,5 @@ defmodule Testcontainers.Util.ListFromDeepStruct do + @moduledoc false def from_deep_struct(%{} = map), do: convert(map) defp convert(%Regex{} = data) do diff --git a/lib/wait_strategy/command_wait_strategy.ex b/lib/wait_strategy/command_wait_strategy.ex index e3f83166..af7e1432 100644 --- a/lib/wait_strategy/command_wait_strategy.ex +++ b/lib/wait_strategy/command_wait_strategy.ex @@ -34,9 +34,10 @@ defmodule Testcontainers.CommandWaitStrategy do # Main loop for waiting strategy defp perform_recursive_wait(wait_strategy, container_id, conn, started_at) do - with {:ok, 0} <- execute_command_and_wait(wait_strategy, container_id, conn) do - :ok - else + case execute_command_and_wait(wait_strategy, container_id, conn) do + {:ok, 0} -> + :ok + {:ok, exit_code} -> handle_non_zero_exit(wait_strategy, container_id, exit_code, conn, started_at) @@ -88,7 +89,7 @@ defmodule Testcontainers.CommandWaitStrategy do end end - defp get_current_time_millis(), do: System.monotonic_time(:millisecond) + defp get_current_time_millis, do: System.monotonic_time(:millisecond) defp timed_out?(started_at, timeout), do: get_current_time_millis() - started_at > timeout diff --git a/lib/wait_strategy/http_wait_strategy.ex b/lib/wait_strategy/http_wait_strategy.ex index 25a82c91..df4fe42f 100644 --- a/lib/wait_strategy/http_wait_strategy.ex +++ b/lib/wait_strategy/http_wait_strategy.ex @@ -69,8 +69,8 @@ defmodule Testcontainers.HttpWaitStrategy do # Private functions and implementations defimpl Testcontainers.WaitStrategy do - alias Testcontainers.HttpWaitStrategy alias Testcontainers.Container + alias Testcontainers.HttpWaitStrategy @impl true def wait_until_container_is_ready(wait_strategy, container, _conn) do diff --git a/lib/wait_strategy/log_wait_strategy.ex b/lib/wait_strategy/log_wait_strategy.ex index 8b09e68f..e4a3c1f6 100644 --- a/lib/wait_strategy/log_wait_strategy.ex +++ b/lib/wait_strategy/log_wait_strategy.ex @@ -51,14 +51,13 @@ defmodule Testcontainers.LogWaitStrategy do end defp log_matches?(container_id, log_regex, conn) do - with {:ok, log_output} <- Docker.Api.stdout_logs(container_id, conn) do - Regex.match?(log_regex, log_output) - else + case Docker.Api.stdout_logs(container_id, conn) do + {:ok, log_output} -> Regex.match?(log_regex, log_output) _ -> false end end - defp get_current_time_millis(), do: System.monotonic_time(:millisecond) + defp get_current_time_millis, do: System.monotonic_time(:millisecond) defp reached_timeout?(start_time, timeout), do: get_current_time_millis() - start_time > timeout diff --git a/lib/wait_strategy/port_wait_strategy.ex b/lib/wait_strategy/port_wait_strategy.ex index 38175172..6a3f32cf 100644 --- a/lib/wait_strategy/port_wait_strategy.ex +++ b/lib/wait_strategy/port_wait_strategy.ex @@ -73,7 +73,7 @@ defmodule Testcontainers.PortWaitStrategy do end end - defp current_time_millis(), do: System.monotonic_time(:millisecond) + defp current_time_millis, do: System.monotonic_time(:millisecond) defp reached_timeout?(timeout, start_time), do: current_time_millis() - start_time > timeout diff --git a/lib/wait_strategy/protocols/wait_strategy.ex b/lib/wait_strategy/protocols/wait_strategy.ex index ff48d280..c5d43e4a 100644 --- a/lib/wait_strategy/protocols/wait_strategy.ex +++ b/lib/wait_strategy/protocols/wait_strategy.ex @@ -7,7 +7,7 @@ defprotocol Testcontainers.WaitStrategy do """ alias Testcontainers.Container - @spec wait_until_container_is_ready(t(), %Container{}, Tesla.Env.client()) :: + @spec wait_until_container_is_ready(t(), Container.t(), Tesla.Env.client()) :: :ok | {:error, atom()} def wait_until_container_is_ready(wait_strategy, container, conn) end diff --git a/mix.exs b/mix.exs index 80aead77..ac221213 100644 --- a/mix.exs +++ b/mix.exs @@ -50,6 +50,7 @@ defmodule TestcontainersElixir.MixProject do defp deps do [ {:uniq, "~> 0.6"}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.3", only: [:dev], runtime: false}, {:ex_doc, "~> 0.30", only: :dev, runtime: false}, {:tesla, "~> 1.7"}, diff --git a/mix.lock b/mix.lock index 4bebf6ed..a4d74297 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,10 @@ %{ + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "crc32cer": {:hex, :crc32cer, "0.1.12", "b018bd5dcbba9c35972822f53ad40b6b483d453204ef67daf92af3a314bbfbf6", [:rebar3], [], "hexpm", "56ad9380651c2c4cb21d7741c91cbcc4709e032fd31a98a33f007ee30e526972"}, + "credo": {:hex, :credo, "1.7.18", "5c5596bf7aedf9c8c227f13272ac499fe8eae6237bd326f2f07dfc173786f042", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a189d164685fd945809e862fe76a7420c4398fa288d76257662aecb909d6b3e5"}, "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, @@ -12,6 +14,7 @@ "ex_aws": {:hex, :ex_aws, "2.6.1", "194582c7b09455de8a5ab18a0182e6dd937d53df82be2e63c619d01bddaccdfa", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "67842a08c90a1d9a09dbe4ac05754175c7ca253abe4912987c759395d4bd9d26"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.9", "862b7792f2e60d7010e2920d79964e3fab289bc0fd951b0ba8457a3f7f9d1199", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "a480d2bb2da64610014021629800e1e9457ca5e4a62f6775bffd963360c2bf90"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "fs": {:hex, :fs, "11.4.1", "11fb3153bb2e1de851b8263bb5698d526894853c73a525ebeb5e69108b2d25cd", [:rebar3], [], "hexpm", "dd00a61d89eac01d16d3fc51d5b0eb5f0722ef8e3c1a3a547cd086957f3260a9"}, "gen_state_machine": {:hex, :gen_state_machine, "3.0.0", "1e57f86a494e5c6b14137ebef26a7eb342b3b0070c7135f2d6768ed3f6b6cdff", [:mix], [], "hexpm", "0a59652574bebceb7309f6b749d2a41b45fdeda8dbb4da0791e355dd19f0ed15"}, "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, diff --git a/test/compose/cli_test.exs b/test/compose/cli_test.exs index 7576daa5..460ead8e 100644 --- a/test/compose/cli_test.exs +++ b/test/compose/cli_test.exs @@ -202,12 +202,12 @@ defmodule Testcontainers.Compose.CliTest do %{ "URL" => "0.0.0.0", "TargetPort" => 6379, - "PublishedPort" => 32768, + "PublishedPort" => 32_768, "Protocol" => "tcp" } ] - assert Cli.parse_publishers(publishers) == [{6379, 32768}] + assert Cli.parse_publishers(publishers) == [{6379, 32_768}] end test "filters out publishers with PublishedPort of 0" do @@ -228,21 +228,21 @@ defmodule Testcontainers.Compose.CliTest do %{ "URL" => "0.0.0.0", "TargetPort" => 6379, - "PublishedPort" => 32768, + "PublishedPort" => 32_768, "Protocol" => "tcp" }, %{ "URL" => "0.0.0.0", "TargetPort" => 5432, - "PublishedPort" => 32769, + "PublishedPort" => 32_769, "Protocol" => "tcp" } ] result = Cli.parse_publishers(publishers) assert length(result) == 2 - assert {6379, 32768} in result - assert {5432, 32769} in result + assert {6379, 32_768} in result + assert {5432, 32_769} in result end test "deduplicates port tuples" do @@ -250,18 +250,18 @@ defmodule Testcontainers.Compose.CliTest do %{ "URL" => "0.0.0.0", "TargetPort" => 6379, - "PublishedPort" => 32768, + "PublishedPort" => 32_768, "Protocol" => "tcp" }, %{ "URL" => "::", "TargetPort" => 6379, - "PublishedPort" => 32768, + "PublishedPort" => 32_768, "Protocol" => "tcp" } ] - assert Cli.parse_publishers(publishers) == [{6379, 32768}] + assert Cli.parse_publishers(publishers) == [{6379, 32_768}] end test "handles nil publishers" do diff --git a/test/compose/compose_integration_test.exs b/test/compose/compose_integration_test.exs index 376d9ee4..40817239 100644 --- a/test/compose/compose_integration_test.exs +++ b/test/compose/compose_integration_test.exs @@ -3,8 +3,8 @@ defmodule Testcontainers.Compose.ComposeIntegrationTest do @moduletag :integration - alias Testcontainers.DockerCompose alias Testcontainers.Compose.ComposeEnvironment + alias Testcontainers.DockerCompose @fixtures_path Path.expand("../fixtures", __DIR__) diff --git a/test/connection/docker_host_strategy/docker_host_from_env_test.exs b/test/connection/docker_host_strategy/docker_host_from_env_test.exs index ae3b34ae..c9c378ee 100644 --- a/test/connection/docker_host_strategy/docker_host_from_env_test.exs +++ b/test/connection/docker_host_strategy/docker_host_from_env_test.exs @@ -1,8 +1,8 @@ defmodule Testcontainers.Connection.DockerHostStrategy.DockerHostFromEnvTest do use ExUnit.Case, async: true - alias Testcontainers.DockerHostStrategyEvaluator alias Testcontainers.DockerHostFromEnvStrategy + alias Testcontainers.DockerHostStrategyEvaluator describe "DockerHostFromEnvStrategy" do setup do diff --git a/test/connection/docker_host_strategy/docker_host_from_properties_test.exs b/test/connection/docker_host_strategy/docker_host_from_properties_test.exs index 1046bd34..1646116f 100644 --- a/test/connection/docker_host_strategy/docker_host_from_properties_test.exs +++ b/test/connection/docker_host_strategy/docker_host_from_properties_test.exs @@ -1,8 +1,8 @@ defmodule Testcontainers.Connection.DockerHostStrategy.DockerHostFromPropertiesTest do use ExUnit.Case, async: true - alias Testcontainers.DockerHostStrategyEvaluator alias Testcontainers.DockerHostFromPropertiesStrategy + alias Testcontainers.DockerHostStrategyEvaluator describe "DockerHostFromPropertiesStrategy" do test "should return :econnrefused response if property file exist but is not an open url" do diff --git a/test/constants_test.exs b/test/constants_test.exs index 024b9288..5c3a7f0c 100644 --- a/test/constants_test.exs +++ b/test/constants_test.exs @@ -3,7 +3,7 @@ defmodule Testcontainers.ConstantsTest do test "have correct values" do assert Testcontainers.Constants.container_label() == "org.testcontainers" - assert Testcontainers.Constants.container_sessionId_label() == "org.testcontainers.session-id" + assert Testcontainers.Constants.container_session_id_label() == "org.testcontainers.session-id" assert Testcontainers.Constants.container_reuse_hash_label() == "org.testcontainers.reuse-hash" diff --git a/test/container/container_builder_helper_test.exs b/test/container/container_builder_helper_test.exs index 22911b32..60114eec 100644 --- a/test/container/container_builder_helper_test.exs +++ b/test/container/container_builder_helper_test.exs @@ -12,7 +12,7 @@ defmodule Testcontainers.ContainerBuilderHelperTest do {:noreuse, built, nil} = ContainerBuilderHelper.build(builder, state) assert Map.get(built.labels, container_reuse()) == "false" assert Map.get(built.labels, container_reuse_hash_label()) == nil - assert Map.get(built.labels, container_sessionId_label()) == "123" + assert Map.get(built.labels, container_session_id_label()) == "123" assert Map.get(built.labels, container_version_label()) == library_version() assert Map.get(built.labels, container_lang_label()) == container_lang_value() assert Map.get(built.labels, container_label()) == "true" @@ -27,7 +27,7 @@ defmodule Testcontainers.ContainerBuilderHelperTest do assert hash != nil assert Map.get(built.labels, container_reuse()) == "true" assert Map.get(built.labels, container_reuse_hash_label()) != nil - assert Map.get(built.labels, container_sessionId_label()) == "123" + assert Map.get(built.labels, container_session_id_label()) == "123" assert Map.get(built.labels, container_version_label()) == library_version() assert Map.get(built.labels, container_lang_label()) == container_lang_value() assert Map.get(built.labels, container_label()) == "true" diff --git a/test/container/emqx_container_test.exs b/test/container/emqx_container_test.exs index a7a18e63..6a995820 100644 --- a/test/container/emqx_container_test.exs +++ b/test/container/emqx_container_test.exs @@ -27,7 +27,7 @@ defmodule Testcontainers.Container.EmqxContainerTest do :emqx, EmqxContainer.new() |> EmqxContainer.with_image("emqx:5.5.1") - |> EmqxContainer.with_ports(1884, 8883, 8083, 8084, 18084) + |> EmqxContainer.with_ports(1884, 8883, 8083, 8084, 18_084) ) @tag :dood_limitation diff --git a/test/container/kafka_container_test.exs b/test/container/kafka_container_test.exs index 3b66fc7b..71acfb39 100644 --- a/test/container/kafka_container_test.exs +++ b/test/container/kafka_container_test.exs @@ -9,7 +9,7 @@ defmodule Testcontainers.Container.KafkaContainerTest do config = KafkaContainer.new() assert config.image == "apache/kafka:3.9.0" - assert config.kafka_port >= 29000 and config.kafka_port <= 29999 + assert config.kafka_port >= 29_000 and config.kafka_port <= 29_999 assert config.internal_kafka_port == 9092 assert config.controller_port == 9093 assert config.node_id == 1 diff --git a/test/container/mongo_container_test.exs b/test/container/mongo_container_test.exs index 53720d2c..bf12570b 100644 --- a/test/container/mongo_container_test.exs +++ b/test/container/mongo_container_test.exs @@ -16,7 +16,7 @@ defmodule Testcontainers.Container.MongoContainerTest do assert config.user == "test" assert config.password == "test" assert config.database == "test" - assert config.port == 27017 + assert config.port == 27_017 assert config.wait_timeout == 180_000 end diff --git a/test/container/toxiproxy_container_test.exs b/test/container/toxiproxy_container_test.exs index 99bc268b..fdf02e19 100644 --- a/test/container/toxiproxy_container_test.exs +++ b/test/container/toxiproxy_container_test.exs @@ -234,8 +234,8 @@ defmodule Testcontainers.Container.ToxiproxyContainerTest do @tag :dood_limitation test "can proxy and inject faults into Redis traffic", %{network_name: network_name} do - alias Testcontainers.RedisContainer alias Testcontainers.ContainerBuilder + alias Testcontainers.RedisContainer # Start Redis on the network redis_config = diff --git a/test/container_test.exs b/test/container_test.exs index 31c8be58..80bc16ce 100644 --- a/test/container_test.exs +++ b/test/container_test.exs @@ -1,8 +1,10 @@ defmodule Testcontainers.ContainerTest do use ExUnit.Case, async: true - alias Testcontainers.ContainerBuilder alias Testcontainers.Container + alias Testcontainers.ContainerBuilder + alias Testcontainers.PostgresContainer + alias Testcontainers.Util.Hash describe "with reuse" do test "sets reuse to true" do @@ -17,13 +19,13 @@ defmodule Testcontainers.ContainerTest do describe "hash" do test "returns the same hash for the same container" do - container1 = ContainerBuilder.build(Testcontainers.PostgresContainer.new()) - container2 = ContainerBuilder.build(Testcontainers.PostgresContainer.new()) + container1 = ContainerBuilder.build(PostgresContainer.new()) + container2 = ContainerBuilder.build(PostgresContainer.new()) - assert Testcontainers.Util.Hash.struct_to_hash(container1) == + assert Hash.struct_to_hash(container1) == "a0b9a403e485c224323eabc27b2b8e94a6353c785cdc27f9ac2b8a9b67a47cb1" - assert Testcontainers.Util.Hash.struct_to_hash(container2) == + assert Hash.struct_to_hash(container2) == "a0b9a403e485c224323eabc27b2b8e94a6353c785cdc27f9ac2b8a9b67a47cb1" end end diff --git a/test/generic_container_test.exs b/test/generic_container_test.exs index b4df1190..834c39d3 100644 --- a/test/generic_container_test.exs +++ b/test/generic_container_test.exs @@ -17,14 +17,14 @@ defmodule Testcontainers.GenericContainerTest do @tag :needs_root @tag :dood_limitation test "can start and stop generic container with network mode set to host" do - if not is_os(:linux) do - Logger.warning("Host is not Linux, therefore not running network_mode test") - else + if is_os(:linux) do config = %Testcontainers.Container{image: "redis:latest", network_mode: "host"} assert {:ok, container} = Testcontainers.start_container(config) Process.sleep(5000) assert :ok = port_open?("127.0.0.1", 6379) assert :ok = Testcontainers.stop_container(container.container_id) + else + Logger.warning("Host is not Linux, therefore not running network_mode test") end end end diff --git a/test/pull_policy_test.exs b/test/pull_policy_test.exs index 0816b3e0..e466c9b6 100644 --- a/test/pull_policy_test.exs +++ b/test/pull_policy_test.exs @@ -1,12 +1,17 @@ defmodule Testcontainers.PullPolicyTest do use ExUnit.Case, async: true + alias Testcontainers.Connection + alias Testcontainers.Container + alias Testcontainers.Docker.Api + alias Testcontainers.PullPolicy + @moduletag :needs_registry test "always_pull/0 fetches image from remote repository" do - config = %Testcontainers.Container{ + config = %Container{ image: "alpine:latest", - pull_policy: Testcontainers.PullPolicy.always_pull() + pull_policy: PullPolicy.always_pull() } assert {:ok, container} = Testcontainers.start_container(config) @@ -14,13 +19,13 @@ defmodule Testcontainers.PullPolicyTest do end test "never_pull/0 does not fetch image from remote repository" do - {conn, _url, _host} = Testcontainers.Connection.get_connection() - {:ok, _nil} = Testcontainers.Docker.Api.pull_image("alpine:latest", conn) - {:ok, _name} = Testcontainers.Docker.Api.tag_image("alpine", "local_alpine", "latest", conn) + {conn, _url, _host} = Connection.get_connection() + {:ok, _nil} = Api.pull_image("alpine:latest", conn) + {:ok, _name} = Api.tag_image("alpine", "local_alpine", "latest", conn) - config = %Testcontainers.Container{ + config = %Container{ image: "local_alpine:latest", - pull_policy: Testcontainers.PullPolicy.never_pull() + pull_policy: PullPolicy.never_pull() } assert {:ok, container} = Testcontainers.start_container(config) @@ -28,10 +33,10 @@ defmodule Testcontainers.PullPolicyTest do end test "pull_condition/1 fetches image if expression evaluates to true" do - config = %Testcontainers.Container{ + config = %Container{ image: "alpine:latest", pull_policy: - Testcontainers.PullPolicy.pull_condition(fn _config, _conn -> + PullPolicy.pull_condition(fn _config, _conn -> true end) } @@ -41,14 +46,14 @@ defmodule Testcontainers.PullPolicyTest do end test "pull_condition/1 does not fetch image if expression evaluates to a falsey value" do - {conn, _url, _host} = Testcontainers.Connection.get_connection() - {:ok, _nil} = Testcontainers.Docker.Api.pull_image("alpine:latest", conn) - {:ok, _name} = Testcontainers.Docker.Api.tag_image("alpine", "local_alpine2", "latest", conn) + {conn, _url, _host} = Connection.get_connection() + {:ok, _nil} = Api.pull_image("alpine:latest", conn) + {:ok, _name} = Api.tag_image("alpine", "local_alpine2", "latest", conn) - config = %Testcontainers.Container{ + config = %Container{ image: "local_alpine2:latest", pull_policy: - Testcontainers.PullPolicy.pull_condition(fn _config, _conn -> + PullPolicy.pull_condition(fn _config, _conn -> false end) } @@ -56,4 +61,31 @@ defmodule Testcontainers.PullPolicyTest do assert {:ok, container} = Testcontainers.start_container(config) assert :ok = Testcontainers.stop_container(container.container_id) end + + test "pull_if_missing/0 starts a container when image already exists locally" do + {conn, _url, _host} = Connection.get_connection() + {:ok, _nil} = Api.pull_image("alpine:latest", conn) + {:ok, _name} = Api.tag_image("alpine", "local_alpine3", "latest", conn) + + config = %Container{ + image: "local_alpine3:latest", + pull_policy: PullPolicy.pull_if_missing() + } + + assert {:ok, container} = Testcontainers.start_container(config) + assert :ok = Testcontainers.stop_container(container.container_id) + end + + test "pull_if_missing/0 fetches image when not present locally" do + {conn, _url, _host} = Connection.get_connection() + _ = Api.delete_image("alpine:3.19", conn) + + config = %Container{ + image: "alpine:3.19", + pull_policy: PullPolicy.pull_if_missing() + } + + assert {:ok, container} = Testcontainers.start_container(config) + assert :ok = Testcontainers.stop_container(container.container_id) + end end diff --git a/test/support/nginx_container.ex b/test/support/nginx_container.ex index d2df0f7a..1676a674 100644 --- a/test/support/nginx_container.ex +++ b/test/support/nginx_container.ex @@ -1,4 +1,5 @@ defmodule Test.NginxContainer do + @moduledoc false defstruct [] defimpl Testcontainers.ContainerBuilder do diff --git a/test/support/test_helper.ex b/test/support/test_helper.ex index 3176c160..a0b61433 100644 --- a/test/support/test_helper.ex +++ b/test/support/test_helper.ex @@ -1,4 +1,5 @@ defmodule TestHelper do + @moduledoc false @doc """ Waits for the specified GenServer to change its running state (either start or stop). diff --git a/test/testcontainers_test.exs b/test/testcontainers_test.exs index 5f0f676d..c140c997 100644 --- a/test/testcontainers_test.exs +++ b/test/testcontainers_test.exs @@ -1,7 +1,7 @@ defmodule TestcontainersTest do alias Testcontainers.Connection - alias Testcontainers.Docker alias Testcontainers.Container + alias Testcontainers.Docker use ExUnit.Case, async: true test "cleans up containers on terminate" do diff --git a/test/wait_strategy/http_wait_strategy_test.exs b/test/wait_strategy/http_wait_strategy_test.exs index c0cc0035..bd0b448a 100644 --- a/test/wait_strategy/http_wait_strategy_test.exs +++ b/test/wait_strategy/http_wait_strategy_test.exs @@ -1,6 +1,6 @@ defmodule Testcontainers.HttpWaitStrategyTest do - alias Testcontainers.HttpWaitStrategy alias Testcontainers.Container + alias Testcontainers.HttpWaitStrategy use ExUnit.Case, async: true test "can wait for a http request and retrieve content" do