-module(gurka). -include("gurka.hrl"). -export([run/1, run/2, run/3]). -spec run(File) -> {ok, Result} | {fail, Result} | {error, Reason} when File :: file:name_all(), Result :: result(), Reason :: file:posix() | badarg | terminated | system_limit. run(File) -> run(File, []). -spec run(File, Options) -> {ok, Result} | {fail, Result} | {error, Reason} when File :: file:name_all(), Options :: [term()], Result :: result(), Reason :: file:posix() | badarg | terminated | system_limit. run(File, Options) -> Module = list_to_atom("feature_" ++ filename:basename(File, ".feature")), run(File, Module, Options). -spec run(File, Module, Options) -> {ok, Result} | {fail, Result} | {error, Reason} when File :: file:name_all(), Module :: module(), Options :: [term()], Result :: result(), Reason :: file:posix() | badarg | terminated | system_limit. run(File, Module, Options) -> case gurka_parser:parse(File) of {ok, Feature} -> Result = run(Module, Options, undefined, undefined, undefined, Feature), Passed = has_passed(Result), if Passed -> {ok, Result}; true -> {fail, Result} end; {error, Reason} -> {error, Reason} end. run(Module, Options, FeatureState, _ScenarioState, _PreviousPhase, []) -> apply(Module, Options, teardown_feature, [FeatureState]), []; 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, Options, teardown_feature, [FeatureState]); background -> apply(Module, Options, teardown_feature, [FeatureState]); scenario -> apply(Module, Options, teardown_scenario, [ScenarioState]), apply(Module, Options, teardown_feature, [FeatureState]); _ -> ok end, case apply(Module, Options, setup_feature, [Tokens]) of {ok, State} -> [{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, 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, 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, Step#step{action = skip}} | skip_scenario(Module, Options, FeatureState, Steps)] end; 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, Options, _FeatureState, ScenarioState, _PreviousPhase, [#step{phase = scenario_outline, action = 'end'}]) -> apply(Module, Options, teardown_scenario, [ScenarioState]), []; run(Module, Options, FeatureState, ScenarioState, PreviousPhase, [Step = #step{phase = scenario_outline, action = start} | Steps]) -> case PreviousPhase of scenario -> apply(Module, Options, teardown_scenario, [ScenarioState]); _ -> ok end, run_outline(Module, Options, FeatureState, ScenarioState, PreviousPhase, Steps, [Step]); run(Module, Options, FeatureState, ScenarioState, PreviousPhase, [Step = #step{phase = scenario_outline} | Steps]) -> run_outline(Module, Options, FeatureState, ScenarioState, PreviousPhase, Steps, [Step]); run(Module, Options, FeatureState, ScenarioState, _PreviousPhase, [Step = #step{phase = Phase} | Steps]) -> [{ok, Step} | run(Module, Options, FeatureState, ScenarioState, Phase, Steps)]. 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, is_binary(Header)], {Results, _} = lists:mapfoldl(fun(Row, Count) -> Example = lists:zip(TaggedHeaders, Row), Scenario = lists:foldl(fun(Step, ScenarioAcc) -> Tokens = lists:map(fun(Token) -> lists:foldr(fun replace_tags/2, Token, Example) 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)}; replace_tags({Pattern, Replacement}, Subject) when is_binary(Subject) -> binary:replace(Subject, Pattern, Replacement, [global]); replace_tags({Pattern, Replacement}, Subject) when is_list(Subject) -> [replace_tags({Pattern, Replacement}, Token) || Token <- Subject]; replace_tags(_, Subject) -> Subject. 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}; {'EXIT', Term} -> {error, Term}; Term -> Term end. has_passed([]) -> true; has_passed([Step | Steps]) when is_list(Step) -> has_passed(Step) and has_passed(Steps); has_passed([{ok, _} | Steps]) -> has_passed(Steps); has_passed(_) -> false. resolve_module(Module, []) -> Module; resolve_module(_Module, [<<"(module:", Term/binary>> | Pattern]) -> NewModule = hd(binary:split(Term, <<")">>)), resolve_module(binary_to_atom(NewModule, utf8), 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).