Current section

Files

Jump to
dream_test src dream_test@gherkin@feature.erl
Raw

src/dream_test@gherkin@feature.erl

-module(dream_test@gherkin@feature).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream_test/gherkin/feature.gleam").
-export([to_test_suite/2, scenario/2, with_tags/2, given/1, 'when'/1, then/1, 'and'/1, but/1, feature/3, background/1, feature_with_background/4, to_test_cases/2]).
-export_type([feature_config/0, inline_scenario/0, inline_step/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(
" Feature execution and TestSuite conversion for Gherkin tests.\n"
"\n"
" This module converts parsed Gherkin features to dream_test TestSuites\n"
" and provides an inline DSL for defining features directly in Gleam.\n"
"\n"
" ## Two Approaches\n"
"\n"
" 1. **File-based**: Parse `.feature` files with standard Gherkin syntax\n"
" 2. **Inline DSL**: Define features directly in Gleam code\n"
"\n"
" Both approaches share the same step definitions and execution engine.\n"
"\n"
" ## File-Based Usage\n"
"\n"
" Parse a `.feature` file and convert to TestSuite:\n"
"\n"
" ```gleam\n"
" import dream_test/gherkin/feature.{FeatureConfig, to_test_suite}\n"
" import dream_test/gherkin/parser\n"
" import dream_test/gherkin/steps.{new_registry, given, when_, then_}\n"
" import dream_test/runner\n"
"\n"
" pub fn main() {\n"
" let steps = new_registry()\n"
" |> given(\"I have {int} items\", have_items)\n"
" |> when_(\"I add {int} items\", add_items)\n"
" |> then_(\"the total is ${float}\", check_total)\n"
"\n"
" let assert Ok(parsed) = parser.parse_file(\"features/cart.feature\")\n"
" let config = FeatureConfig(feature: parsed, step_registry: steps)\n"
" \n"
" to_test_suite(\"cart_test\", config)\n"
" |> runner.run_suite()\n"
" }\n"
" ```\n"
"\n"
" ## Inline DSL Usage\n"
"\n"
" Define features directly in Gleam without `.feature` files:\n"
"\n"
" ```gleam\n"
" import dream_test/gherkin/feature.{\n"
" feature, scenario, given, when, then, and, with_tags,\n"
" }\n"
"\n"
" pub fn tests() -> TestSuite {\n"
" let steps = cart_steps()\n"
" \n"
" feature(\"Shopping Cart\", steps, [\n"
" scenario(\"Adding items\", [\n"
" given(\"I have an empty cart\"),\n"
" when(\"I add 2 apples to the cart\"),\n"
" then(\"the cart should contain 2 items\"),\n"
" and(\"the total should be $3.00\"),\n"
" ])\n"
" |> with_tags([\"happy-path\"]),\n"
" ])\n"
" }\n"
" ```\n"
"\n"
).
-type feature_config() :: {feature_config,
dream_test@gherkin@types:feature(),
dream_test@gherkin@steps:step_registry()}.
-type inline_scenario() :: {inline_scenario,
binary(),
list(inline_step()),
list(binary())}.
-type inline_step() :: {inline_step, binary(), binary()}.
-file("src/dream_test/gherkin/feature.gleam", 250).
-spec build_full_name(binary(), binary(), gleam@option:option(binary())) -> list(binary()).
build_full_name(Feature_name, Scenario_name, Example_suffix) ->
case Example_suffix of
none ->
[Feature_name, Scenario_name];
{some, Suffix} ->
[Feature_name,
<<<<Scenario_name/binary, " "/utf8>>/binary, Suffix/binary>>]
end.
-file("src/dream_test/gherkin/feature.gleam", 289).
-spec build_substitution_map(list(binary()), list(binary())) -> gleam@dict:dict(binary(), binary()).
build_substitution_map(Headers, Values) ->
_pipe = gleam@list:zip(Headers, Values),
gleam@list:fold(
_pipe,
maps:new(),
fun(Acc, Pair) ->
{Header, Value} = Pair,
gleam@dict:insert(Acc, Header, Value)
end
).
-file("src/dream_test/gherkin/feature.gleam", 319).
-spec substitute_placeholders(binary(), gleam@dict:dict(binary(), binary())) -> binary().
substitute_placeholders(Text, Substitutions) ->
gleam@dict:fold(
Substitutions,
Text,
fun(Acc, Header, Value) ->
Placeholder = <<<<"<"/utf8, Header/binary>>/binary, ">"/utf8>>,
gleam@string:replace(Acc, Placeholder, Value)
end
).
-file("src/dream_test/gherkin/feature.gleam", 307).
-spec substitute_step(
dream_test@gherkin@types:step(),
gleam@dict:dict(binary(), binary())
) -> dream_test@gherkin@types:step().
substitute_step(Step, Substitutions) ->
New_text = substitute_placeholders(erlang:element(3, Step), Substitutions),
{step, erlang:element(2, Step), New_text, erlang:element(4, Step)}.
-file("src/dream_test/gherkin/feature.gleam", 300).
-spec substitute_steps(
list(dream_test@gherkin@types:step()),
gleam@dict:dict(binary(), binary())
) -> list(dream_test@gherkin@types:step()).
substitute_steps(Steps, Substitutions) ->
gleam@list:map(Steps, fun(Step) -> substitute_step(Step, Substitutions) end).
-file("src/dream_test/gherkin/feature.gleam", 421).
-spec extract_table_from_step(dream_test@gherkin@types:step()) -> gleam@option:option(list(list(binary()))).
extract_table_from_step(Step) ->
case erlang:element(4, Step) of
{some, {data_table, Rows}} ->
{some, Rows};
_ ->
none
end.
-file("src/dream_test/gherkin/feature.gleam", 430).
-spec extract_doc_string_from_step(dream_test@gherkin@types:step()) -> gleam@option:option(binary()).
extract_doc_string_from_step(Step) ->
case erlang:element(4, Step) of
{some, {doc_string, Content, _}} ->
{some, Content};
_ ->
none
end.
-file("src/dream_test/gherkin/feature.gleam", 405).
-spec build_step_context(
dream_test@gherkin@step_trie:step_match(fun((dream_test@gherkin@steps:step_context()) -> dream_test@types:assertion_result())),
dream_test@gherkin@types:step(),
dream_test@gherkin@world:world()
) -> dream_test@gherkin@steps:step_context().
build_step_context(Match, Step, The_world) ->
Table = extract_table_from_step(Step),
Doc_string = extract_doc_string_from_step(Step),
{step_context, erlang:element(3, Match), Table, Doc_string, The_world}.
-file("src/dream_test/gherkin/feature.gleam", 379).
-spec execute_step(
dream_test@gherkin@types:step(),
dream_test@gherkin@steps:step_registry(),
dream_test@gherkin@world:world(),
dream_test@gherkin@types:step_keyword()
) -> dream_test@types:assertion_result().
execute_step(Step, Registry, The_world, Previous_keyword) ->
Effective_keyword = dream_test@gherkin@types:resolve_keyword(
erlang:element(2, Step),
Previous_keyword
),
case dream_test@gherkin@steps:find_step(
Registry,
Effective_keyword,
erlang:element(3, Step)
) of
{ok, Match} ->
Context = build_step_context(Match, Step, The_world),
(erlang:element(2, Match))(Context);
{error, Msg} ->
{assertion_failed, {assertion_failure, <<"step"/utf8>>, Msg, none}}
end.
-file("src/dream_test/gherkin/feature.gleam", 358).
-spec execute_steps(
list(dream_test@gherkin@types:step()),
dream_test@gherkin@steps:step_registry(),
dream_test@gherkin@world:world(),
dream_test@gherkin@types:step_keyword()
) -> dream_test@types:assertion_result().
execute_steps(Steps, Registry, The_world, Previous_keyword) ->
case Steps of
[] ->
assertion_ok;
[Step | Rest] ->
case execute_step(Step, Registry, The_world, Previous_keyword) of
assertion_ok ->
Effective_keyword = dream_test@gherkin@types:resolve_keyword(
erlang:element(2, Step),
Previous_keyword
),
execute_steps(Rest, Registry, The_world, Effective_keyword);
Failure ->
Failure
end
end.
-file("src/dream_test/gherkin/feature.gleam", 341).
-spec execute_scenario(
binary(),
list(dream_test@gherkin@types:step()),
dream_test@gherkin@steps:step_registry()
) -> dream_test@types:assertion_result().
execute_scenario(Scenario_id, Steps, Registry) ->
The_world = dream_test@gherkin@world:new_world(Scenario_id),
Result = execute_steps(Steps, Registry, The_world, given),
dream_test@gherkin@world:cleanup(The_world),
Result.
-file("src/dream_test/gherkin/feature.gleam", 333).
-spec build_scenario_runner(
binary(),
list(dream_test@gherkin@types:step()),
dream_test@gherkin@steps:step_registry()
) -> fun(() -> dream_test@types:assertion_result()).
build_scenario_runner(Scenario_id, Steps, Registry) ->
fun() -> execute_scenario(Scenario_id, Steps, Registry) end.
-file("src/dream_test/gherkin/feature.gleam", 213).
-spec build_scenario_test_case(
dream_test@gherkin@types:feature(),
binary(),
list(binary()),
list(dream_test@gherkin@types:step()),
feature_config(),
gleam@option:option(binary())
) -> dream_test@types:test_case().
build_scenario_test_case(
Feature,
Scenario_name,
Scenario_tags,
Steps,
Config,
Example_suffix
) ->
Full_name = build_full_name(
erlang:element(2, Feature),
Scenario_name,
Example_suffix
),
Scenario_id = gleam@string:join(Full_name, <<"::"/utf8>>),
All_tags = lists:append(erlang:element(4, Feature), Scenario_tags),
All_steps = case erlang:element(5, Feature) of
{some, {background, Background_steps}} ->
lists:append(Background_steps, Steps);
none ->
Steps
end,
Run_fn = build_scenario_runner(
Scenario_id,
All_steps,
erlang:element(3, Config)
),
Single_config = {single_test_config,
Scenario_name,
Full_name,
All_tags,
{gherkin_scenario, Scenario_id},
Run_fn,
none,
[],
[]},
{test_case, Single_config}.
-file("src/dream_test/gherkin/feature.gleam", 261).
-spec expand_scenario_outline(
dream_test@gherkin@types:feature(),
binary(),
list(binary()),
list(dream_test@gherkin@types:step()),
dream_test@gherkin@types:examples_table(),
feature_config()
) -> list(dream_test@types:test_suite_item()).
expand_scenario_outline(Feature, Name, Tags, Steps, Examples, Config) ->
Headers = erlang:element(2, Examples),
gleam@list:index_map(
erlang:element(3, Examples),
fun(Row, Index) ->
Substitutions = build_substitution_map(Headers, Row),
Expanded_steps = substitute_steps(Steps, Substitutions),
Suffix = <<<<"(Example "/utf8,
(erlang:integer_to_binary(Index + 1))/binary>>/binary,
")"/utf8>>,
Test_case = build_scenario_test_case(
Feature,
Name,
Tags,
Expanded_steps,
Config,
{some, Suffix}
),
{suite_test, Test_case}
end
).
-file("src/dream_test/gherkin/feature.gleam", 196).
-spec scenario_to_suite_items(
dream_test@gherkin@types:feature(),
dream_test@gherkin@types:scenario(),
feature_config()
) -> list(dream_test@types:test_suite_item()).
scenario_to_suite_items(Feature, Scenario, Config) ->
case Scenario of
{scenario, Name, Tags, Steps} ->
Test_case = build_scenario_test_case(
Feature,
Name,
Tags,
Steps,
Config,
none
),
[{suite_test, Test_case}];
{scenario_outline, Name@1, Tags@1, Steps@1, Examples} ->
expand_scenario_outline(
Feature,
Name@1,
Tags@1,
Steps@1,
Examples,
Config
)
end.
-file("src/dream_test/gherkin/feature.gleam", 187).
-spec build_suite_items(dream_test@gherkin@types:feature(), feature_config()) -> list(dream_test@types:test_suite_item()).
build_suite_items(Feature, Config) ->
gleam@list:flat_map(
erlang:element(6, Feature),
fun(Scenario) -> scenario_to_suite_items(Feature, Scenario, Config) end
).
-file("src/dream_test/gherkin/feature.gleam", 141).
?DOC(
" Convert a Feature to a TestSuite.\n"
"\n"
" Creates a TestSuite with one test per scenario. Background steps are\n"
" prepended to each scenario. ScenarioOutlines are expanded based on\n"
" their examples table.\n"
"\n"
" ## Parameters\n"
"\n"
" - `module_name`: Name for the suite (usually the test module name)\n"
" - `config`: FeatureConfig with feature and step registry\n"
"\n"
" ## Returns\n"
"\n"
" A TestSuite that can be run with `runner.run_suite()`\n"
).
-spec to_test_suite(binary(), feature_config()) -> dream_test@types:test_suite().
to_test_suite(Module_name, Config) ->
Feature = erlang:element(2, Config),
Items = build_suite_items(Feature, Config),
{test_suite,
erlang:element(2, Feature),
[Module_name, erlang:element(2, Feature)],
[],
[],
Items}.
-file("src/dream_test/gherkin/feature.gleam", 496).
?DOC(
" Define an inline scenario.\n"
"\n"
" ## Parameters\n"
"\n"
" - `name`: Scenario name\n"
" - `steps`: List of inline steps\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" scenario(\"Adding items\", [\n"
" given(\"I have an empty cart\"),\n"
" when_step(\"I add 5 items\"),\n"
" then_step(\"I should have 5 items\"),\n"
" ])\n"
" ```\n"
).
-spec scenario(binary(), list(inline_step())) -> inline_scenario().
scenario(Name, Inline_steps) ->
{inline_scenario, Name, Inline_steps, []}.
-file("src/dream_test/gherkin/feature.gleam", 517).
?DOC(
" Add tags to a Gherkin scenario for filtering.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" scenario(\"Adding items\", [\n"
" when(\"I add 2 apples to the cart\"),\n"
" then(\"the cart should contain 2 items\"),\n"
" ])\n"
" |> with_tags([\"happy-path\", \"smoke\"])\n"
" ```\n"
"\n"
" ## Note\n"
"\n"
" This function is for Gherkin scenarios. For unit tests (`it`), use\n"
" `dream_test/unit.with_tags` instead.\n"
).
-spec with_tags(inline_scenario(), list(binary())) -> inline_scenario().
with_tags(Inline_scenario, Tags) ->
{inline_scenario,
erlang:element(2, Inline_scenario),
erlang:element(3, Inline_scenario),
Tags}.
-file("src/dream_test/gherkin/feature.gleam", 532).
?DOC(
" Create a Given step for inline DSL.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" given(\"I have {int} items in my cart\")\n"
" ```\n"
).
-spec given(binary()) -> inline_step().
given(Text) ->
{inline_step, <<"Given"/utf8>>, Text}.
-file("src/dream_test/gherkin/feature.gleam", 544).
?DOC(
" Create a When step for inline DSL.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" when(\"I add {int} items\")\n"
" ```\n"
).
-spec 'when'(binary()) -> inline_step().
'when'(Text) ->
{inline_step, <<"When"/utf8>>, Text}.
-file("src/dream_test/gherkin/feature.gleam", 556).
?DOC(
" Create a Then step for inline DSL.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" then(\"I should have {int} items\")\n"
" ```\n"
).
-spec then(binary()) -> inline_step().
then(Text) ->
{inline_step, <<"Then"/utf8>>, Text}.
-file("src/dream_test/gherkin/feature.gleam", 568).
?DOC(
" Create an And step for inline DSL.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" and(\"I have a coupon\")\n"
" ```\n"
).
-spec 'and'(binary()) -> inline_step().
'and'(Text) ->
{inline_step, <<"And"/utf8>>, Text}.
-file("src/dream_test/gherkin/feature.gleam", 580).
?DOC(
" Create a But step for inline DSL.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" but(\"I should not see errors\")\n"
" ```\n"
).
-spec but(binary()) -> inline_step().
but(Text) ->
{inline_step, <<"But"/utf8>>, Text}.
-file("src/dream_test/gherkin/feature.gleam", 598).
-spec parse_keyword(binary()) -> dream_test@gherkin@types:step_keyword().
parse_keyword(Keyword_str) ->
case Keyword_str of
<<"Given"/utf8>> ->
given;
<<"When"/utf8>> ->
'when';
<<"Then"/utf8>> ->
then;
<<"And"/utf8>> ->
'and';
<<"But"/utf8>> ->
but;
_ ->
given
end.
-file("src/dream_test/gherkin/feature.gleam", 593).
-spec inline_to_parsed_step(inline_step()) -> dream_test@gherkin@types:step().
inline_to_parsed_step(Inline) ->
Keyword = parse_keyword(erlang:element(2, Inline)),
{step, Keyword, erlang:element(3, Inline), none}.
-file("src/dream_test/gherkin/feature.gleam", 584).
-spec inline_to_parsed_scenario(inline_scenario()) -> dream_test@gherkin@types:scenario().
inline_to_parsed_scenario(Inline) ->
Parsed_steps = gleam@list:map(
erlang:element(3, Inline),
fun inline_to_parsed_step/1
),
{scenario,
erlang:element(2, Inline),
erlang:element(4, Inline),
Parsed_steps}.
-file("src/dream_test/gherkin/feature.gleam", 460).
?DOC(
" Define a feature inline in Gleam code.\n"
"\n"
" Creates a TestSuite from inline scenario definitions without needing\n"
" a `.feature` file.\n"
"\n"
" ## Parameters\n"
"\n"
" - `name`: Feature name\n"
" - `registry`: Step registry with step definitions\n"
" - `scenarios`: List of inline scenarios\n"
"\n"
" ## Returns\n"
"\n"
" A TestSuite that can be run with `runner.run_suite()`\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" feature(\"Shopping Cart\", steps, [\n"
).
-spec feature(
binary(),
dream_test@gherkin@steps:step_registry(),
list(inline_scenario())
) -> dream_test@types:test_suite().
feature(Name, Registry, Scenarios) ->
Parsed_scenarios = gleam@list:map(
Scenarios,
fun inline_to_parsed_scenario/1
),
Parsed_feature = {feature, Name, none, [], none, Parsed_scenarios},
Config = {feature_config, Parsed_feature, Registry},
to_test_suite(<<Name/binary, "_test"/utf8>>, Config).
-file("src/dream_test/gherkin/feature.gleam", 628).
?DOC(
" Define a background for inline features.\n"
"\n"
" Background steps run before each scenario.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let bg = background([\n"
" given(\"I am logged in\"),\n"
" given(\"I have an empty cart\"),\n"
" ])\n"
"\n"
" feature_with_background(\"Shopping\", steps, bg, [...scenarios...])\n"
" ```\n"
).
-spec background(list(inline_step())) -> list(dream_test@gherkin@types:step()).
background(Inline_steps) ->
gleam@list:map(Inline_steps, fun inline_to_parsed_step/1).
-file("src/dream_test/gherkin/feature.gleam", 641).
?DOC(
" Define a feature with a background.\n"
"\n"
" ## Parameters\n"
"\n"
" - `name`: Feature name\n"
" - `registry`: Step registry\n"
" - `background_steps`: Steps to run before each scenario\n"
" - `scenarios`: List of inline scenarios\n"
).
-spec feature_with_background(
binary(),
dream_test@gherkin@steps:step_registry(),
list(dream_test@gherkin@types:step()),
list(inline_scenario())
) -> dream_test@types:test_suite().
feature_with_background(Name, Registry, Background_steps, Scenarios) ->
Parsed_scenarios = gleam@list:map(
Scenarios,
fun inline_to_parsed_scenario/1
),
Parsed_feature = {feature,
Name,
none,
[],
{some, {background, Background_steps}},
Parsed_scenarios},
Config = {feature_config, Parsed_feature, Registry},
to_test_suite(<<Name/binary, "_test"/utf8>>, Config).
-file("src/dream_test/gherkin/feature.gleam", 180).
-spec flatten_item(dream_test@types:test_suite_item()) -> list(dream_test@types:test_case()).
flatten_item(Item) ->
case Item of
{suite_test, Test_case} ->
[Test_case];
{suite_group, Nested_suite} ->
flatten_suite(Nested_suite)
end.
-file("src/dream_test/gherkin/feature.gleam", 176).
-spec flatten_suite(dream_test@types:test_suite()) -> list(dream_test@types:test_case()).
flatten_suite(Suite) ->
gleam@list:flat_map(erlang:element(6, Suite), fun flatten_item/1).
-file("src/dream_test/gherkin/feature.gleam", 168).
?DOC(
" Convert a Feature to a flat list of TestCases.\n"
"\n"
" Unlike `to_test_suite`, this flattens the feature to a simple list.\n"
" Use this when you don't need before_all/after_all hooks.\n"
"\n"
" ## Parameters\n"
"\n"
" - `module_name`: Name prefix for test paths\n"
" - `config`: FeatureConfig with feature and step registry\n"
"\n"
" ## Returns\n"
"\n"
" A list of TestCases that can be run with `runner.run_all()`\n"
).
-spec to_test_cases(binary(), feature_config()) -> list(dream_test@types:test_case()).
to_test_cases(Module_name, Config) ->
Suite = to_test_suite(Module_name, Config),
flatten_suite(Suite).