-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" " Note: discovery requires compiled BEAM modules.\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", 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", 403). -spec ensure_beam_extension(binary()) -> binary(). ensure_beam_extension(Path) -> case gleam_stdlib:string_ends_with(Path, <<".beam"/utf8>>) of true -> Path; false -> <> end. -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", 444). -spec format_load_error(binary(), binary()) -> binary(). format_load_error(Module_name, Message) -> <<<>/binary, Message/binary>>. -file("src/dream_test/discover.gleam", 448). -spec format_discover_error(binary(), binary()) -> binary(). format_discover_error(Pattern, Message) -> <<<>/binary, Message/binary>>. -file("src/dream_test/discover.gleam", 545). -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", 514). -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", 564). -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", 560). -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", 550). -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}. -file("src/dream_test/discover.gleam", 524). -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", 534). -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", 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 | 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", 469). -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", 456). -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", 452). -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", 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", 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", 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)}}. -file("src/dream_test/discover.gleam", 501). -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", 490). -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.