Current section

Files

Jump to
dream_test src dream_test@process.erl
Raw

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 Dream Test runs a test, it runs in an isolated BEAM process. Any\n"
" processes you spawn inside a test can be *linked* to that test process, so\n"
" they automatically die when the test ends (pass, fail, timeout, crash).\n"
"\n"
" This module gives you a few “batteries included” patterns:\n"
"\n"
" - A simple counter actor (`start_counter`) you can use to test stateful code.\n"
" - A generic actor starter (`start_actor`) + a pipe-friendly call helper (`call_actor`).\n"
" - “wait until ready” polling (`await_ready` / `await_some`) for async systems.\n"
" - A safe-ish random port helper (`unique_port`) for test servers.\n"
"\n"
" ## Example\n"
" Use this inside an `it` block.\n"
"\n"
" ```gleam\n"
" let counter = process.start_counter()\n"
" process.increment(counter)\n"
" process.increment(counter)\n"
"\n"
" process.get_count(counter)\n"
" |> should\n"
" |> be_equal(2)\n"
" |> or_fail_with(\"expected counter to be 2\")\n"
" ```\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(JER) :: {ready, JER} | timed_out.
-file("src/dream_test/process.gleam", 119).
-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", 110).
?DOC(
" Start a counter actor with a specific initial value.\n"
"\n"
" ## Parameters\n"
"\n"
" - `initial`: The initial count value.\n"
"\n"
" ## Returns\n"
"\n"
" A `Subject(CounterMessage)` for the started counter.\n"
"\n"
" ## Example\n"
" Use this inside an `it` block.\n"
"\n"
" ```gleam\n"
" let counter = process.start_counter_with(10)\n"
" process.decrement(counter)\n"
"\n"
" process.get_count(counter)\n"
" |> should\n"
" |> be_equal(9)\n"
" |> or_fail_with(\"expected counter to be 9 after decrement\")\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 => 111,
value => _assert_fail,
start => 3029,
'end' => 3143,
pattern_start => 3040,
pattern_end => 3051})
end,
erlang:element(3, Started@1).
-file("src/dream_test/process.gleam", 83).
?DOC(
" Start a counter actor initialized to 0.\n"
"\n"
" The counter is linked to the test process and will be automatically cleaned\n"
" up when the test ends.\n"
"\n"
" ## Returns\n"
"\n"
" A `Subject(CounterMessage)` you can pass to the other counter helpers (or\n"
" send messages to directly).\n"
"\n"
" ## Example\n"
" Use this inside an `it` block.\n"
"\n"
" ```gleam\n"
" let counter = process.start_counter()\n"
" process.increment(counter)\n"
" process.increment(counter)\n"
"\n"
" process.get_count(counter)\n"
" |> should\n"
" |> be_equal(2)\n"
" |> or_fail_with(\"expected counter to be 2\")\n"
" ```\n"
).
-spec start_counter() -> gleam@erlang@process:subject(counter_message()).
start_counter() ->
start_counter_with(0).
-file("src/dream_test/process.gleam", 160).
?DOC(
" Get the current value from a counter.\n"
"\n"
" This is a synchronous call that blocks until the counter responds.\n"
"\n"
" ## Parameters\n"
"\n"
" - `counter`: The counter actor to query.\n"
"\n"
" ## Returns\n"
"\n"
" The current counter value.\n"
"\n"
" ## Example\n"
"\n"
" Use this inside an `it` block.\n"
"\n"
" ```gleam\n"
" let counter = process.start_counter()\n"
" process.increment(counter)\n"
"\n"
" process.get_count(counter)\n"
" |> should\n"
" |> be_equal(1)\n"
" |> or_fail_with(\"expected counter to be 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", 191).
?DOC(
" Increment a counter by 1.\n"
"\n"
" This is an asynchronous send—it returns immediately.\n"
"\n"
" ## Parameters\n"
"\n"
" - `counter`: The counter actor to increment.\n"
"\n"
" ## Returns\n"
"\n"
" `Nil`. (The message is sent asynchronously.)\n"
"\n"
" ## Example\n"
"\n"
" Use this inside an `it` block.\n"
"\n"
" ```gleam\n"
" let counter = process.start_counter()\n"
" process.increment(counter)\n"
" process.increment(counter)\n"
"\n"
" process.get_count(counter)\n"
" |> should\n"
" |> be_equal(2)\n"
" |> or_fail_with(\"expected counter to be 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", 221).
?DOC(
" Decrement a counter by 1.\n"
"\n"
" This is an asynchronous send—it returns immediately.\n"
"\n"
" ## Parameters\n"
"\n"
" - `counter`: The counter actor to decrement.\n"
"\n"
" ## Returns\n"
"\n"
" `Nil`. (The message is sent asynchronously.)\n"
"\n"
" ## Example\n"
"\n"
" Use this inside an `it` block.\n"
"\n"
" ```gleam\n"
" let counter = process.start_counter_with(10)\n"
" process.decrement(counter)\n"
"\n"
" process.get_count(counter)\n"
" |> should\n"
" |> be_equal(9)\n"
" |> or_fail_with(\"expected counter to be 9 after decrement\")\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", 252).
?DOC(
" Set a counter to a specific value.\n"
"\n"
" This is an asynchronous send—it returns immediately.\n"
"\n"
" ## Parameters\n"
"\n"
" - `counter`: The counter actor to set.\n"
" - `value`: The new value to set.\n"
"\n"
" ## Returns\n"
"\n"
" `Nil`. (The message is sent asynchronously.)\n"
"\n"
" ## Example\n"
"\n"
" Use this inside an `it` block.\n"
"\n"
" ```gleam\n"
" let counter = process.start_counter()\n"
" process.set_count(counter, 42)\n"
"\n"
" process.get_count(counter)\n"
" |> should\n"
" |> be_equal(42)\n"
" |> or_fail_with(\"expected counter to be 42 after set_count\")\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", 304).
?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"
" Use this in test setup, before starting a server.\n"
"\n"
" ```gleam\n"
" process.unique_port()\n"
" |> should\n"
" |> be_between(10_000, 60_000)\n"
" |> or_fail_with(\"expected unique_port to be within 10k..60k\")\n"
" ```\n"
).
-spec unique_port() -> integer().
unique_port() ->
Base = 10000,
Range = 50000,
Base + gleam@int:random(Range).
-file("src/dream_test/process.gleam", 338).
?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"
" ## Returns\n"
"\n"
" A `Subject(msg)` you can send messages to (or call with `call_actor`).\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let todos = process.start_actor([], handle_todo_message)\n"
"\n"
" erlang_process.send(todos, Add(\"Write tests\"))\n"
" erlang_process.send(todos, Add(\"Run tests\"))\n"
"\n"
" process.call_actor(todos, GetAll, 1000)\n"
" |> should\n"
" |> be_equal([\"Write tests\", \"Run tests\"])\n"
" |> or_fail_with(\"expected items to be preserved in insertion order\")\n"
" ```\n"
).
-spec start_actor(JFA, fun((JFA, JFB) -> gleam@otp@actor:next(JFA, JFB))) -> gleam@erlang@process:subject(JFB).
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 => 342,
value => _assert_fail,
start => 8471,
'end' => 8576,
pattern_start => 8482,
pattern_end => 8493})
end,
erlang:element(3, Started@1).
-file("src/dream_test/process.gleam", 374).
?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"
" ## Returns\n"
"\n"
" The reply value from the actor.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" process.call_actor(todos, GetAll, 1000)\n"
" |> should\n"
" |> be_equal([\"Write tests\", \"Run tests\"])\n"
" |> or_fail_with(\"expected items to be preserved in insertion order\")\n"
" ```\n"
).
-spec call_actor(
gleam@erlang@process:subject(JFF),
fun((gleam@erlang@process:subject(JFH)) -> JFF),
integer()
) -> JFH.
call_actor(Subject, Make_message, Timeout_ms) ->
gleam@otp@actor:call(Subject, Timeout_ms, Make_message).
-file("src/dream_test/process.gleam", 433).
?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"
"\n"
" ## Returns\n"
"\n"
" `PollConfig(timeout_ms: 5000, interval_ms: 50)`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" process.default_poll_config()\n"
" |> should\n"
" |> be_equal(process.PollConfig(timeout_ms: 5000, interval_ms: 50))\n"
" |> or_fail_with(\"expected default_poll_config to be 5000ms/50ms\")\n"
" ```\n"
).
-spec default_poll_config() -> poll_config().
default_poll_config() ->
{poll_config, 5000, 50}.
-file("src/dream_test/process.gleam", 457).
?DOC(
" Quick polling configuration.\n"
"\n"
" - 1 second timeout\n"
" - Check every 10ms\n"
"\n"
" Good for fast local operations like servers starting.\n"
"\n"
" ## Returns\n"
"\n"
" `PollConfig(timeout_ms: 1000, interval_ms: 10)`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" process.quick_poll_config()\n"
" |> should\n"
" |> be_equal(process.PollConfig(timeout_ms: 1000, interval_ms: 10))\n"
" |> or_fail_with(\"expected quick_poll_config to be 1000ms/10ms\")\n"
" ```\n"
).
-spec quick_poll_config() -> poll_config().
quick_poll_config() ->
{poll_config, 1000, 10}.
-file("src/dream_test/process.gleam", 585).
-spec poll_check_result(
integer(),
integer(),
fun(() -> {ok, JFW} | {error, any()})
) -> poll_result(JFW).
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", 574).
-spec poll_until_ok(integer(), integer(), fun(() -> {ok, JFR} | {error, any()})) -> poll_result(JFR).
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", 542).
?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"
" ## Parameters\n"
"\n"
" - `config`: Poll timeout + interval.\n"
" - `check`: A zero-arg function returning `Result(value, error)`.\n"
"\n"
" ## Returns\n"
"\n"
" `Ready(value)` when the check returns `Ok(value)`, otherwise `TimedOut`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" process.await_some(process.default_poll_config(), always_ok_42)\n"
" |> should\n"
" |> be_equal(process.Ready(42))\n"
" |> or_fail_with(\"expected await_some to return Ready(42)\")\n"
" ```\n"
).
-spec await_some(poll_config(), fun(() -> {ok, JFK} | {error, any()})) -> poll_result(JFK).
await_some(Config, Check) ->
poll_until_ok(erlang:element(2, Config), erlang:element(3, Config), Check).
-file("src/dream_test/process.gleam", 560).
-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", 549).
-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", 506).
?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"
" ## Parameters\n"
"\n"
" - `config`: Poll timeout + interval.\n"
" - `check`: A zero-arg function returning `Bool`.\n"
"\n"
" ## Returns\n"
"\n"
" `Ready(True)` when the check returns `True`, otherwise `TimedOut`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" process.await_ready(process.quick_poll_config(), always_true)\n"
" |> should\n"
" |> be_equal(process.Ready(True))\n"
" |> or_fail_with(\"expected await_ready to return Ready(True)\")\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).