Current section
Files
Jump to
Current section
Files
src/dream_test@process.erl
-module(dream_test@process).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream_test/process.gleam").
-export([start_counter_with/1, start_counter/0, get_count/1, increment/1, decrement/1, set_count/2, unique_port/0, start_actor/2, call_actor/3, default_poll_config/0, quick_poll_config/0, await_some/2, await_ready/2]).
-export_type([counter_message/0, port_selection/0, poll_config/0, poll_result/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Process helpers for tests that need actors or async operations.\n"
"\n"
" When tests run in isolated BEAM processes, any processes spawned by the\n"
" test are automatically terminated when the test ends. This module provides\n"
" helpers for common patterns.\n"
"\n"
" ## Auto-Cleanup\n"
"\n"
" Processes started with these helpers are linked to the test process.\n"
" When the test completes (pass, fail, or timeout), all linked processes\n"
" are automatically cleaned up. No manual teardown needed.\n"
"\n"
" ## Quick Start\n"
"\n"
" ```gleam\n"
" import dream_test/process.{start_counter, get_count, increment}\n"
" import dream_test/unit.{describe, it}\n"
" import dream_test/assertions/should.{should, equal, or_fail_with}\n"
"\n"
" pub fn tests() {\n"
" describe(\"Counter\", [\n"
" it(\"increments correctly\", fn() {\n"
" let counter = start_counter()\n"
" increment(counter)\n"
" increment(counter)\n"
"\n"
" get_count(counter)\n"
" |> should()\n"
" |> equal(2)\n"
" |> or_fail_with(\"Counter should be 2\")\n"
" }),\n"
" ])\n"
" }\n"
" ```\n"
"\n"
" ## Available Helpers\n"
"\n"
" | Helper | Purpose |\n"
" |---------------------|----------------------------------------------|\n"
" | `start_counter` | Simple counter actor for testing state |\n"
" | `start_actor` | Generic actor with custom state and handler |\n"
" | `unique_port` | Generate random port for test servers |\n"
" | `await_ready` | Poll until a condition is true |\n"
" | `await_some` | Poll until a function returns Ok |\n"
).
-type counter_message() :: increment |
decrement |
{set_count, integer()} |
{get_count, gleam@erlang@process:subject(integer())}.
-type port_selection() :: {port, integer()} | random_port.
-type poll_config() :: {poll_config, integer(), integer()}.
-type poll_result(GGN) :: {ready, GGN} | timed_out.
-file("src/dream_test/process.gleam", 104).
-spec handle_counter_message(integer(), counter_message()) -> gleam@otp@actor:next(integer(), counter_message()).
handle_counter_message(State, Message) ->
case Message of
increment ->
gleam@otp@actor:continue(State + 1);
decrement ->
gleam@otp@actor:continue(State - 1);
{set_count, Value} ->
gleam@otp@actor:continue(Value);
{get_count, Reply_to} ->
gleam@erlang@process:send(Reply_to, State),
gleam@otp@actor:continue(State)
end.
-file("src/dream_test/process.gleam", 95).
?DOC(
" Start a counter actor with a specific initial value.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let counter = start_counter_with(100)\n"
" decrement(counter)\n"
" get_count(counter) // -> 99\n"
" ```\n"
).
-spec start_counter_with(integer()) -> gleam@erlang@process:subject(counter_message()).
start_counter_with(Initial) ->
Started@1 = case begin
_pipe = gleam@otp@actor:new(Initial),
_pipe@1 = gleam@otp@actor:on_message(
_pipe,
fun handle_counter_message/2
),
gleam@otp@actor:start(_pipe@1)
end of
{ok, Started} -> Started;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"dream_test/process"/utf8>>,
function => <<"start_counter_with"/utf8>>,
line => 96,
value => _assert_fail,
start => 2774,
'end' => 2888,
pattern_start => 2785,
pattern_end => 2796})
end,
erlang:element(3, Started@1).
-file("src/dream_test/process.gleam", 81).
?DOC(
" Start a counter actor initialized to 0.\n"
"\n"
" The counter is linked to the test process and will be automatically\n"
" cleaned up when the test ends.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let counter = start_counter()\n"
" increment(counter)\n"
" increment(counter)\n"
" get_count(counter) // -> 2\n"
" ```\n"
).
-spec start_counter() -> gleam@erlang@process:subject(counter_message()).
start_counter() ->
start_counter_with(0).
-file("src/dream_test/process.gleam", 131).
?DOC(
" Get the current value from a counter.\n"
"\n"
" This is a synchronous call that blocks until the counter responds.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let counter = start_counter()\n"
" increment(counter)\n"
" let value = get_count(counter) // -> 1\n"
" ```\n"
).
-spec get_count(gleam@erlang@process:subject(counter_message())) -> integer().
get_count(Counter) ->
gleam@otp@actor:call(
Counter,
1000,
fun(Field@0) -> {get_count, Field@0} end
).
-file("src/dream_test/process.gleam", 148).
?DOC(
" Increment a counter by 1.\n"
"\n"
" This is an asynchronous send—it returns immediately.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let counter = start_counter()\n"
" increment(counter)\n"
" increment(counter)\n"
" get_count(counter) // -> 2\n"
" ```\n"
).
-spec increment(gleam@erlang@process:subject(counter_message())) -> nil.
increment(Counter) ->
gleam@erlang@process:send(Counter, increment).
-file("src/dream_test/process.gleam", 164).
?DOC(
" Decrement a counter by 1.\n"
"\n"
" This is an asynchronous send—it returns immediately.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let counter = start_counter_with(10)\n"
" decrement(counter)\n"
" get_count(counter) // -> 9\n"
" ```\n"
).
-spec decrement(gleam@erlang@process:subject(counter_message())) -> nil.
decrement(Counter) ->
gleam@erlang@process:send(Counter, decrement).
-file("src/dream_test/process.gleam", 180).
?DOC(
" Set a counter to a specific value.\n"
"\n"
" This is an asynchronous send—it returns immediately.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let counter = start_counter()\n"
" set_count(counter, 42)\n"
" get_count(counter) // -> 42\n"
" ```\n"
).
-spec set_count(gleam@erlang@process:subject(counter_message()), integer()) -> nil.
set_count(Counter, Value) ->
gleam@erlang@process:send(Counter, {set_count, Value}).
-file("src/dream_test/process.gleam", 215).
?DOC(
" Generate a unique port number for test servers.\n"
"\n"
" Returns a random port between 10,000 and 60,000. This range avoids:\n"
" - Well-known ports (0-1023)\n"
" - Registered ports commonly used by services (1024-9999)\n"
" - Ports near the upper limit that some systems reserve\n"
"\n"
" Using random ports prevents conflicts when running tests in parallel.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let port = unique_port()\n"
" start_server(port)\n"
" // port is something like 34521\n"
" ```\n"
).
-spec unique_port() -> integer().
unique_port() ->
Base = 10000,
Range = 50000,
Base + gleam@int:random(Range).
-file("src/dream_test/process.gleam", 255).
?DOC(
" Start a generic actor with custom state and message handler.\n"
"\n"
" The actor is linked to the test process and will be automatically\n"
" cleaned up when the test ends.\n"
"\n"
" ## Parameters\n"
"\n"
" - `initial_state` - The actor's starting state\n"
" - `handler` - Function `fn(state, message) -> actor.Next(state, message)`\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" pub type TodoMessage {\n"
" Add(String)\n"
" GetAll(Subject(List(String)))\n"
" }\n"
"\n"
" let todos = start_actor([], fn(items, msg) {\n"
" case msg {\n"
" Add(item) -> actor.continue([item, ..items])\n"
" GetAll(reply) -> {\n"
" process.send(reply, items)\n"
" actor.continue(items)\n"
" }\n"
" }\n"
" })\n"
"\n"
" process.send(todos, Add(\"Write tests\"))\n"
" process.send(todos, Add(\"Run tests\"))\n"
" let items = call_actor(todos, GetAll, 1000)\n"
" // items == [\"Run tests\", \"Write tests\"]\n"
" ```\n"
).
-spec start_actor(GGW, fun((GGW, GGX) -> gleam@otp@actor:next(GGW, GGX))) -> gleam@erlang@process:subject(GGX).
start_actor(Initial_state, Handler) ->
Started@1 = case begin
_pipe = gleam@otp@actor:new(Initial_state),
_pipe@1 = gleam@otp@actor:on_message(_pipe, Handler),
gleam@otp@actor:start(_pipe@1)
end of
{ok, Started} -> Started;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"dream_test/process"/utf8>>,
function => <<"start_actor"/utf8>>,
line => 259,
value => _assert_fail,
start => 6685,
'end' => 6790,
pattern_start => 6696,
pattern_end => 6707})
end,
erlang:element(3, Started@1).
-file("src/dream_test/process.gleam", 288).
?DOC(
" Call an actor and wait for a response.\n"
"\n"
" This is a convenience wrapper around `actor.call` that makes the parameter\n"
" order more ergonomic for piping.\n"
"\n"
" ## Parameters\n"
"\n"
" - `subject` - The actor to call\n"
" - `make_message` - Function that creates the message given a reply subject\n"
" - `timeout_ms` - How long to wait for a response\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" pub type Msg {\n"
" GetValue(Subject(Int))\n"
" }\n"
"\n"
" let value = call_actor(my_actor, GetValue, 1000)\n"
" ```\n"
).
-spec call_actor(
gleam@erlang@process:subject(GHB),
fun((gleam@erlang@process:subject(GHD)) -> GHB),
integer()
) -> GHD.
call_actor(Subject, Make_message, Timeout_ms) ->
gleam@otp@actor:call(Subject, Timeout_ms, Make_message).
-file("src/dream_test/process.gleam", 332).
?DOC(
" Default polling configuration.\n"
"\n"
" - 5 second timeout\n"
" - Check every 50ms\n"
"\n"
" Good for operations that might take a few seconds.\n"
).
-spec default_poll_config() -> poll_config().
default_poll_config() ->
{poll_config, 5000, 50}.
-file("src/dream_test/process.gleam", 343).
?DOC(
" Quick polling configuration.\n"
"\n"
" - 1 second timeout\n"
" - Check every 10ms\n"
"\n"
" Good for fast local operations like servers starting.\n"
).
-spec quick_poll_config() -> poll_config().
quick_poll_config() ->
{poll_config, 1000, 10}.
-file("src/dream_test/process.gleam", 458).
-spec poll_check_result(
integer(),
integer(),
fun(() -> {ok, GHS} | {error, any()})
) -> poll_result(GHS).
poll_check_result(Remaining_ms, Interval_ms, Check) ->
case Check() of
{ok, Value} ->
{ready, Value};
{error, _} ->
gleam_erlang_ffi:sleep(Interval_ms),
poll_until_ok(Remaining_ms - Interval_ms, Interval_ms, Check)
end.
-file("src/dream_test/process.gleam", 447).
-spec poll_until_ok(integer(), integer(), fun(() -> {ok, GHN} | {error, any()})) -> poll_result(GHN).
poll_until_ok(Remaining_ms, Interval_ms, Check) ->
case Remaining_ms =< 0 of
true ->
timed_out;
false ->
poll_check_result(Remaining_ms, Interval_ms, Check)
end.
-file("src/dream_test/process.gleam", 415).
?DOC(
" Wait until a function returns Ok.\n"
"\n"
" Polls the check function at regular intervals until it returns `Ok(value)`\n"
" or the timeout is reached. The value is returned in `Ready(value)`.\n"
"\n"
" ## Use Cases\n"
"\n"
" - Waiting for a record to appear in a database\n"
" - Waiting for an async job to complete\n"
" - Waiting for a resource to become available\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Wait for user to appear in database\n"
" case await_some(default_poll_config(), fn() { find_user(user_id) }) {\n"
" Ready(user) -> {\n"
" user.name\n"
" |> should()\n"
" |> equal(\"Alice\")\n"
" |> or_fail_with(\"User should be Alice\")\n"
" }\n"
" TimedOut -> fail_with(\"User never appeared in database\")\n"
" }\n"
" ```\n"
).
-spec await_some(poll_config(), fun(() -> {ok, GHG} | {error, any()})) -> poll_result(GHG).
await_some(Config, Check) ->
poll_until_ok(erlang:element(2, Config), erlang:element(3, Config), Check).
-file("src/dream_test/process.gleam", 433).
-spec poll_check_bool(integer(), integer(), fun(() -> boolean())) -> poll_result(boolean()).
poll_check_bool(Remaining_ms, Interval_ms, Check) ->
case Check() of
true ->
{ready, true};
false ->
gleam_erlang_ffi:sleep(Interval_ms),
poll_until_true(Remaining_ms - Interval_ms, Interval_ms, Check)
end.
-file("src/dream_test/process.gleam", 422).
-spec poll_until_true(integer(), integer(), fun(() -> boolean())) -> poll_result(boolean()).
poll_until_true(Remaining_ms, Interval_ms, Check) ->
case Remaining_ms =< 0 of
true ->
timed_out;
false ->
poll_check_bool(Remaining_ms, Interval_ms, Check)
end.
-file("src/dream_test/process.gleam", 385).
?DOC(
" Wait until a condition returns True.\n"
"\n"
" Polls the check function at regular intervals until it returns `True`\n"
" or the timeout is reached.\n"
"\n"
" ## Use Cases\n"
"\n"
" - Waiting for a server to start accepting connections\n"
" - Waiting for a file to appear\n"
" - Waiting for a service to become healthy\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Wait for server to be ready\n"
" case await_ready(quick_poll_config(), fn() { is_port_open(port) }) {\n"
" Ready(True) -> {\n"
" // Server is up, proceed with test\n"
" make_request(port)\n"
" |> should()\n"
" |> be_ok()\n"
" |> or_fail_with(\"Request should succeed\")\n"
" }\n"
" TimedOut -> fail_with(\"Server didn't start in time\")\n"
" }\n"
" ```\n"
).
-spec await_ready(poll_config(), fun(() -> boolean())) -> poll_result(boolean()).
await_ready(Config, Check) ->
poll_until_true(erlang:element(2, Config), erlang:element(3, Config), Check).