Current section
Files
Jump to
Current section
Files
src/dream_test@unit.erl
-module(dream_test@unit).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream_test/unit.gleam").
-export([it/2, skip/2, describe/2, before_all/1, before_each/1, after_each/1, after_all/1, to_test_suite/2, to_test_cases/2]).
-export_type([unit_test/0, hook_context/0, suite_hooks/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(
" Unit test DSL for dream_test.\n"
"\n"
" This module provides a BDD-style syntax for defining tests: `describe`,\n"
" `it`, and lifecycle hooks (`before_all`, `before_each`, `after_each`,\n"
" `after_all`). Tests are organized hierarchically and converted to\n"
" runnable test cases or suites.\n"
"\n"
" ## Quick Start\n"
"\n"
" ```gleam\n"
" import dream_test/unit.{describe, it, to_test_cases}\n"
" import dream_test/assertions/should.{should, equal, or_fail_with}\n"
" import dream_test/runner.{run_all}\n"
" import dream_test/reporter/bdd.{report}\n"
" import gleam/io\n"
"\n"
" pub fn main() {\n"
" tests()\n"
" |> to_test_cases(\"my_module_test\")\n"
" |> run_all()\n"
" |> report(io.print)\n"
" }\n"
"\n"
" pub fn tests() {\n"
" describe(\"Calculator\", [\n"
" describe(\"add\", [\n"
" it(\"adds positive numbers\", fn() {\n"
" add(2, 3)\n"
" |> should()\n"
" |> equal(5)\n"
" |> or_fail_with(\"2 + 3 should equal 5\")\n"
" }),\n"
" it(\"handles zero\", fn() {\n"
" add(0, 5)\n"
" |> should()\n"
" |> equal(5)\n"
" |> or_fail_with(\"0 + 5 should equal 5\")\n"
" }),\n"
" ]),\n"
" ])\n"
" }\n"
" ```\n"
"\n"
" ## Output\n"
"\n"
" ```text\n"
" Calculator\n"
" add\n"
" ✓ adds positive numbers\n"
" ✓ handles zero\n"
"\n"
" Summary: 2 run, 0 failed, 2 passed\n"
" ```\n"
"\n"
" ## Lifecycle Hooks\n"
"\n"
" Setup and teardown logic for tests:\n"
"\n"
" ```gleam\n"
" import dream_test/unit.{describe, it, before_each, after_each, to_test_cases}\n"
" import dream_test/types.{AssertionOk}\n"
"\n"
" describe(\"Database\", [\n"
" before_each(fn() {\n"
" reset_database()\n"
" AssertionOk\n"
" }),\n"
"\n"
" it(\"creates users\", fn() { ... }),\n"
" it(\"queries users\", fn() { ... }),\n"
"\n"
" after_each(fn() {\n"
" rollback()\n"
" AssertionOk\n"
" }),\n"
" ])\n"
" ```\n"
"\n"
" | Hook | Runs | Requires |\n"
" |---------------|-----------------------------------|-------------------|\n"
" | `before_all` | Once before all tests in group | `to_test_suite` |\n"
" | `before_each` | Before each test | Either mode |\n"
" | `after_each` | After each test (always) | Either mode |\n"
" | `after_all` | Once after all tests in group | `to_test_suite` |\n"
"\n"
" ## Two Execution Modes\n"
"\n"
" **Flat mode** — faster, simpler, no `before_all`/`after_all`:\n"
"\n"
" ```gleam\n"
" tests() |> to_test_cases(\"my_test\") |> run_all()\n"
" ```\n"
"\n"
" **Suite mode** — supports all hooks, preserves group structure:\n"
"\n"
" ```gleam\n"
" tests() |> to_test_suite(\"my_test\") |> run_suite()\n"
" ```\n"
"\n"
" ## Nesting\n"
"\n"
" You can nest `describe` blocks as deeply as needed. Each level adds to\n"
" the test's `full_name`, which the reporter uses for grouping output.\n"
" Lifecycle hooks are inherited by nested groups.\n"
"\n"
" ```gleam\n"
" describe(\"User\", [\n"
" before_each(fn() { create_user(); AssertionOk }),\n"
"\n"
" describe(\"authentication\", [\n"
" describe(\"with valid credentials\", [\n"
" it(\"returns the user\", fn() { ... }),\n"
" it(\"sets the session\", fn() { ... }),\n"
" ]),\n"
" describe(\"with invalid credentials\", [\n"
" it(\"returns an error\", fn() { ... }),\n"
" ]),\n"
" ]),\n"
" ])\n"
" ```\n"
).
-type unit_test() :: {it_test,
binary(),
fun(() -> dream_test@types:assertion_result())} |
{describe_group, binary(), list(unit_test())} |
{before_all, fun(() -> dream_test@types:assertion_result())} |
{before_each, fun(() -> dream_test@types:assertion_result())} |
{after_each, fun(() -> dream_test@types:assertion_result())} |
{after_all, fun(() -> dream_test@types:assertion_result())}.
-type hook_context() :: {hook_context,
list(fun(() -> dream_test@types:assertion_result())),
list(fun(() -> dream_test@types:assertion_result()))}.
-type suite_hooks() :: {suite_hooks,
list(fun(() -> dream_test@types:assertion_result())),
list(fun(() -> dream_test@types:assertion_result()))}.
-file("src/dream_test/unit.gleam", 178).
?DOC(
" Define a single test case.\n"
"\n"
" The test body is a function that returns an `AssertionResult`. Use the\n"
" `should` API to build assertions that produce this result.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" it(\"calculates the sum correctly\", fn() {\n"
" add(2, 3)\n"
" |> should()\n"
" |> equal(5)\n"
" |> or_fail_with(\"Expected 2 + 3 to equal 5\")\n"
" })\n"
" ```\n"
"\n"
" ## Naming Conventions\n"
"\n"
" Good test names describe the expected behavior:\n"
" - ✓ \"returns the user when credentials are valid\"\n"
" - ✓ \"rejects empty passwords\"\n"
" - ✗ \"test1\"\n"
" - ✗ \"works\"\n"
).
-spec it(binary(), fun(() -> dream_test@types:assertion_result())) -> unit_test().
it(Name, Run) ->
{it_test, Name, Run}.
-file("src/dream_test/unit.gleam", 224).
?DOC(
" Skip a test case.\n"
"\n"
" Use `skip` to temporarily disable a test without removing it. The test\n"
" will appear in reports with a `-` marker and won't affect the pass/fail\n"
" outcome.\n"
"\n"
" This is designed to be a drop-in replacement for `it` — just change `it`\n"
" to `skip` to disable a test, and change it back when ready to run again.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" describe(\"Feature\", [\n"
" it(\"works correctly\", fn() { ... }), // Runs normally\n"
" skip(\"needs fixing\", fn() { ... }), // Skipped\n"
" it(\"handles edge cases\", fn() { ... }), // Runs normally\n"
" ])\n"
" ```\n"
"\n"
" ## Output\n"
"\n"
" ```text\n"
" Feature\n"
" ✓ works correctly\n"
" - needs fixing\n"
" ✓ handles edge cases\n"
"\n"
" Summary: 3 run, 0 failed, 2 passed, 1 skipped\n"
" ```\n"
"\n"
" ## When to Use\n"
"\n"
" - Test is broken and you need to fix it later\n"
" - Test depends on unimplemented functionality\n"
" - Test is flaky and needs investigation\n"
" - Temporarily disable slow tests during development\n"
"\n"
" ## Note\n"
"\n"
" The test body is preserved but not executed. This makes it easy to\n"
" toggle between `it` and `skip` without losing your test code.\n"
).
-spec skip(binary(), fun(() -> dream_test@types:assertion_result())) -> unit_test().
skip(Name, _) ->
{it_test, Name, fun() -> assertion_skipped end}.
-file("src/dream_test/unit.gleam", 258).
?DOC(
" Group related tests under a common description.\n"
"\n"
" Groups can be nested to any depth. The group names form a hierarchy that\n"
" appears in test output and failure messages.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" describe(\"String utilities\", [\n"
" describe(\"trim\", [\n"
" it(\"removes leading spaces\", fn() { ... }),\n"
" it(\"removes trailing spaces\", fn() { ... }),\n"
" ]),\n"
" describe(\"split\", [\n"
" it(\"splits on delimiter\", fn() { ... }),\n"
" ]),\n"
" ])\n"
" ```\n"
"\n"
" ## Output\n"
"\n"
" ```text\n"
" String utilities\n"
" trim\n"
" ✓ removes leading spaces\n"
" ✓ removes trailing spaces\n"
" split\n"
" ✓ splits on delimiter\n"
" ```\n"
).
-spec describe(binary(), list(unit_test())) -> unit_test().
describe(Name, Children) ->
{describe_group, Name, Children}.
-file("src/dream_test/unit.gleam", 317).
?DOC(
" Run setup once before all tests in the current `describe` block.\n"
"\n"
" Use `before_all` when you have expensive setup that should happen once\n"
" for the entire group rather than before each individual test.\n"
"\n"
" ## When to Use\n"
"\n"
" - Starting a database server\n"
" - Creating temporary files or directories\n"
" - Launching external services\n"
" - Any setup that's slow or has side effects you want to share\n"
"\n"
" ## Execution Behavior\n"
"\n"
" - Runs exactly once, before the first test in the group\n"
" - If it returns `AssertionFailed`, all tests in the group are skipped\n"
" and marked as `SetupFailed`\n"
" - Nested `describe` blocks each run their own `before_all` hooks\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/unit.{describe, it, before_all, after_all, to_test_suite}\n"
" import dream_test/runner.{run_suite}\n"
" import dream_test/types.{AssertionOk}\n"
"\n"
" describe(\"Database integration\", [\n"
" before_all(fn() {\n"
" // This runs once before any test\n"
" start_test_database()\n"
" run_migrations()\n"
" AssertionOk\n"
" }),\n"
"\n"
" it(\"creates users\", fn() { ... }),\n"
" it(\"queries users\", fn() { ... }),\n"
" it(\"updates users\", fn() { ... }),\n"
"\n"
" after_all(fn() {\n"
" stop_test_database()\n"
" AssertionOk\n"
" }),\n"
" ])\n"
" |> to_test_suite(\"db_test\")\n"
" |> run_suite()\n"
" ```\n"
"\n"
" ## Important: Requires Suite Mode\n"
"\n"
" `before_all` hooks only work with `to_test_suite` + `run_suite`.\n"
" When using `to_test_cases` + `run_all`, they are silently ignored.\n"
"\n"
" This is because flat mode loses the group structure needed to know\n"
" where \"all tests in a group\" begins and ends.\n"
).
-spec before_all(fun(() -> dream_test@types:assertion_result())) -> unit_test().
before_all(Setup) ->
{before_all, Setup}.
-file("src/dream_test/unit.gleam", 393).
?DOC(
" Run setup before each test in the current `describe` block.\n"
"\n"
" Use `before_each` when tests need a fresh, isolated state. This is the\n"
" most commonly used lifecycle hook.\n"
"\n"
" ## When to Use\n"
"\n"
" - Resetting database state between tests\n"
" - Creating fresh test fixtures\n"
" - Beginning a transaction to rollback later\n"
" - Clearing caches or in-memory state\n"
"\n"
" ## Execution Behavior\n"
"\n"
" - Runs before every test in the group and all nested groups\n"
" - If it returns `AssertionFailed`, the test is skipped and marked `SetupFailed`\n"
" - Multiple `before_each` hooks in the same group run in declaration order\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/unit.{describe, it, before_each, after_each, to_test_cases}\n"
" import dream_test/runner.{run_all}\n"
" import dream_test/types.{AssertionOk}\n"
"\n"
" describe(\"Shopping cart\", [\n"
" before_each(fn() {\n"
" // Fresh cart for each test\n"
" clear_cart()\n"
" AssertionOk\n"
" }),\n"
"\n"
" it(\"starts empty\", fn() {\n"
" get_cart_items()\n"
" |> should()\n"
" |> equal([])\n"
" |> or_fail_with(\"New cart should be empty\")\n"
" }),\n"
"\n"
" it(\"adds items\", fn() {\n"
" add_to_cart(\"apple\")\n"
" get_cart_items()\n"
" |> should()\n"
" |> contain(\"apple\")\n"
" |> or_fail_with(\"Cart should contain apple\")\n"
" }),\n"
" ])\n"
" |> to_test_cases(\"cart_test\")\n"
" |> run_all()\n"
" ```\n"
"\n"
" ## Hook Inheritance\n"
"\n"
" Nested `describe` blocks inherit parent `before_each` hooks. Parent hooks\n"
" run first (outer-to-inner order):\n"
"\n"
" ```gleam\n"
" describe(\"Outer\", [\n"
" before_each(fn() { setup_outer(); AssertionOk }), // Runs 1st\n"
"\n"
" describe(\"Inner\", [\n"
" before_each(fn() { setup_inner(); AssertionOk }), // Runs 2nd\n"
" it(\"test\", fn() { ... }),\n"
" ]),\n"
" ])\n"
" ```\n"
"\n"
" ## Works in Both Modes\n"
"\n"
" Unlike `before_all`, `before_each` works with both `to_test_cases`\n"
" and `to_test_suite`. Use whichever fits your needs.\n"
).
-spec before_each(fun(() -> dream_test@types:assertion_result())) -> unit_test().
before_each(Setup) ->
{before_each, Setup}.
-file("src/dream_test/unit.gleam", 462).
?DOC(
" Run teardown after each test in the current `describe` block.\n"
"\n"
" Use `after_each` to clean up resources created during a test. This hook\n"
" runs even if the test fails, ensuring reliable cleanup.\n"
"\n"
" ## When to Use\n"
"\n"
" - Rolling back database transactions\n"
" - Deleting temporary files created by the test\n"
" - Resetting global state or mocks\n"
" - Closing connections or releasing resources\n"
"\n"
" ## Execution Behavior\n"
"\n"
" - Runs after every test in the group and all nested groups\n"
" - **Always runs**, even if the test or `before_each` hooks fail\n"
" - Multiple `after_each` hooks in the same group run in reverse declaration order\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/unit.{describe, it, before_each, after_each, to_test_cases}\n"
" import dream_test/runner.{run_all}\n"
" import dream_test/types.{AssertionOk}\n"
"\n"
" describe(\"File operations\", [\n"
" before_each(fn() {\n"
" create_temp_directory()\n"
" AssertionOk\n"
" }),\n"
"\n"
" after_each(fn() {\n"
" // Always clean up, even if test crashes\n"
" delete_temp_directory()\n"
" AssertionOk\n"
" }),\n"
"\n"
" it(\"writes files\", fn() { ... }),\n"
" it(\"reads files\", fn() { ... }),\n"
" ])\n"
" |> to_test_cases(\"file_test\")\n"
" |> run_all()\n"
" ```\n"
"\n"
" ## Hook Inheritance\n"
"\n"
" Nested `describe` blocks inherit parent `after_each` hooks. Child hooks\n"
" run first (inner-to-outer order, reverse of `before_each`):\n"
"\n"
" ```gleam\n"
" describe(\"Outer\", [\n"
" after_each(fn() { teardown_outer(); AssertionOk }), // Runs 2nd\n"
"\n"
" describe(\"Inner\", [\n"
" after_each(fn() { teardown_inner(); AssertionOk }), // Runs 1st\n"
" it(\"test\", fn() { ... }),\n"
" ]),\n"
" ])\n"
" ```\n"
"\n"
" ## Works in Both Modes\n"
"\n"
" Like `before_each`, `after_each` works with both `to_test_cases`\n"
" and `to_test_suite`.\n"
).
-spec after_each(fun(() -> dream_test@types:assertion_result())) -> unit_test().
after_each(Teardown) ->
{after_each, Teardown}.
-file("src/dream_test/unit.gleam", 540).
?DOC(
" Run teardown once after all tests in the current `describe` block.\n"
"\n"
" Use `after_all` to clean up expensive resources that were set up by\n"
" `before_all`. This hook runs once after all tests complete, regardless\n"
" of whether tests passed or failed.\n"
"\n"
" ## When to Use\n"
"\n"
" - Stopping a database server started by `before_all`\n"
" - Removing temporary directories\n"
" - Shutting down external services\n"
" - Any cleanup that corresponds to `before_all` setup\n"
"\n"
" ## Execution Behavior\n"
"\n"
" - Runs exactly once, after the last test in the group completes\n"
" - **Always runs**, even if tests fail or `before_all` fails\n"
" - Nested `describe` blocks each run their own `after_all` hooks\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/unit.{describe, it, before_all, after_all, to_test_suite}\n"
" import dream_test/runner.{run_suite}\n"
" import dream_test/types.{AssertionOk}\n"
"\n"
" describe(\"External API integration\", [\n"
" before_all(fn() {\n"
" start_mock_server(port: 8080)\n"
" AssertionOk\n"
" }),\n"
"\n"
" it(\"fetches users\", fn() { ... }),\n"
" it(\"creates users\", fn() { ... }),\n"
" it(\"handles errors\", fn() { ... }),\n"
"\n"
" after_all(fn() {\n"
" // Clean up even if tests failed\n"
" stop_mock_server()\n"
" AssertionOk\n"
" }),\n"
" ])\n"
" |> to_test_suite(\"api_test\")\n"
" |> run_suite()\n"
" ```\n"
"\n"
" ## Complete Lifecycle Example\n"
"\n"
" Here's a complete example showing all four hooks working together:\n"
"\n"
" ```gleam\n"
" describe(\"Database tests\", [\n"
" // Once at start: start the database\n"
" before_all(fn() { start_db(); AssertionOk }),\n"
"\n"
" // Before each test: begin a transaction\n"
" before_each(fn() { begin_transaction(); AssertionOk }),\n"
"\n"
" it(\"creates records\", fn() { ... }),\n"
" it(\"queries records\", fn() { ... }),\n"
"\n"
" // After each test: rollback the transaction\n"
" after_each(fn() { rollback_transaction(); AssertionOk }),\n"
"\n"
" // Once at end: stop the database\n"
" after_all(fn() { stop_db(); AssertionOk }),\n"
" ])\n"
" ```\n"
"\n"
" ## Important: Requires Suite Mode\n"
"\n"
" `after_all` hooks only work with `to_test_suite` + `run_suite`.\n"
" When using `to_test_cases` + `run_all`, they are silently ignored.\n"
).
-spec after_all(fun(() -> dream_test@types:assertion_result())) -> unit_test().
after_all(Teardown) ->
{after_all, Teardown}.
-file("src/dream_test/unit.gleam", 558).
-spec empty_hook_context() -> hook_context().
empty_hook_context() ->
{hook_context, [], []}.
-file("src/dream_test/unit.gleam", 656).
-spec collect_hook_from_node(unit_test(), hook_context()) -> hook_context().
collect_hook_from_node(Node, Context) ->
case Node of
{before_each, Setup} ->
Hooks = lists:append(erlang:element(2, Context), [Setup]),
{hook_context, Hooks, erlang:element(3, Context)};
{after_each, Teardown} ->
{hook_context,
erlang:element(2, Context),
[Teardown | erlang:element(3, Context)]};
_ ->
Context
end.
-file("src/dream_test/unit.gleam", 643).
-spec collect_hooks_from_list(list(unit_test()), hook_context()) -> hook_context().
collect_hooks_from_list(Remaining, Context) ->
case Remaining of
[] ->
Context;
[Head | Tail] ->
Updated = collect_hook_from_node(Head, Context),
collect_hooks_from_list(Tail, Updated)
end.
-file("src/dream_test/unit.gleam", 636).
?DOC(" Collect hooks from a list of children and merge with inherited hooks.\n").
-spec collect_hooks_from_children(list(unit_test()), hook_context()) -> hook_context().
collect_hooks_from_children(Children, Inherited) ->
collect_hooks_from_list(Children, Inherited).
-file("src/dream_test/unit.gleam", 675).
-spec build_it_test_case(
list(binary()),
binary(),
fun(() -> dream_test@types:assertion_result()),
hook_context(),
list(dream_test@types:test_case())
) -> list(dream_test@types:test_case()).
build_it_test_case(Name_prefix, Name, Run, Hook_context, Accumulated) ->
Full_name = lists:append(Name_prefix, [Name]),
Config = {single_test_config,
Name,
Full_name,
[],
unit,
Run,
none,
erlang:element(2, Hook_context),
erlang:element(3, Hook_context)},
Test_case = {test_case, Config},
[Test_case | Accumulated].
-file("src/dream_test/unit.gleam", 918).
-spec collect_suite_hook(unit_test(), suite_hooks()) -> suite_hooks().
collect_suite_hook(Node, Hooks) ->
case Node of
{before_all, Setup} ->
{suite_hooks,
[Setup | erlang:element(2, Hooks)],
erlang:element(3, Hooks)};
{after_all, Teardown} ->
{suite_hooks,
erlang:element(2, Hooks),
[Teardown | erlang:element(3, Hooks)]};
_ ->
Hooks
end.
-file("src/dream_test/unit.gleam", 901).
-spec collect_suite_hooks_from_list(list(unit_test()), suite_hooks()) -> suite_hooks().
collect_suite_hooks_from_list(Remaining, Hooks) ->
case Remaining of
[] ->
{suite_hooks,
lists:reverse(erlang:element(2, Hooks)),
lists:reverse(erlang:element(3, Hooks))};
[Head | Tail] ->
Updated = collect_suite_hook(Head, Hooks),
collect_suite_hooks_from_list(Tail, Updated)
end.
-file("src/dream_test/unit.gleam", 894).
-spec collect_suite_hooks(list(unit_test())) -> suite_hooks().
collect_suite_hooks(Children) ->
collect_suite_hooks_from_list(Children, {suite_hooks, [], []}).
-file("src/dream_test/unit.gleam", 976).
-spec build_single_test_case(
list(binary()),
binary(),
fun(() -> dream_test@types:assertion_result()),
hook_context()
) -> dream_test@types:test_case().
build_single_test_case(Full_name, Name, Run, Hook_context) ->
Config = {single_test_config,
Name,
Full_name,
[],
unit,
Run,
none,
erlang:element(2, Hook_context),
erlang:element(3, Hook_context)},
{test_case, Config}.
-file("src/dream_test/unit.gleam", 996).
-spec extract_last_name(list(binary())) -> binary().
extract_last_name(Full_name) ->
case lists:reverse(Full_name) of
[Last | _] ->
Last;
[] ->
<<""/utf8>>
end.
-file("src/dream_test/unit.gleam", 946).
-spec build_suite_item(binary(), list(binary()), hook_context(), unit_test()) -> list(dream_test@types:test_suite_item()).
build_suite_item(Module_name, Name_prefix, Hook_context, Node) ->
case Node of
{it_test, Name, Run} ->
Full_name = lists:append(Name_prefix, [Name]),
Test_case = build_single_test_case(
Full_name,
Name,
Run,
Hook_context
),
[{suite_test, Test_case}];
{describe_group, Name@1, Children} ->
Full_name@1 = lists:append(Name_prefix, [Name@1]),
Nested_suite = build_suite_from_describe(
Module_name,
Full_name@1,
Hook_context,
Children
),
[{suite_group, Nested_suite}];
{before_all, _} ->
[];
{before_each, _} ->
[];
{after_each, _} ->
[];
{after_all, _} ->
[]
end.
-file("src/dream_test/unit.gleam", 860).
-spec build_suite_from_describe(
binary(),
list(binary()),
hook_context(),
list(unit_test())
) -> dream_test@types:test_suite().
build_suite_from_describe(Module_name, Full_name, Inherited_hooks, Children) ->
Suite_hooks = collect_suite_hooks(Children),
Group_hooks = collect_hooks_from_children(Children, Inherited_hooks),
Items = build_suite_items(Module_name, Full_name, Group_hooks, Children, []),
Name = extract_last_name(Full_name),
{test_suite,
Name,
Full_name,
erlang:element(2, Suite_hooks),
erlang:element(3, Suite_hooks),
Items}.
-file("src/dream_test/unit.gleam", 928).
-spec build_suite_items(
binary(),
list(binary()),
hook_context(),
list(unit_test()),
list(dream_test@types:test_suite_item())
) -> list(dream_test@types:test_suite_item()).
build_suite_items(
Module_name,
Name_prefix,
Hook_context,
Remaining,
Accumulated
) ->
case Remaining of
[] ->
lists:reverse(Accumulated);
[Head | Tail] ->
New_items = build_suite_item(
Module_name,
Name_prefix,
Hook_context,
Head
),
Updated = lists:append(lists:reverse(New_items), Accumulated),
build_suite_items(
Module_name,
Name_prefix,
Hook_context,
Tail,
Updated
)
end.
-file("src/dream_test/unit.gleam", 817).
-spec to_suite_from_unit_test(
binary(),
list(binary()),
hook_context(),
unit_test()
) -> dream_test@types:test_suite().
to_suite_from_unit_test(Module_name, Name_prefix, Inherited_hooks, Node) ->
case Node of
{describe_group, Name, Children} ->
Full_name = lists:append(Name_prefix, [Name]),
build_suite_from_describe(
Module_name,
Full_name,
Inherited_hooks,
Children
);
{it_test, Name@1, Run} ->
Full_name@1 = lists:append(Name_prefix, [Name@1]),
Test_case = build_single_test_case(
Full_name@1,
Name@1,
Run,
Inherited_hooks
),
{test_suite,
Module_name,
[Module_name],
[],
[],
[{suite_test, Test_case}]};
{before_all, _} ->
{test_suite, Module_name, [Module_name], [], [], []};
{before_each, _} ->
{test_suite, Module_name, [Module_name], [], [], []};
{after_each, _} ->
{test_suite, Module_name, [Module_name], [], [], []};
{after_all, _} ->
{test_suite, Module_name, [Module_name], [], [], []}
end.
-file("src/dream_test/unit.gleam", 812).
?DOC(
" Convert a test tree into a structured test suite.\n"
"\n"
" Use `to_test_suite` when you need `before_all` or `after_all` hooks.\n"
" Unlike `to_test_cases`, this preserves the group hierarchy required\n"
" for once-per-group semantics.\n"
"\n"
" ## When to Use Each Mode\n"
"\n"
" | Scenario | Function | Runner |\n"
" |---------------------------------------------|------------------|--------------|\n"
" | Simple tests, no hooks | `to_test_cases` | `run_all` |\n"
" | Only `before_each`/`after_each` | `to_test_cases` | `run_all` |\n"
" | Need `before_all` or `after_all` | `to_test_suite` | `run_suite` |\n"
" | Expensive setup shared across tests | `to_test_suite` | `run_suite` |\n"
"\n"
" ## How It Works\n"
"\n"
" ```text\n"
" describe(\"A\", [ TestSuite(\"A\")\n"
" before_all(setup), → before_all: [setup]\n"
" it(\"test1\", ...), items: [\n"
" describe(\"B\", [ SuiteTest(test1),\n"
" it(\"test2\", ...), SuiteGroup(TestSuite(\"B\", ...))\n"
" ]), ]\n"
" ])\n"
" ```\n"
"\n"
" The tree structure is preserved, allowing the runner to execute\n"
" `before_all` before entering a group and `after_all` after leaving.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream_test/unit.{describe, it, before_all, after_all, to_test_suite}\n"
" import dream_test/runner.{run_suite}\n"
" import dream_test/reporter/bdd.{report}\n"
" import dream_test/types.{AssertionOk}\n"
" import gleam/io\n"
"\n"
" pub fn main() {\n"
" tests()\n"
" |> to_test_suite(\"integration_test\")\n"
" |> run_suite()\n"
" |> report(io.print)\n"
" }\n"
"\n"
" pub fn tests() {\n"
" describe(\"Payment processing\", [\n"
" before_all(fn() {\n"
" start_payment_gateway_mock()\n"
" AssertionOk\n"
" }),\n"
"\n"
" describe(\"successful payments\", [\n"
" it(\"processes credit cards\", fn() { ... }),\n"
" it(\"processes debit cards\", fn() { ... }),\n"
" ]),\n"
"\n"
" describe(\"failed payments\", [\n"
" it(\"handles declined cards\", fn() { ... }),\n"
" it(\"handles network errors\", fn() { ... }),\n"
" ]),\n"
"\n"
" after_all(fn() {\n"
" stop_payment_gateway_mock()\n"
" AssertionOk\n"
" }),\n"
" ])\n"
" }\n"
" ```\n"
"\n"
" ## Parameters\n"
"\n"
" - `module_name` - Name of the test module (appears in output)\n"
" - `root` - The root `UnitTest` node (typically from `describe`)\n"
"\n"
" ## Returns\n"
"\n"
" A `TestSuite` that can be executed with `run_suite` or `run_suite_with_config`.\n"
).
-spec to_test_suite(binary(), unit_test()) -> dream_test@types:test_suite().
to_test_suite(Module_name, Root) ->
Context = empty_hook_context(),
to_suite_from_unit_test(Module_name, [], Context, Root).
-file("src/dream_test/unit.gleam", 698).
-spec to_test_cases_from_list(
binary(),
list(binary()),
hook_context(),
list(unit_test()),
list(dream_test@types:test_case())
) -> list(dream_test@types:test_case()).
to_test_cases_from_list(
Module_name,
Name_prefix,
Hook_context,
Remaining,
Accumulated
) ->
case Remaining of
[] ->
lists:reverse(Accumulated);
[Head | Tail] ->
Updated = to_test_cases_from_unit_test(
Module_name,
Name_prefix,
Hook_context,
Head,
Accumulated
),
to_test_cases_from_list(
Module_name,
Name_prefix,
Hook_context,
Tail,
Updated
)
end.
-file("src/dream_test/unit.gleam", 602).
-spec to_test_cases_from_unit_test(
binary(),
list(binary()),
hook_context(),
unit_test(),
list(dream_test@types:test_case())
) -> list(dream_test@types:test_case()).
to_test_cases_from_unit_test(
Module_name,
Name_prefix,
Hook_context,
Node,
Accumulated
) ->
case Node of
{it_test, Name, Run} ->
build_it_test_case(
Name_prefix,
Name,
Run,
Hook_context,
Accumulated
);
{describe_group, Name@1, Children} ->
New_prefix = lists:append(Name_prefix, [Name@1]),
Group_hooks = collect_hooks_from_children(Children, Hook_context),
to_test_cases_from_list(
Module_name,
New_prefix,
Group_hooks,
Children,
Accumulated
);
{before_all, _} ->
Accumulated;
{after_all, _} ->
Accumulated;
{before_each, _} ->
Accumulated;
{after_each, _} ->
Accumulated
end.
-file("src/dream_test/unit.gleam", 597).
?DOC(
" Convert a test tree into a flat list of runnable test cases.\n"
"\n"
" This function walks the `UnitTest` tree and produces `TestCase` values\n"
" that the runner can execute. Each test case includes:\n"
"\n"
" - `name` - The test's own name (from `it`)\n"
" - `full_name` - The complete path including all `describe` ancestors\n"
" - `tags` - Currently empty (tag support coming soon)\n"
" - `kind` - Set to `Unit` for all tests from this DSL\n"
" - `before_each_hooks` - Inherited hooks to run before the test\n"
" - `after_each_hooks` - Inherited hooks to run after the test\n"
"\n"
" ## Hook Handling\n"
"\n"
" - `before_each`/`after_each` hooks are collected and attached to each test\n"
" - `before_all`/`after_all` hooks are ignored (use `to_test_suite` instead)\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let test_cases =\n"
" describe(\"Math\", [\n"
" it(\"adds\", fn() { ... }),\n"
" it(\"subtracts\", fn() { ... }),\n"
" ])\n"
" |> to_test_cases(\"math_test\")\n"
"\n"
" // test_cases is now a List(TestCase) ready for run_all()\n"
" ```\n"
"\n"
" ## Parameters\n"
"\n"
" - `module_name` - The name of the test module (used for identification)\n"
" - `root` - The root `UnitTest` node (typically from `describe`)\n"
).
-spec to_test_cases(binary(), unit_test()) -> list(dream_test@types:test_case()).
to_test_cases(Module_name, Root) ->
Context = empty_hook_context(),
to_test_cases_from_unit_test(Module_name, [], Context, Root, []).