Current section
Files
Jump to
Current section
Files
src/dream_test@discover.erl
-module(dream_test@discover).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream_test/discover.gleam").
-export([new/0, from_path/2, tests/1, list_modules/1, load/1, to_suites/1, to_suite/2]).
-export_type([test_discovery/0, load_result/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.
?MODULEDOC(
" Test module discovery for Dream Test.\n"
"\n"
" This module provides an ergonomic way to discover test modules at runtime\n"
" (compiled `.beam` modules) and load their `tests/0` suites without having to\n"
" manually import every module.\n"
"\n"
" ## Mental model\n"
"\n"
" - You provide one or more **module path globs** (e.g. `\"unit/**_test.gleam\"`).\n"
" - Dream Test finds matching modules under `./test/` that export `tests/0`.\n"
" - It calls `tests/0` to get `TestSuite(Nil)` values.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/discover.{from_path, to_suites}\n"
" import dream_test/reporters/bdd\n"
" import dream_test/reporters/progress\n"
" import dream_test/runner.{exit_on_failure, progress_reporter, results_reporters, run}\n"
"\n"
" pub fn main() {\n"
" let suites =\n"
" discover.new()\n"
" |> from_path(\"snippets/unit/**.gleam\")\n"
" |> to_suites()\n"
"\n"
" runner.new(suites)\n"
" |> progress_reporter(progress.new())\n"
" |> results_reporters([bdd.new()])\n"
" |> exit_on_failure()\n"
" |> run()\n"
" }\n"
" ```\n"
"\n"
" <sub>Note: discovery requires compiled BEAM modules.</sub>\n"
).
-opaque test_discovery() :: {test_discovery, list(binary())}.
-type load_result() :: {load_result,
list(dream_test@types:root(nil)),
list(binary())}.
-file("src/dream_test/discover.gleam", 122).
?DOC(
" Create an empty discovery builder.\n"
"\n"
" Most users will start with `tests(pattern)` instead.\n"
"\n"
" ## Returns\n"
"\n"
" A new empty `TestDiscovery`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/discover.{from_path, to_suites}\n"
" import dream_test/reporters/bdd\n"
" import dream_test/reporters/progress\n"
" import dream_test/runner.{exit_on_failure, progress_reporter, results_reporters, run}\n"
" import gleam/io\n"
"\n"
" pub fn main() {\n"
" let suites =\n"
" discover.new()\n"
" |> from_path(\"snippets/unit/**.gleam\")\n"
" |> to_suites()\n"
"\n"
" runner.new(suites)\n"
" |> progress_reporter(progress.new())\n"
" |> results_reporters([bdd.new()])\n"
" |> exit_on_failure()\n"
" |> run()\n"
" }\n"
" ```\n"
).
-spec new() -> test_discovery().
new() ->
{test_discovery, []}.
-file("src/dream_test/discover.gleam", 161).
?DOC(
" Add a glob pattern to the discovery set.\n"
"\n"
" You can call this multiple times to build up a list of globs.\n"
"\n"
" ## Parameters\n"
"\n"
" - `discovery`: The current discovery builder\n"
" - `pattern`: A slash-separated module path glob (the `.gleam` extension is optional)\n"
"\n"
" ## Returns\n"
"\n"
" A new `TestDiscovery` with the pattern appended.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/discover.{from_path, to_suites}\n"
" import dream_test/reporters/bdd\n"
" import dream_test/reporters/progress\n"
" import dream_test/runner.{exit_on_failure, progress_reporter, results_reporters, run}\n"
" import gleam/io\n"
"\n"
" pub fn main() {\n"
" let suites =\n"
" discover.new()\n"
" |> from_path(\"snippets/unit/**.gleam\")\n"
" |> to_suites()\n"
"\n"
" runner.new(suites)\n"
" |> progress_reporter(progress.new())\n"
" |> results_reporters([bdd.new()])\n"
" |> exit_on_failure()\n"
" |> run()\n"
" }\n"
" ```\n"
).
-spec from_path(test_discovery(), binary()) -> test_discovery().
from_path(Discovery, Pattern) ->
{test_discovery, lists:append(erlang:element(2, Discovery), [Pattern])}.
-file("src/dream_test/discover.gleam", 207).
?DOC(
" Start discovering tests matching a module path glob pattern.\n"
"\n"
" The pattern is written using slash-separated module paths and may include\n"
" `*` / `**` globs. The `.gleam` extension is optional.\n"
"\n"
" Examples:\n"
" - `\"unit/**_test.gleam\"`\n"
" - `\"unit/errors/**_test.gleam\"`\n"
" - `\"dream_test/**_test.gleam\"`\n"
"\n"
" ## Parameters\n"
"\n"
" - `pattern`: A slash-separated module path glob (the `.gleam` extension is optional)\n"
"\n"
" ## Returns\n"
"\n"
" A new `TestDiscovery` initialized with the pattern.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/discover\n"
" import dream_test/reporters/bdd\n"
" import dream_test/reporters/progress\n"
" import dream_test/runner.{exit_on_failure, progress_reporter, results_reporters, run}\n"
" import gleam/io\n"
"\n"
" pub fn main() {\n"
" let suites =\n"
" discover.tests(\"snippets/unit/**.gleam\")\n"
" |> discover.to_suites()\n"
"\n"
" runner.new(suites)\n"
" |> progress_reporter(progress.new())\n"
" |> results_reporters([bdd.new()])\n"
" |> exit_on_failure()\n"
" |> run()\n"
" }\n"
" ```\n"
).
-spec tests(binary()) -> test_discovery().
tests(Pattern) ->
_pipe = new(),
from_path(_pipe, Pattern).
-file("src/dream_test/discover.gleam", 452).
-spec format_discover_error(binary(), binary()) -> binary().
format_discover_error(Pattern, Message) ->
<<<<Pattern/binary, ": "/utf8>>/binary, Message/binary>>.
-file("src/dream_test/discover.gleam", 507).
-spec add_unique_modules_next(
binary(),
list(binary()),
list(binary()),
list(binary())
) -> {list(binary()), list(binary())}.
add_unique_modules_next(Module_name, Rest, Seen, Acc_rev) ->
case gleam@list:contains(Seen, Module_name) of
true ->
add_unique_modules(Rest, Seen, Acc_rev);
false ->
add_unique_modules(
Rest,
[Module_name | Seen],
[Module_name | Acc_rev]
)
end.
-file("src/dream_test/discover.gleam", 496).
-spec add_unique_modules(list(binary()), list(binary()), list(binary())) -> {list(binary()),
list(binary())}.
add_unique_modules(Modules, Seen, Acc_rev) ->
case Modules of
[] ->
{Seen, Acc_rev};
[M | Rest] ->
add_unique_modules_next(M, Rest, Seen, Acc_rev)
end.
-file("src/dream_test/discover.gleam", 403).
-spec ensure_beam_extension(binary()) -> binary().
ensure_beam_extension(Path) ->
case gleam_stdlib:string_ends_with(Path, <<".beam"/utf8>>) of
true ->
Path;
false ->
<<Path/binary, ".beam"/utf8>>
end.
-file("src/dream_test/discover.gleam", 399).
-spec replace_gleam_extension(binary()) -> binary().
replace_gleam_extension(Path) ->
_pipe = Path,
gleam@string:replace(_pipe, <<".gleam"/utf8>>, <<".beam"/utf8>>).
-file("src/dream_test/discover.gleam", 384).
-spec to_beam_glob(binary()) -> binary().
to_beam_glob(Pattern) ->
Normalized = begin
_pipe = Pattern,
_pipe@1 = gleam@string:trim(_pipe),
gleam@string:replace(_pipe@1, <<"/"/utf8>>, <<"@"/utf8>>)
end,
Flattened = begin
_pipe@2 = Normalized,
gleam@string:replace(_pipe@2, <<"**"/utf8>>, <<"*"/utf8>>)
end,
case gleam_stdlib:string_ends_with(Flattened, <<".gleam"/utf8>>) of
true ->
replace_gleam_extension(Flattened);
false ->
ensure_beam_extension(Flattened)
end.
-file("src/dream_test/discover.gleam", 475).
-spec discover_all_modules_loop_next(
binary(),
list(binary()),
list(binary()),
list(binary()),
list(binary())
) -> {list(binary()), list(binary())}.
discover_all_modules_loop_next(Pattern, Rest, Seen, Acc_rev, Errors_rev) ->
Beam_glob = to_beam_glob(Pattern),
case dream_test_test_discovery_ffi:discover_test_modules(Beam_glob) of
{ok, Mods} ->
{Seen2, Acc2} = add_unique_modules(Mods, Seen, Acc_rev),
discover_all_modules_loop(Rest, Seen2, Acc2, Errors_rev);
{error, Message} ->
discover_all_modules_loop(
Rest,
Seen,
Acc_rev,
[format_discover_error(Pattern, Message) | Errors_rev]
)
end.
-file("src/dream_test/discover.gleam", 462).
-spec discover_all_modules_loop(
list(binary()),
list(binary()),
list(binary()),
list(binary())
) -> {list(binary()), list(binary())}.
discover_all_modules_loop(Patterns, Seen, Acc_rev, Errors_rev) ->
case Patterns of
[] ->
{lists:reverse(Acc_rev), lists:reverse(Errors_rev)};
[Pattern | Rest] ->
discover_all_modules_loop_next(
Pattern,
Rest,
Seen,
Acc_rev,
Errors_rev
)
end.
-file("src/dream_test/discover.gleam", 456).
-spec discover_all_modules(list(binary())) -> {list(binary()), list(binary())}.
discover_all_modules(Patterns) ->
discover_all_modules_loop(Patterns, [], [], []).
-file("src/dream_test/discover.gleam", 235).
?DOC(
" List module names discovered for the configured pattern.\n"
"\n"
" This returns the discovered module names (as strings) or an aggregated error\n"
" message if discovery failed.\n"
"\n"
" ## Parameters\n"
"\n"
" - `discovery`: The configured discovery builder\n"
"\n"
" ## Returns\n"
"\n"
" - `Ok(modules)`: A list of discovered module names\n"
" - `Error(message)`: A human-readable error message (may contain multiple causes)\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/discover\n"
"\n"
" pub fn main() {\n"
" discover.tests(\"snippets/unit/**.gleam\")\n"
" |> discover.list_modules()\n"
" }\n"
" ```\n"
).
-spec list_modules(test_discovery()) -> {ok, list(binary())} | {error, binary()}.
list_modules(Discovery) ->
{Modules, Errors} = discover_all_modules(erlang:element(2, Discovery)),
case Errors of
[] ->
{ok, Modules};
_ ->
{error, gleam@string:join(Errors, <<"; "/utf8>>)}
end.
-file("src/dream_test/discover.gleam", 448).
-spec format_load_error(binary(), binary()) -> binary().
format_load_error(Module_name, Message) ->
<<<<Module_name/binary, ": "/utf8>>/binary, Message/binary>>.
-file("src/dream_test/discover.gleam", 582).
-spec children_with_source(
list(dream_test@types:node_(nil)),
binary(),
list(dream_test@types:node_(nil))
) -> list(dream_test@types:node_(nil)).
children_with_source(Children, Source, Acc_rev) ->
case Children of
[] ->
lists:reverse(Acc_rev);
[Child | Rest] ->
children_with_source(
Rest,
Source,
[node_with_source(Child, Source) | Acc_rev]
)
end.
-file("src/dream_test/discover.gleam", 561).
-spec node_with_source(dream_test@types:node_(nil), binary()) -> dream_test@types:node_(nil).
node_with_source(Node, Source) ->
case Node of
{group, Name, Tags, Children} ->
{group, Name, Tags, children_with_source(Children, Source, [])};
{test, Name@1, Tags@1, Kind, Run, Timeout_ms, _} ->
{test, Name@1, Tags@1, Kind, Run, Timeout_ms, {some, Source}};
Other ->
Other
end.
-file("src/dream_test/discover.gleam", 556).
-spec suite_with_source(dream_test@types:root(nil), binary()) -> dream_test@types:root(nil).
suite_with_source(Suite, Source) ->
{root, Seed, Tree} = Suite,
{root, Seed, node_with_source(Tree, Source)}.
-file("src/dream_test/discover.gleam", 427).
-spec load_suites_from_modules_next(
binary(),
list(binary()),
list(dream_test@types:root(nil)),
list(binary())
) -> load_result().
load_suites_from_modules_next(Module_name, Rest, Suites_rev, Errors_rev) ->
case dream_test_test_discovery_ffi:call_tests(Module_name) of
{ok, Suite} ->
load_suites_from_modules(
Rest,
[suite_with_source(Suite, Module_name) | Suites_rev],
Errors_rev
);
{error, Message} ->
load_suites_from_modules(
Rest,
Suites_rev,
[format_load_error(Module_name, Message) | Errors_rev]
)
end.
-file("src/dream_test/discover.gleam", 410).
-spec load_suites_from_modules(
list(binary()),
list(dream_test@types:root(nil)),
list(binary())
) -> load_result().
load_suites_from_modules(Module_names, Suites_rev, Errors_rev) ->
case Module_names of
[] ->
{load_result, lists:reverse(Suites_rev), lists:reverse(Errors_rev)};
[Module_name | Rest] ->
load_suites_from_modules_next(
Module_name,
Rest,
Suites_rev,
Errors_rev
)
end.
-file("src/dream_test/discover.gleam", 269).
?DOC(
" Load discovered suites and return both suites and errors.\n"
"\n"
" This never panics; discovery errors are returned in `LoadResult.errors`.\n"
"\n"
" ## Parameters\n"
"\n"
" - `discovery`: The configured discovery builder\n"
"\n"
" ## Returns\n"
"\n"
" A `LoadResult` with:\n"
" - `suites`: successfully loaded `TestSuite(Nil)` values\n"
" - `errors`: discovery or load errors (as strings)\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/discover\n"
"\n"
" pub fn main() {\n"
" discover.tests(\"snippets/unit/**.gleam\")\n"
" |> discover.load()\n"
" }\n"
" ```\n"
).
-spec load(test_discovery()) -> load_result().
load(Discovery) ->
{Module_names, Discover_errors} = discover_all_modules(
erlang:element(2, Discovery)
),
{load_result, Suites, Load_errors} = load_suites_from_modules(
Module_names,
[],
[]
),
{load_result, Suites, lists:append(Discover_errors, Load_errors)}.
-file("src/dream_test/discover.gleam", 612).
-spec discovery_error_assertion() -> dream_test@types:assertion_result().
discovery_error_assertion() ->
{assertion_failed,
{assertion_failure,
<<"discover"/utf8>>,
<<"Failed to discover/load test modules (see test name for details)"/utf8>>,
none}}.
-file("src/dream_test/discover.gleam", 608).
-spec discovery_error_run(nil) -> {ok, dream_test@types:assertion_result()} |
{error, binary()}.
discovery_error_run(_) ->
{ok, discovery_error_assertion()}.
-file("src/dream_test/discover.gleam", 597).
-spec error_to_node(binary()) -> dream_test@types:node_(nil).
error_to_node(Error) ->
{test,
<<"Discovery Error: "/utf8, Error/binary>>,
[<<"discovery-error"/utf8>>],
unit,
fun discovery_error_run/1,
none,
none}.
-file("src/dream_test/discover.gleam", 530).
-spec errors_to_nodes(list(binary()), list(dream_test@types:node_(nil))) -> list(dream_test@types:node_(nil)).
errors_to_nodes(Errors, Acc_rev) ->
case Errors of
[] ->
lists:reverse(Acc_rev);
[Error | Rest] ->
errors_to_nodes(Rest, [error_to_node(Error) | Acc_rev])
end.
-file("src/dream_test/discover.gleam", 540).
-spec errors_suite(list(binary())) -> dream_test@types:root(nil).
errors_suite(Errors) ->
{root,
nil,
{group,
<<"Discovery Errors"/utf8>>,
[<<"discovery-error"/utf8>>],
errors_to_nodes(Errors, [])}}.
-file("src/dream_test/discover.gleam", 314).
?DOC(
" Load discovered suites and return them as a list.\n"
"\n"
" Any discovery/load errors are converted into failing unit tests tagged with\n"
" `\"discovery-error\"`, so missing coverage is visible.\n"
"\n"
" ## Parameters\n"
"\n"
" - `discovery`: The configured discovery builder\n"
"\n"
" ## Returns\n"
"\n"
" A list of suites. If any errors occurred, an additional failing suite tagged\n"
" `\"discovery-error\"` is appended.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/discover.{from_path, to_suites}\n"
" import dream_test/reporters/bdd\n"
" import dream_test/reporters/progress\n"
" import dream_test/runner.{exit_on_failure, progress_reporter, results_reporters, run}\n"
" import gleam/io\n"
"\n"
" pub fn main() {\n"
" let suites =\n"
" discover.new()\n"
" |> from_path(\"snippets/unit/**.gleam\")\n"
" |> to_suites()\n"
"\n"
" runner.new(suites)\n"
" |> progress_reporter(progress.new())\n"
" |> results_reporters([bdd.new()])\n"
" |> exit_on_failure()\n"
" |> run()\n"
" }\n"
" ```\n"
).
-spec to_suites(test_discovery()) -> list(dream_test@types:root(nil)).
to_suites(Discovery) ->
{load_result, Suites, Errors} = load(Discovery),
case gleam@list:is_empty(Errors) of
true ->
Suites;
false ->
lists:append(Suites, [errors_suite(Errors)])
end.
-file("src/dream_test/discover.gleam", 551).
-spec root_to_group(dream_test@types:root(nil)) -> dream_test@types:node_(nil).
root_to_group(Suite) ->
{root, _, Tree} = Suite,
Tree.
-file("src/dream_test/discover.gleam", 520).
-spec suites_to_nodes(
list(dream_test@types:root(nil)),
list(dream_test@types:node_(nil))
) -> list(dream_test@types:node_(nil)).
suites_to_nodes(Suites, Acc_rev) ->
case Suites of
[] ->
lists:reverse(Acc_rev);
[Suite | Rest] ->
suites_to_nodes(Rest, [root_to_group(Suite) | Acc_rev])
end.
-file("src/dream_test/discover.gleam", 360).
?DOC(
" Build a single suite from discovered suites.\n"
"\n"
" Any discovery/load errors are converted into failing unit tests tagged with\n"
" `\"discovery-error\"`.\n"
"\n"
" ## Parameters\n"
"\n"
" - `discovery`: The configured discovery builder\n"
" - `suite_name`: Name to use for the outer group in the combined suite\n"
"\n"
" ## Returns\n"
"\n"
" A single `TestSuite(Nil)` containing:\n"
" - all discovered suites, and\n"
" - any errors as failing tests tagged `\"discovery-error\"`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/discover\n"
" import dream_test/reporters/bdd\n"
" import dream_test/reporters/progress\n"
" import dream_test/runner.{exit_on_failure, progress_reporter, results_reporters, run}\n"
" import gleam/io\n"
"\n"
" pub fn main() {\n"
" let suite =\n"
" discover.tests(\"snippets/unit/**.gleam\")\n"
" |> discover.to_suite(\"discovered tests\")\n"
"\n"
" runner.new([suite])\n"
" |> progress_reporter(progress.new())\n"
" |> results_reporters([bdd.new()])\n"
" |> exit_on_failure()\n"
" |> run()\n"
" }\n"
" ```\n"
).
-spec to_suite(test_discovery(), binary()) -> dream_test@types:root(nil).
to_suite(Discovery, Suite_name) ->
{load_result, Suites, Errors} = load(Discovery),
Suite_nodes = suites_to_nodes(Suites, []),
Error_nodes = errors_to_nodes(Errors, []),
{root, nil, {group, Suite_name, [], lists:append(Suite_nodes, Error_nodes)}}.