Packages

Testcontainers for Gleam: typed container lifecycle with crash-safe cleanup and a native Unix-socket Docker client.

Current section

Files

Jump to
testcontainer src testcontainer@container.erl
Raw

src/testcontainer@container.erl

-module(testcontainer@container).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/testcontainer/container.gleam").
-export([bind_mount/2, readonly_bind_mount/2, tmpfs/1, volume_kind/1, new/1, with_env/3, with_envs/2, with_secret_env/3, expose_port/2, expose_ports/2, wait_for/2, with_command/2, with_entrypoint/2, with_volume/2, with_bind_mount/3, with_readonly_bind/3, with_tmpfs/2, on_network/2, with_name/2, with_label/3, with_privileged/1, image/1, env/1, ports/1, wait_strategy/1, command/1, entrypoint/1, volumes/1, network/1, name/1, labels/1, is_privileged/1, build/5, with_keep/2, stop_timeout_sec/1, id/1, host/1, keep/1, host_port/2, mapped_url/3]).
-export_type([volume/0, container_spec/0, container/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-opaque volume() :: {bind_mount, binary(), binary(), boolean()} |
{tmpfs_mount, binary()}.
-opaque container_spec() :: {container_spec,
binary(),
list({binary(), cowl:secret(binary())}),
list(testcontainer@port:port_()),
testcontainer@wait:wait_strategy(),
gleam@option:option(list(binary())),
gleam@option:option(list(binary())),
list(volume()),
gleam@option:option(binary()),
gleam@option:option(binary()),
list({binary(), binary()}),
boolean()}.
-opaque container() :: {container,
binary(),
binary(),
gleam@dict:dict({integer(), binary()}, integer()),
boolean(),
integer()}.
-file("src/testcontainer/container.gleam", 20).
?DOC(" Creates a read-write bind mount from `host_path` to `container_path`.\n").
-spec bind_mount(binary(), binary()) -> volume().
bind_mount(Host_path, Container_path) ->
{bind_mount, Host_path, Container_path, false}.
-file("src/testcontainer/container.gleam", 25).
?DOC(" Creates a read-only bind mount.\n").
-spec readonly_bind_mount(binary(), binary()) -> volume().
readonly_bind_mount(Host_path, Container_path) ->
{bind_mount, Host_path, Container_path, true}.
-file("src/testcontainer/container.gleam", 33).
?DOC(" Creates an in-memory tmpfs mount at `container_path`.\n").
-spec tmpfs(binary()) -> volume().
tmpfs(Container_path) ->
{tmpfs_mount, Container_path}.
-file("src/testcontainer/container.gleam", 42).
?DOC(false).
-spec volume_kind(volume()) -> {ok, {binary(), binary(), boolean()}} |
{error, binary()}.
volume_kind(V) ->
case V of
{bind_mount, H, C, Ro} ->
{ok, {H, C, Ro}};
{tmpfs_mount, C@1} ->
{error, C@1}
end.
-file("src/testcontainer/container.gleam", 71).
?DOC(
" Creates a new spec for the given image (e.g. `\"redis:7-alpine\"`).\n"
" The default wait strategy is `wait.none/0` (no wait) - most images need\n"
" a real `wait_for/2` (e.g. `wait.log(\"ready\")`) to be reliable.\n"
).
-spec new(binary()) -> container_spec().
new(Image) ->
{container_spec,
Image,
[],
[],
testcontainer@wait:none(),
none,
none,
[],
none,
none,
[],
false}.
-file("src/testcontainer/container.gleam", 89).
?DOC(
" Adds an environment variable. The value is wrapped in a `cowl.Secret`\n"
" internally; use `with_secret_env/3` if you already have a Secret.\n"
).
-spec with_env(container_spec(), binary(), binary()) -> container_spec().
with_env(Spec, Key, Value) ->
{container_spec,
erlang:element(2, Spec),
lists:append(erlang:element(3, Spec), [{Key, cowl:secret(Value)}]),
erlang:element(4, Spec),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 101).
?DOC(" Adds multiple environment variables at once.\n").
-spec with_envs(container_spec(), list({binary(), binary()})) -> container_spec().
with_envs(Spec, Pairs) ->
Wrapped = gleam@list:map(
Pairs,
fun(P) -> {erlang:element(1, P), cowl:secret(erlang:element(2, P))} end
),
{container_spec,
erlang:element(2, Spec),
lists:append(erlang:element(3, Spec), Wrapped),
erlang:element(4, Spec),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 110).
?DOC(" Adds an environment variable whose value is already a `cowl.Secret`.\n").
-spec with_secret_env(container_spec(), binary(), cowl:secret(binary())) -> container_spec().
with_secret_env(Spec, Key, Value) ->
{container_spec,
erlang:element(2, Spec),
lists:append(erlang:element(3, Spec), [{Key, Value}]),
erlang:element(4, Spec),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 119).
?DOC(" Exposes a single container port to the host (host port assigned dynamically).\n").
-spec expose_port(container_spec(), testcontainer@port:port_()) -> container_spec().
expose_port(Spec, P) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
lists:append(erlang:element(4, Spec), [P]),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 124).
?DOC(" Exposes multiple container ports to the host.\n").
-spec expose_ports(container_spec(), list(testcontainer@port:port_())) -> container_spec().
expose_ports(Spec, Ps) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
lists:append(erlang:element(4, Spec), Ps),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 129).
?DOC(" Sets the readiness wait strategy. The default is \"no wait\".\n").
-spec wait_for(container_spec(), testcontainer@wait:wait_strategy()) -> container_spec().
wait_for(Spec, Strategy) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
erlang:element(4, Spec),
Strategy,
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 137).
?DOC(" Overrides the container's CMD.\n").
-spec with_command(container_spec(), list(binary())) -> container_spec().
with_command(Spec, Cmd) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
erlang:element(4, Spec),
erlang:element(5, Spec),
{some, Cmd},
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 142).
?DOC(" Overrides the container's ENTRYPOINT.\n").
-spec with_entrypoint(container_spec(), list(binary())) -> container_spec().
with_entrypoint(Spec, Ep) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
erlang:element(4, Spec),
erlang:element(5, Spec),
erlang:element(6, Spec),
{some, Ep},
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 170).
?DOC(" Adds an arbitrary `Volume` (built with `bind_mount/2`, `tmpfs/1`, …).\n").
-spec with_volume(container_spec(), volume()) -> container_spec().
with_volume(Spec, V) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
erlang:element(4, Spec),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
lists:append(erlang:element(8, Spec), [V]),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 147).
?DOC(" Adds a read-write bind mount (host path → container path).\n").
-spec with_bind_mount(container_spec(), binary(), binary()) -> container_spec().
with_bind_mount(Spec, Host, Container_path) ->
with_volume(Spec, bind_mount(Host, Container_path)).
-file("src/testcontainer/container.gleam", 156).
?DOC(" Adds a read-only bind mount.\n").
-spec with_readonly_bind(container_spec(), binary(), binary()) -> container_spec().
with_readonly_bind(Spec, Host, Container_path) ->
with_volume(Spec, readonly_bind_mount(Host, Container_path)).
-file("src/testcontainer/container.gleam", 165).
?DOC(" Mounts an in-memory tmpfs at the given path inside the container.\n").
-spec with_tmpfs(container_spec(), binary()) -> container_spec().
with_tmpfs(Spec, Path) ->
with_volume(Spec, tmpfs(Path)).
-file("src/testcontainer/container.gleam", 175).
?DOC(" Attaches the container to the named Docker network.\n").
-spec on_network(container_spec(), binary()) -> container_spec().
on_network(Spec, Network) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
erlang:element(4, Spec),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
{some, Network},
erlang:element(10, Spec),
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 180).
?DOC(" Assigns a fixed name to the container (otherwise Docker auto-generates one).\n").
-spec with_name(container_spec(), binary()) -> container_spec().
with_name(Spec, N) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
erlang:element(4, Spec),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
{some, N},
erlang:element(11, Spec),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 185).
?DOC(" Adds a label to the container.\n").
-spec with_label(container_spec(), binary(), binary()) -> container_spec().
with_label(Spec, Key, Value) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
erlang:element(4, Spec),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
lists:append(erlang:element(11, Spec), [{Key, Value}]),
erlang:element(12, Spec)}.
-file("src/testcontainer/container.gleam", 194).
?DOC(" Marks the container as privileged.\n").
-spec with_privileged(container_spec()) -> container_spec().
with_privileged(Spec) ->
{container_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
erlang:element(4, Spec),
erlang:element(5, Spec),
erlang:element(6, Spec),
erlang:element(7, Spec),
erlang:element(8, Spec),
erlang:element(9, Spec),
erlang:element(10, Spec),
erlang:element(11, Spec),
true}.
-file("src/testcontainer/container.gleam", 200).
-spec image(container_spec()) -> binary().
image(Spec) ->
erlang:element(2, Spec).
-file("src/testcontainer/container.gleam", 204).
-spec env(container_spec()) -> list({binary(), cowl:secret(binary())}).
env(Spec) ->
erlang:element(3, Spec).
-file("src/testcontainer/container.gleam", 208).
-spec ports(container_spec()) -> list(testcontainer@port:port_()).
ports(Spec) ->
erlang:element(4, Spec).
-file("src/testcontainer/container.gleam", 212).
-spec wait_strategy(container_spec()) -> testcontainer@wait:wait_strategy().
wait_strategy(Spec) ->
erlang:element(5, Spec).
-file("src/testcontainer/container.gleam", 216).
-spec command(container_spec()) -> gleam@option:option(list(binary())).
command(Spec) ->
erlang:element(6, Spec).
-file("src/testcontainer/container.gleam", 220).
-spec entrypoint(container_spec()) -> gleam@option:option(list(binary())).
entrypoint(Spec) ->
erlang:element(7, Spec).
-file("src/testcontainer/container.gleam", 224).
-spec volumes(container_spec()) -> list(volume()).
volumes(Spec) ->
erlang:element(8, Spec).
-file("src/testcontainer/container.gleam", 228).
-spec network(container_spec()) -> gleam@option:option(binary()).
network(Spec) ->
erlang:element(9, Spec).
-file("src/testcontainer/container.gleam", 232).
-spec name(container_spec()) -> gleam@option:option(binary()).
name(Spec) ->
erlang:element(10, Spec).
-file("src/testcontainer/container.gleam", 236).
-spec labels(container_spec()) -> list({binary(), binary()}).
labels(Spec) ->
erlang:element(11, Spec).
-file("src/testcontainer/container.gleam", 240).
-spec is_privileged(container_spec()) -> boolean().
is_privileged(Spec) ->
erlang:element(12, Spec).
-file("src/testcontainer/container.gleam", 267).
?DOC(false).
-spec build(
binary(),
binary(),
gleam@dict:dict({integer(), binary()}, integer()),
boolean(),
integer()
) -> container().
build(Id, Gateway_host, Port_mapping, Keep, Stop_timeout_sec) ->
{container, Id, Gateway_host, Port_mapping, Keep, Stop_timeout_sec}.
-file("src/testcontainer/container.gleam", 280).
?DOC(false).
-spec with_keep(container(), boolean()) -> container().
with_keep(C, Keep) ->
{container,
erlang:element(2, C),
erlang:element(3, C),
erlang:element(4, C),
Keep,
erlang:element(6, C)}.
-file("src/testcontainer/container.gleam", 287).
?DOC(false).
-spec stop_timeout_sec(container()) -> integer().
stop_timeout_sec(C) ->
erlang:element(6, C).
-file("src/testcontainer/container.gleam", 292).
?DOC(" Returns the Docker container id.\n").
-spec id(container()) -> binary().
id(C) ->
erlang:element(2, C).
-file("src/testcontainer/container.gleam", 299).
?DOC(
" Returns the host that the test runner should use to reach the\n"
" container's mapped ports (typically the bridge gateway IP, or\n"
" the value of `TESTCONTAINERS_HOST_OVERRIDE`).\n"
).
-spec host(container()) -> binary().
host(C) ->
erlang:element(3, C).
-file("src/testcontainer/container.gleam", 305).
?DOC(
" Whether this container should be kept alive after the test\n"
" (`TESTCONTAINERS_KEEP=true` or `start_and_keep/1`).\n"
).
-spec keep(container()) -> boolean().
keep(C) ->
erlang:element(5, C).
-file("src/testcontainer/container.gleam", 311).
?DOC(
" Returns the host port that maps to the given container port,\n"
" or `PortNotMapped` if the port is not exposed.\n"
).
-spec host_port(container(), testcontainer@port:port_()) -> {ok, integer()} |
{error, testcontainer@error:error()}.
host_port(C, P) ->
N = testcontainer@port:number(P),
Proto = testcontainer@port:protocol(P),
case gleam_stdlib:map_get(erlang:element(4, C), {N, Proto}) of
{ok, Hp} ->
{ok, Hp};
{error, nil} ->
{error, {port_not_mapped, N}}
end.
-file("src/testcontainer/container.gleam", 322).
?DOC(
" Builds a URL for the given container port using the configured scheme,\n"
" host, and mapped host port (e.g. `\"http://127.0.0.1:32768\"`).\n"
).
-spec mapped_url(container(), testcontainer@port:port_(), binary()) -> {ok,
binary()} |
{error, testcontainer@error:error()}.
mapped_url(C, P, Scheme) ->
case host_port(C, P) of
{ok, Hp} ->
{ok,
<<<<<<<<Scheme/binary, "://"/utf8>>/binary, (host(C))/binary>>/binary,
":"/utf8>>/binary,
(erlang:integer_to_binary(Hp))/binary>>};
{error, E} ->
{error, E}
end.