diff --git a/.travis.yml b/.travis.yml index 5c4c3487df673560cd388b2064665905cf92b70c..74acb06755da80daa40af698d0a2777d7a0132a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: erlang + otp_release: - 17.4 - - R16B + - R16B03 - R15B03 notifications: diff --git a/include/gurka.hrl b/include/gurka.hrl new file mode 100644 index 0000000000000000000000000000000000000000..412934605be8a05d5f51a7f00414534d02753352 --- /dev/null +++ b/include/gurka.hrl @@ -0,0 +1,16 @@ +-type phase() :: feature | background | scenario | scenario_outline | meta. +-type action() :: desc | title | given | 'when' | then | examples | headers | values | tags. +-type tokens() :: [binary()]. +-type row() :: pos_integer(). +-type meta() :: [{atom(),term()}]. +-type lines() :: [{row(), binary(), tokens()}]. + +-record(step, { + phase :: phase(), + action = [] :: action(), + tokens = [] :: tokens(), + row = 0 :: row(), + meta = [] :: meta() +}). + +-type feature() :: [#step{}]. diff --git a/rebar b/rebar index 9bc9bd14eb3cae28559586bb78bcdd61b78b90ed..8978e55ff2a2853d7aabb298d411e63ccd7e9835 100755 Binary files a/rebar and b/rebar differ diff --git a/src/gurka.app.src b/src/gurka.app.src index 00994ad045f68eb67b39b3d4e8763dc2a59561a8..28577a8fb70cedec559c72807e5aff0ce18c1d93 100644 --- a/src/gurka.app.src +++ b/src/gurka.app.src @@ -1,7 +1,7 @@ {application, gurka, [ {description, "Erlang implementation of Cucumber"}, - {vsn, "0.0.1"}, + {vsn, "0.3.0"}, {applications, [ kernel, stdlib diff --git a/src/gurka.erl b/src/gurka.erl index 8c28323c6e95c130251b977ade37b3ffbae721af..850fe74e8494b44a309154ba04abe4dddc58a88f 100644 --- a/src/gurka.erl +++ b/src/gurka.erl @@ -1,16 +1,20 @@ --compile({no_auto_import, [apply/3]}). -module(gurka). --export([run/1, run/2]). +-include("gurka.hrl"). + +-export([run/1, run/2, run/3]). run(File) -> + run(File, []). + +run(File, Options) -> Module = list_to_atom("feature_" ++ filename:basename(File, ".feature")), - run(File, Module). + run(File, Module, Options). -run(File, Module) -> +run(File, Module, Options) -> case gurka_parser:parse(File) of {ok, Feature} -> - Result = run(Module, undefined, undefined, undefined, Feature), + Result = run(Module, Options, undefined, undefined, undefined, Feature), Passed = has_passed(Result), if Passed -> @@ -22,103 +26,122 @@ run(File, Module) -> {error, Reason} end. -run(Module, FeatureState, _ScenarioState, _PreviousPhase, []) -> - apply(Module, teardown_feature, [FeatureState]), +run(Module, Options, FeatureState, _ScenarioState, _PreviousPhase, []) -> + apply(Module, Options, teardown_feature, [FeatureState]), []; -run(DefaultModule, FeatureState, ScenarioState, PreviousPhase, [Step = {feature, start, Pattern, _RowNum} | Steps]) -> - Module = resolve_module(DefaultModule, Pattern), +run(DefaultModule, Options, FeatureState, ScenarioState, PreviousPhase, [Step = #step{phase = feature, action = start, tokens = Tokens} | Steps]) -> + Module = resolve_module(DefaultModule, Tokens), case PreviousPhase of feature -> - apply(Module, teardown_feature, [FeatureState]); + apply(Module, Options, teardown_feature, [FeatureState]); background -> - apply(Module, teardown_feature, [FeatureState]); + apply(Module, Options, teardown_feature, [FeatureState]); scenario -> - apply(Module, teardown_scenario, [ScenarioState]), - apply(Module, teardown_feature, [FeatureState]); + apply(Module, Options, teardown_scenario, [ScenarioState]), + apply(Module, Options, teardown_feature, [FeatureState]); _ -> ok end, - case apply(Module, setup_feature, [Pattern]) of + case apply(Module, Options, setup_feature, [Tokens]) of {ok, State} -> - [{ok, Step} | run(Module, State, ScenarioState, feature, Steps)]; - {error, undef} -> - [{ok, Step} | run(Module, undefined, ScenarioState, feature, Steps)]; + [{ok, Step} | run(Module, Options, State, ScenarioState, feature, Steps)]; + {error, undef, _Stack} -> + [{ok, Step} | run(Module, Options, undefined, ScenarioState, feature, Steps)]; Term -> [{Term, Step}] end; -run(Module, FeatureState, ScenarioState, _PreviousPhase, [Step = {background, given, Pattern, _RowNum} | Steps]) -> - case apply(Module, given, [Pattern, FeatureState]) of - ok -> - [{ok, Step} | run(Module, FeatureState, ScenarioState, background, Steps)]; - {ok, State} -> - [{ok, Step} | run(Module, State, ScenarioState, background, Steps)]; +run(Module, Options, FeatureState, ScenarioState, _PreviousPhase, [Step = #step{phase = background, action = given, tokens = Tokens} | Steps]) -> + case apply(Module, Options, given, [Tokens, FeatureState]) of + Flag when Flag == ok; Flag == true -> + [{ok, Step} | run(Module, Options, FeatureState, ScenarioState, background, Steps)]; + {Flag, State} when Flag == ok; Flag == true -> + [{ok, Step} | run(Module, Options, State, ScenarioState, background, Steps)]; Term -> + apply(Module, Options, teardown_feature, [FeatureState]), [{Term, Step}] end; -run(Module, FeatureState, ScenarioState, PreviousPhase, [Step = {scenario, start, Pattern, _RowNum} | Steps]) -> - case PreviousPhase of - scenario -> - apply(Module, teardown_scenario, [ScenarioState]); +run(Module, Options, FeatureState, ScenarioState, PreviousPhase, [Step = #step{phase = scenario, action = start, tokens = Tokens} | Steps]) -> + case do_tags_match(Options, Step) of + true -> + case PreviousPhase of + scenario -> + apply(Module, Options, teardown_scenario, [ScenarioState]); + _ -> + ok + end, + case apply(Module, Options, setup_scenario, [Tokens, FeatureState]) of + {ok, State} -> + [{ok, Step} | run(Module, Options, FeatureState, State, scenario, Steps)]; + {error, undef, _Stack} -> + [{ok, Step} | run(Module, Options, FeatureState, FeatureState, scenario, Steps)]; + Term -> + apply(Module, Options, teardown_feature, [FeatureState]), + [{Term, Step}] + end; _ -> - ok - end, - case apply(Module, setup_scenario, [Pattern, FeatureState]) of - {ok, State} -> - [{ok, Step} | run(Module, FeatureState, State, scenario, Steps)]; - {error, undef} -> - [{ok, Step} | run(Module, FeatureState, FeatureState, scenario, Steps)]; - Term -> - [{Term, Step}] + [{ok, Step#step{action = skip}} | skip_scenario(Module, Options, FeatureState, Steps)] end; -run(Module, FeatureState, ScenarioState, _PreviousPhase, [Step = {scenario, Action, Pattern, _RowNum} | Steps]) when Action == given; Action == 'when'; Action == then -> - case apply(Module, Action, [Pattern, ScenarioState]) of - ok -> - [{ok, Step} | run(Module, FeatureState, ScenarioState, scenario, Steps)]; - {ok, State} -> - [{ok, Step} | run(Module, FeatureState, State, scenario, Steps)]; +run(Module, Options, FeatureState, ScenarioState, _PreviousPhase, [Step = #step{phase = scenario, action = Action, tokens = Tokens} | Steps]) when Action == given; Action == 'when'; Action == then -> + case apply(Module, Options, Action, [Tokens, ScenarioState]) of + Flag when Flag == ok; Flag == true -> + [{ok, Step} | run(Module, Options, FeatureState, ScenarioState, scenario, Steps)]; + {Flag, State} when Flag == ok; Flag == true -> + [{ok, Step} | run(Module, Options, FeatureState, State, scenario, Steps)]; Term -> + apply(Module, Options, teardown_scenario, [ScenarioState]), + apply(Module, Options, teardown_feature, [FeatureState]), [{Term, Step}] end; -run(Module, _FeatureState, ScenarioState, _PreviousPhase, [{scenario_outline, 'end', _Pattern, _RowNum}]) -> - apply(Module, teardown_scenario, [ScenarioState]), +run(Module, Options, _FeatureState, ScenarioState, _PreviousPhase, [#step{phase = scenario_outline, action = 'end'}]) -> + apply(Module, Options, teardown_scenario, [ScenarioState]), []; -run(Module, FeatureState, ScenarioState, PreviousPhase, [{scenario_outline, start, Pattern, RowNum} | Steps]) -> +run(Module, Options, FeatureState, ScenarioState, PreviousPhase, [Step = #step{phase = scenario_outline, action = start} | Steps]) -> case PreviousPhase of scenario -> - apply(Module, teardown_scenario, [ScenarioState]); + apply(Module, Options, teardown_scenario, [ScenarioState]); _ -> ok end, - run_outline(Module, FeatureState, ScenarioState, PreviousPhase, Steps, [{start, Pattern, RowNum}]); + run_outline(Module, Options, FeatureState, ScenarioState, PreviousPhase, Steps, [Step]); -run(Module, FeatureState, ScenarioState, PreviousPhase, [{scenario_outline, Action, Pattern, RowNum} | Steps]) -> - run_outline(Module, FeatureState, ScenarioState, PreviousPhase, Steps, [{Action, Pattern, RowNum}]); +run(Module, Options, FeatureState, ScenarioState, PreviousPhase, [Step = #step{phase = scenario_outline} | Steps]) -> + run_outline(Module, Options, FeatureState, ScenarioState, PreviousPhase, Steps, [Step]); -run(Module, FeatureState, ScenarioState, _PreviousPhase, [Step = {Phase, _Action, _Pattern, _RowNum} | Steps]) -> - [{ok, Step} | run(Module, FeatureState, ScenarioState, Phase, Steps)]. +run(Module, Options, FeatureState, ScenarioState, _PreviousPhase, [Step = #step{phase = Phase} | Steps]) -> + [{ok, Step} | run(Module, Options, FeatureState, ScenarioState, Phase, Steps)]. -run_outline(Module, FeatureState, ScenarioState, _PreviousPhase, [{scenario_outline, examples, [Headers | Rows], _RowNum} | Steps], ScenarioOutline) -> +run_outline(Module, Options, FeatureState, ScenarioState, _PreviousPhase, [ExamplesStep = #step{phase = scenario_outline, action = examples, row = ExampleRow, tokens = [Headers | Rows]} | Steps], ScenarioOutline) -> TaggedHeaders = [<<"<", Header/binary, ">">> || Header <- Headers], - Results = lists:map(fun(Row) -> + {Results, _} = lists:mapfoldl(fun(Row, Count) -> Example = lists:zip(TaggedHeaders, Row), - Scenario = lists:foldl(fun({Action, Pattern, RowNum}, ScenarioAcc) -> - ReplacedPattern = lists:map(fun(Token) -> + Scenario = lists:foldl(fun(Step, ScenarioAcc) -> + Tokens = lists:map(fun(Token) -> lists:foldr(fun replace_tags/2, Token, Example) - end, Pattern), - [{scenario, Action, ReplacedPattern, RowNum} | ScenarioAcc] - end, [{scenario_outline, 'end', [], 0}], ScenarioOutline), - run(Module, FeatureState, ScenarioState, scenario_outline, Scenario) - end, Rows), - [Results | run(Module, FeatureState, ScenarioState, scenario_outline, Steps)]; - -run_outline(Module, FeatureState, ScenarioState, PreviousPhase, [{scenario_outline, Action, Pattern, RowNum} | Steps], ScenarioOutline) -> - run_outline(Module, FeatureState, ScenarioState, PreviousPhase, Steps, [{Action, Pattern, RowNum} | ScenarioOutline]). + end, Step#step.tokens), + case Step#step.action of + start -> + MergedTags = proplists:get_value(merged_tags, ExamplesStep#step.meta, []), + Meta = [{example_row, {ExampleRow, Count}} | [{merged_tags, MergedTags} | proplists:delete(merged_tags, Step#step.meta)]], + [Step#step{phase = scenario, tokens = Tokens, meta = Meta} | ScenarioAcc]; + _ -> + [Step#step{phase = scenario, tokens = Tokens} | ScenarioAcc] + end + end, [#step{phase = scenario_outline, action = 'end'}], ScenarioOutline), + {run(Module, Options, FeatureState, ScenarioState, scenario_outline, Scenario), Count + 1} + end, 1, Rows), + [Results | run_outline(Module, Options, FeatureState, ScenarioState, scenario_outline, Steps, ScenarioOutline)]; + +run_outline(Module, Options, FeatureState, ScenarioState, PreviousPhase, [Step = #step{phase = scenario_outline} | Steps], ScenarioOutline) -> + run_outline(Module, Options, FeatureState, ScenarioState, PreviousPhase, Steps, [Step | ScenarioOutline]); + +run_outline(Module, Options, FeatureState, ScenarioState, PreviousPhase, Steps, _ScenarioOutline) -> + run(Module, Options, FeatureState, ScenarioState, PreviousPhase, Steps). replace_tags({Pattern, Replacement}, {Term, Subject}) -> {Term, replace_tags({Pattern, Replacement}, Subject)}; @@ -132,7 +155,26 @@ replace_tags({Pattern, Replacement}, Subject) when is_list(Subject) -> replace_tags(_, Subject) -> Subject. -apply(Module, Function, Args) -> +do_tags_match(Options, Step) -> + OptionsTags = proplists:get_value(tags, Options, []), + StepTags = proplists:get_value(merged_tags, Step#step.meta, []), + Flag = lists:foldl( + fun(<<$!, OptionTag/binary>>, undefined) -> not lists:member(OptionTag, StepTags); + (<<$+, $!, OptionTag/binary>>, undefined) -> not lists:member(OptionTag, StepTags); + (<<$+, OptionTag/binary>>, undefined) -> lists:member(OptionTag, StepTags); + (true, undefined) -> true; + (OptionTag, undefined) -> lists:member(OptionTag, StepTags); + (<<$!, OptionTag/binary>>, AccIn) -> AccIn or (not lists:member(OptionTag, StepTags)); + (<<$+, OptionTag/binary>>, AccIn) -> AccIn and lists:member(OptionTag, StepTags); + (<<$+, $!, OptionTag/binary>>, AccIn) -> AccIn and (not lists:member(OptionTag, StepTags)); + (OptionTag, AccIn) -> AccIn or lists:member(OptionTag, StepTags) + end, undefined, OptionsTags), + if + Flag == undefined -> true; + true -> Flag + end. + +apply(Module, _Options, Function, Args) -> case catch erlang:apply(Module, Function, Args) of {'EXIT', {Reason, Stack}} -> {error, Reason, Stack}; @@ -164,3 +206,13 @@ resolve_module(_Module, [<<"(module:", Term/binary>> | Pattern]) -> resolve_module(Module, [_ | Pattern]) -> resolve_module(Module, Pattern). +skip_scenario(Module, Options, FeatureState, []) -> + apply(Module, Options, teardown_feature, [FeatureState]), + []; + +skip_scenario(Module, Options, FeatureState, Feature = [#step{action = start} | _Steps]) -> + run(Module, Options, FeatureState, undefined, undefined, Feature); + +skip_scenario(Module, Options, FeatureState, [_Step | Steps]) -> + skip_scenario(Module, Options, FeatureState, Steps). + diff --git a/src/gurka_eunit.erl b/src/gurka_eunit.erl new file mode 100644 index 0000000000000000000000000000000000000000..0b405d4cf1cceb7e536e4847a995635da86e3574 --- /dev/null +++ b/src/gurka_eunit.erl @@ -0,0 +1,66 @@ +-module(gurka_eunit). + +-include_lib("eunit/include/eunit.hrl"). + +-export([setup/1, setup/2, setup/3]). + +setup(Setup) -> + setup(Setup, fun(_) -> ok end). + +setup(Setup, Cleanup) -> + setup(Setup, Cleanup, fun(Message) -> io:fwrite(user, "~s~n", [Message]) end). + +setup(Features, Cleanup, Log) when is_list(Features) -> + setup(fun() -> Features end, Cleanup, Log); + +setup(Setup, Cleanup, Log) -> + {setup, spawn, Setup, Cleanup, fun(Files) -> features(Log, Files) end}. + +features(Log, Files) -> + Timeout = parse_timeout(), + {inorder, [{timeout, Timeout, fun() -> feature(Log, File) end} || File <- Files]}. + +feature(Log, File) -> + erlang:group_leader(erlang:whereis(user), self()), + Tags = parse_tags(), + Formatter = parse_format(), + Options = [{file, File}, {tags, Tags}, {formatter, Formatter}, {log, Log}], + Result = gurka:run(File, Options), + Log(io_lib:format("~s", [Formatter:format(Result, Options)])), + case Result of + {ok, _} -> + ok; + {fail, _} -> + erlang:error({failed, File}) + end. + +parse_timeout() -> + case os:getenv("TIMEOUT") of + false -> + 60; + Timeout -> + list_to_number(Timeout) + end. + +parse_tags() -> + case os:getenv("TAGS") of + false -> + []; + Tags -> + [list_to_binary(Tag) || Tag <- string:tokens(Tags, ", ")] + end. + +parse_format() -> + case os:getenv("FORMAT") of + false -> + gurka_formatter_compact; + Format -> + list_to_atom("gurka_formatter_" ++ Format) + end. + +list_to_number(L) -> + try list_to_float(L) + catch + error:badarg -> + list_to_integer(L) + end. \ No newline at end of file diff --git a/src/gurka_formatter_compact.erl b/src/gurka_formatter_compact.erl new file mode 100644 index 0000000000000000000000000000000000000000..8e419ee6d5094735dd3741b5684919b51d204250 --- /dev/null +++ b/src/gurka_formatter_compact.erl @@ -0,0 +1,27 @@ +-module(gurka_formatter_compact). + +-include("gurka.hrl"). + +-export([format/2]). + +format({Status, Result}, Opts) -> + case proplists:get_value(file, Opts) of + undefined -> + format_steps(Result); + File when Status == ok -> + io_lib:format("\e[1;32m~s ~s\e[0m~n", ["PASS", File]) ++ format_steps(Result); + File -> + io_lib:format("\e[1;31m~s ~s\e[0m~n", ["FAIL", File]) ++ format_steps(Result) + end. + +format_steps([]) -> + []; + +format_steps([Step | Steps]) when is_list(Step) -> + format_steps(Step) ++ format_steps(Steps); + +format_steps([{ok, _} | Steps]) -> + format_steps(Steps); + +format_steps([{Result, #step{row = Row}} | Steps]) -> + io_lib:format("\e[0;31m~B: ~p\e[0m~n", [Row, Result]) ++ format_steps(Steps). diff --git a/src/gurka_formatter_plain.erl b/src/gurka_formatter_plain.erl index 19094d09c0eaed93c417c9db2372da046c70b453..c24d8b655e883fa527a52ea8178610410bd8d7c5 100644 --- a/src/gurka_formatter_plain.erl +++ b/src/gurka_formatter_plain.erl @@ -1,12 +1,30 @@ -module(gurka_formatter_plain). --export([format/1, format/2]). - -format(Result) -> - format(Result, []). - -format(Result, _Opts) -> - format_steps(Result, undefined). +-include("gurka.hrl"). + +-export([format/2]). + +format({Status, Result}, Opts) -> + format_header(Status, Opts) ++ format_steps(Result, undefined). + +format_header(Status, Opts) -> + H1 = case proplists:get_value(file, Opts) of + undefined -> + []; + File when Status == ok -> + io_lib:format("File: ~s~s~n", [string:left(File, 70, $\s), "PASS"]); + File -> + io_lib:format("File: ~s~s~n", [string:left(File, 70, $\s), "FAIL"]) + end, + H2 = case proplists:get_value(tags, Opts) of + undefined -> + []; + [] -> + []; + Tags -> + io_lib:format("Tags: ~s~n", [string:join([binary_to_list(Tag) || Tag <- Tags],",")]) + end, + H1 ++ H2. format_steps([], _LastAction) -> []; @@ -18,16 +36,19 @@ format_steps([Step | Steps], LastAction) -> {FormattedStep, NewLastAction} = format_step(Step, LastAction), FormattedStep ++ format_steps(Steps, NewLastAction). -format_step({ok, {Phase, Action, Pattern, RowNum}}, LastAction) -> +format_step({ok, #step{phase = Phase, action = Action, tokens = Pattern, row = RowNum}}, LastAction) -> {format_pattern(Pattern, RowNum, format_phase(Phase, Action, LastAction)), Action}; -format_step({Result, {Phase, Action, Pattern, RowNum}}, LastAction) -> +format_step({Result, #step{phase = Phase, action = Action, tokens = Pattern, row = RowNum}}, LastAction) -> FormattedPattern = format_pattern(Pattern, RowNum, format_phase(Phase, Action, LastAction)), FormattedFailure = io_lib:format("~s FAILED: ~60p~n", [FormattedPattern, Result]), {FormattedFailure, Action}. +format_phase(feature, skip, _LastAction) -> + "\nSkipped Feature: "; + format_phase(feature, start, _LastAction) -> - "Feature: "; + "\nFeature: "; format_phase(feature, desc, _LastAction) -> " "; @@ -35,6 +56,9 @@ format_phase(feature, desc, _LastAction) -> format_phase(background, start, _LastAction) -> "\n Background: "; +format_phase(scenario, skip, _LastAction) -> + "\n Skipped Scenario: "; + format_phase(scenario, start, _LastAction) -> "\n Scenario: "; diff --git a/src/gurka_parser.erl b/src/gurka_parser.erl index ceae568fbaa04a8fb3c1bab91f7a2756e1923e5d..2d0c9a4db6e3c524e15bd7940636d68391759f68 100644 --- a/src/gurka_parser.erl +++ b/src/gurka_parser.erl @@ -1,16 +1,8 @@ -module(gurka_parser). --export([parse/1, tokens/1]). - --export_type([phase/0, action/0, step/0, feature/0]). +-include("gurka.hrl"). --type phase() :: feature | background | scenario | scenario_outline. --type action() :: desc | title | given | 'when' | then | examples | headers | values. --type pattern() :: [binary()]. --type row() :: pos_integer(). --type step() :: {phase(), action(), pattern(), row()}. --type feature() :: [step()]. --type lines() :: [{row(), binary(), pattern()}]. +-export([parse/1, tokens/1]). -spec parse(File) -> {ok, Feature} | {error, Reason} when File :: file:name(), @@ -22,7 +14,7 @@ parse(File) -> {_, Lines} = lists:foldl(fun(Line, {Row, AccIn}) -> {Row + 1, [{Row, Line, tokens(Line)} | AccIn]} end, {1, []}, binary:split(Binary, [<<$\n>>, <<$\r>>], [global, trim])), - {ok, compact(process(undefined, undefined, lists:reverse(Lines)))}; + {ok, inherit_tags(compact(process(undefined, undefined, lists:reverse(Lines))))}; {error, Reason} -> {error, Reason} end. @@ -36,54 +28,82 @@ process(_Phase, _Action, []) -> []; process(Phase, Action, [{_Row, _Line, []} | Lines]) -> process(Phase, Action, Lines); +process(Phase, Action, [{Row, _Line, Tags = [<<$@, _/binary>> | _]} | Lines]) -> + [#step{phase = Phase, action = tags, tokens = Tags, row = Row} | process(Phase, Action, Lines)]; process(Phase, Action, [{Row, Line, [<<"|">> | Tokens]} | Lines]) -> {Lines2, Table} = build_table([{Row, Line, [<<"|">> | Tokens]} | Lines], []), - [{Phase, table, Table, Row} | process(Phase, Action, Lines2)]; + [#step{phase = Phase, action = table, tokens = Table, row = Row} | process(Phase, Action, Lines2)]; process(Phase, Action, [{Row, _Line, [<<"\"\"\"">> | _Tokens]} | Lines]) -> {Lines2, Docstring} = build_docstring(Lines, []), - [{Phase, docstring, Docstring, Row} | process(Phase, Action, Lines2)]; + [#step{phase = Phase, action = docstring, tokens = Docstring, row = Row} | process(Phase, Action, Lines2)]; process(Phase, Action, [{Row, _Line, [FirstToken | Tokens]} | Lines]) -> case normalize_token(FirstToken) of "and" -> - [{Phase, Action, Tokens, Row} | process(Phase, Action, Lines)]; + [#step{phase = Phase, action = Action, tokens = Tokens, row = Row} | process(Phase, Action, Lines)]; "but" -> - [{Phase, Action, Tokens, Row} | process(Phase, Action, Lines)]; + [#step{phase = Phase, action = Action, tokens = Tokens, row = Row} | process(Phase, Action, Lines)]; "given" -> - [{Phase, given, Tokens, Row} | process(Phase, given, Lines)]; + [#step{phase = Phase, action = given, tokens = Tokens, row = Row} | process(Phase, given, Lines)]; "when" -> - [{Phase, 'when', Tokens, Row} | process(Phase, 'when', Lines)]; + [#step{phase = Phase, action = 'when', tokens = Tokens, row = Row} | process(Phase, 'when', Lines)]; "then" -> - [{Phase, then, Tokens, Row} | process(Phase, then, Lines)]; + [#step{phase = Phase, action = then, tokens = Tokens, row = Row} | process(Phase, then, Lines)]; "examples" -> - [{Phase, examples, Tokens, Row} | process(Phase, examples, Lines)]; + [#step{phase = Phase, action = examples, tokens = Tokens, row = Row} | process(Phase, examples, Lines)]; "feature" -> - [{feature, start, Tokens, Row} | process(feature, undefined, Lines)]; + [#step{phase = feature, action = start, tokens = Tokens, row = Row} | process(feature, undefined, Lines)]; "background" -> - [{background, start, Tokens, Row} | process(background, undefined, Lines)]; + [#step{phase = background, action = start, tokens = Tokens, row = Row} | process(background, undefined, Lines)]; "scenario" -> case normalize_token(hd(Tokens)) of "outline" -> - [{scenario_outline, start, tl(Tokens), Row} | process(scenario_outline, undefined, Lines)]; + [#step{phase = scenario_outline, action = start, tokens = tl(Tokens), row = Row} | process(scenario_outline, undefined, Lines)]; _ -> - [{scenario, start, Tokens, Row} | process(scenario, undefined, Lines)] + [#step{phase = scenario, action = start, tokens = Tokens, row = Row} | process(scenario, undefined, Lines)] end; _ -> - [{Phase, desc, [FirstToken | Tokens], Row} | process(Phase, Action, Lines)] + [#step{phase = Phase, action = desc, tokens = [FirstToken | Tokens], row = Row} | process(Phase, Action, Lines)] end. -compact(Lines) -> - compact(lists:reverse(Lines), []). +compact(Feature) -> + compact(lists:reverse(Feature), []). compact([], Acc) -> Acc; -compact([{_, docstring, Docstring, _} | [{Phase, Action, Tokens, Row} | Lines]], Acc) -> - compact(Lines, [{Phase, Action, Tokens ++ [{docstring, Docstring}], Row} | Acc]); -compact([{_, table, Table, _} | [{Phase, examples, _Tokens, Row} | Lines]], Acc) -> - compact(Lines, [{Phase, examples, Table, Row} | Acc]); -compact([{_, table, Table, _} | [{Phase, Action, Tokens, Row} | Lines]], Acc) -> - compact(Lines, [{Phase, Action, Tokens ++ [{table, Table}], Row} | Acc]); -compact([Line | Lines], Acc) -> - compact(Lines, [Line | Acc]). +compact([#step{action = docstring, tokens = Docstring} | [Step | Steps]], Acc) -> + compact([Step#step{tokens = Step#step.tokens ++ [{docstring, Docstring}]} | Steps], Acc); +compact([#step{action = table, tokens = Table} | [Step = #step{action = examples} | Steps]], Acc) -> + compact([Step#step{tokens = Table} | Steps], Acc); +compact([#step{action = table, tokens = Table} | [Step | Steps]], Acc) -> + compact([Step#step{tokens = Step#step.tokens ++ [{table, Table}]} | Steps], Acc); +compact([Step = #step{phase = Phase, action = start} | [#step{action = tags, tokens = Tags} | Steps]], Acc) when Phase == feature; Phase == scenario; Phase == scenario_outline -> + compact([Step#step{meta = [{tags, [Tag || <<$@, Tag/binary>> <- Tags]}]} | Steps], Acc); +compact([Step = #step{phase = scenario_outline, action = examples} | [#step{action = tags, tokens = Tags} | Steps]], Acc) -> + compact([Step#step{meta = [{tags, [Tag || <<$@, Tag/binary>> <- Tags]}]} | Steps], Acc); +compact([#step{action = tags} | Steps], Acc) -> + compact(Steps, Acc); +compact([Step | Steps], Acc) -> + compact(Steps, [Step | Acc]). + +inherit_tags(Feature) -> + inherit_tags(Feature, {[], []}). + +inherit_tags([], _) -> + []; +inherit_tags([Step = #step{phase = feature, action = start} | Steps], _) -> + FeatureTags = proplists:get_value(tags, Step#step.meta, []), + NewMeta = [{merged_tags, FeatureTags} | Step#step.meta], + [Step#step{meta = NewMeta} | inherit_tags(Steps, {FeatureTags, []})]; +inherit_tags([Step = #step{phase = Phase, action = start} | Steps], {FeatureTags, _}) when Phase == scenario; Phase == scenario_outline -> + ScenarioTags = proplists:get_value(tags, Step#step.meta, []), + NewMeta = [{merged_tags, FeatureTags ++ ScenarioTags} | Step#step.meta], + [Step#step{meta = NewMeta} | inherit_tags(Steps, {FeatureTags, ScenarioTags})]; +inherit_tags([Step = #step{phase = scenario_outline, action = examples} | Steps], {FeatureTags, ScenarioTags}) -> + ExamplesTags = proplists:get_value(tags, Step#step.meta, []), + NewMeta = [{merged_tags, FeatureTags ++ ScenarioTags ++ ExamplesTags} | Step#step.meta], + [Step#step{meta = NewMeta} | inherit_tags(Steps, {FeatureTags, ScenarioTags})]; +inherit_tags([Step | Steps], Tags) -> + [Step | inherit_tags(Steps, Tags)]. build_table([], Table) -> {[], lists:reverse(Table)}; diff --git a/src/gurka_transform.erl b/src/gurka_transform.erl index 32b3aa6aed1e6321fae82dab96d16fa39eae6db6..48c585cb9f68657d3763d94324adede1d56618ad 100644 --- a/src/gurka_transform.erl +++ b/src/gurka_transform.erl @@ -43,7 +43,9 @@ clause(Clause) -> process_string(Row, String) -> Tokens = gurka_parser:tokens(list_to_binary(String)), Pattern = build_pattern(Row, Tokens), - Pattern. + {match, Row, + {var, Row, 'Tokens'}, + Pattern}. build_pattern(Row, []) -> {nil, Row}; diff --git a/test/feature_parser.erl b/test/feature_parser.erl index 86a7e961451acf36fa48e96600463ee562c35646..5758095d04a9031830796b6517251955ed3e1024 100644 --- a/test/feature_parser.erl +++ b/test/feature_parser.erl @@ -1,6 +1,8 @@ -compile({parse_transform, gurka_transform}). -module(feature_parser). +-include("gurka.hrl"). + -export([setup_feature/1, setup_scenario/2, teardown_feature/1, teardown_scenario/1, given/2, 'when'/2, then/2]). -record(state, {feature_dir, feature}). @@ -17,6 +19,7 @@ setup_scenario(_Tokens, State) -> teardown_scenario(_State) -> ok. +%% noinspection ErlangUnboundVariable given("feature files are loaded from $Directory", State) -> case filelib:is_dir(Directory) of true -> {ok, State#state{feature_dir = Directory}}; @@ -35,15 +38,17 @@ given(Tokens, State) -> io:format("given ~p~n", [Tokens]), {ok, State}. +%% noinspection ErlangUnboundVariable 'when'(Tokens, State) -> io:format("when ~p~n", [Tokens]), {ok, State}. +%% noinspection ErlangUnboundVariable then("the parsed result should have $StepCount $StepType steps", State) -> ExpectedCount = list_to_integer(binary_to_list(StepCount)), Type = binary_to_atom(StepType, utf8), RealCount = lists:foldl( - fun({_, Action, _, _}, Sum) when Action == Type -> Sum + 1; + fun(Step, Sum) when Step#step.action == Type -> Sum + 1; (_, Sum) -> Sum end, 0, State#state.feature), if diff --git a/test/features.erl b/test/features.erl index 689cbe2c469568700813491324d57314eb390d14..96ee999d58976f0bea296bd3a455832e388d8ae0 100644 --- a/test/features.erl +++ b/test/features.erl @@ -3,29 +3,8 @@ -include_lib("eunit/include/eunit.hrl"). gurka_test_() -> - {setup, - spawn, - fun setup/0, - fun teardown/1, - fun features/1}. - -setup() -> - file:set_cwd(".."), - filelib:fold_files("features", ".*[.]feature", true, fun(File, Files) -> [File | Files] end, []). - -teardown(_Files) -> - ok. - -features(Files) -> - {inorder, [{timeout, 60, ?_test(feature(File))} || File <- Files]}. - -feature(File) -> - erlang:group_leader(erlang:whereis(user), self()), - case gurka:run(File) of - {ok, Result} -> - io:format("File: ~sOK~n~s~n", [string:left(File, 72, $\s), gurka_formatter_plain:format(Result)]); - {fail, Result} -> - io:format("File: ~sFAIL~n~s~n", [string:left(File, 70, $\s), gurka_formatter_plain:format(Result)]), - erlang:error({failed, File}) - end. - + Features = fun() -> + file:set_cwd(".."), + filelib:fold_files("features", ".*[.]feature", true, fun(File, Files) -> [File | Files] end, []) + end, + gurka_eunit:setup(Features).