diff --git a/.gitignore b/.gitignore index b89b9a284b123a821fea7bdf4b7ad1868d7a99f4..6f7b6735c3dccd859e1a609d3be2063f18bc4458 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .eunit +ebin deps *.o *.beam diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..f6c9e009f02e71cd57db6dafa993d939b841f045 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: erlang +otp_release: + - R16B + - R15B03 diff --git a/README.md b/README.md index d52c33d77ec4f515a08ca56ea3e32c20bc508277..d9aa303f3fb8a2aa01ecc93da230efbb404e615c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[](https://waffle.io/ehedenst/gurka) + +[](https://travis-ci.org/ehedenst/gurka) + gurka ===== diff --git a/features/parser.feature b/features/parser.feature new file mode 100644 index 0000000000000000000000000000000000000000..70c7475534c08957cf0038ec9a3660b71c247579 --- /dev/null +++ b/features/parser.feature @@ -0,0 +1,17 @@ +Feature: Gurka parser + In order to run tests + Gurka should be able to + parse feature files + + Background: + Given feature files are loaded from samples + + Scenario Outline: Parse feature file + Given I parse the feature file <File> + Then the parsed result should have <Given> given steps + And the parsed result should have <When> when steps + And the parsed result should have <Then> then steps + + Examples: + | File | Given | When | Then | + | sample1.feature | 5 | 3 | 3 | diff --git a/rebar b/rebar new file mode 100755 index 0000000000000000000000000000000000000000..9bc9bd14eb3cae28559586bb78bcdd61b78b90ed Binary files /dev/null and b/rebar differ diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000000000000000000000000000000000000..727a0b01239d86d951513a8451835e4d8729ef7e --- /dev/null +++ b/rebar.config @@ -0,0 +1,3 @@ +{erl_opts, [debug_info, {d, debug}]}. +{eunit_opts, [{report, {eunit_surefire, [{dir, ".eunit"}]}}]}. +{cover_enabled, true}. \ No newline at end of file diff --git a/samples/sample1.feature b/samples/sample1.feature new file mode 100644 index 0000000000000000000000000000000000000000..1f142b80ffbc9c8a473aaae6da54b93b1dbf20eb --- /dev/null +++ b/samples/sample1.feature @@ -0,0 +1,35 @@ +Feature: Test + In order to provide shorter URLs + As a visitor + I want to redirected to the correct index page + + Background: + Given flow is started with etc/flow-test.config + And I am using chrome + And http server is ready + + Scenario: URL without a trailing slash + When I open http://localhost:7070 + And I wait for 1 seconds + Then the browser URL should be http://localhost:7070/index.md + But the browser URL should be http://localhost:7070/index.md + + Scenario Outline: Email confirmation + Given I have a user account with my name "Jojo Binks" + And the following users exist: + | name | email | phone | + | Aslak | aslak@email.com | 123 | + | Matt | matt@email.com | 234 | + | Joe <Role> | joe@email.org | 456 | + When an Admin grants me <Role> rights + Then I should receive an email with the body: + """ + Dear Jojo Binks, + You have been granted <Role> rights. You are <details>. Please be responsible. + -The Admins + """ + + Examples: + | Role | details | + | Manager | now able to manage your employee accounts | + | Admin | able to manage any user account on the system | diff --git a/src/gurka.app.src b/src/gurka.app.src new file mode 100644 index 0000000000000000000000000000000000000000..00994ad045f68eb67b39b3d4e8763dc2a59561a8 --- /dev/null +++ b/src/gurka.app.src @@ -0,0 +1,11 @@ +{application, gurka, + [ + {description, "Erlang implementation of Cucumber"}, + {vsn, "0.0.1"}, + {applications, [ + kernel, + stdlib + ]}, + {registered, []}, + {env, []} + ]}. diff --git a/src/gurka.erl b/src/gurka.erl new file mode 100644 index 0000000000000000000000000000000000000000..8c28323c6e95c130251b977ade37b3ffbae721af --- /dev/null +++ b/src/gurka.erl @@ -0,0 +1,166 @@ +-compile({no_auto_import, [apply/3]}). +-module(gurka). + +-export([run/1, run/2]). + +run(File) -> + Module = list_to_atom("feature_" ++ filename:basename(File, ".feature")), + run(File, Module). + +run(File, Module) -> + case gurka_parser:parse(File) of + {ok, Feature} -> + Result = run(Module, undefined, undefined, undefined, Feature), + Passed = has_passed(Result), + if + Passed -> + {ok, Result}; + true -> + {fail, Result} + end; + {error, Reason} -> + {error, Reason} + end. + +run(Module, FeatureState, _ScenarioState, _PreviousPhase, []) -> + apply(Module, teardown_feature, [FeatureState]), + []; + +run(DefaultModule, FeatureState, ScenarioState, PreviousPhase, [Step = {feature, start, Pattern, _RowNum} | Steps]) -> + Module = resolve_module(DefaultModule, Pattern), + case PreviousPhase of + feature -> + apply(Module, teardown_feature, [FeatureState]); + background -> + apply(Module, teardown_feature, [FeatureState]); + scenario -> + apply(Module, teardown_scenario, [ScenarioState]), + apply(Module, teardown_feature, [FeatureState]); + _ -> + ok + end, + case apply(Module, setup_feature, [Pattern]) of + {ok, State} -> + [{ok, Step} | run(Module, State, ScenarioState, feature, Steps)]; + {error, undef} -> + [{ok, Step} | run(Module, 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)]; + Term -> + [{Term, Step}] + end; + +run(Module, FeatureState, ScenarioState, PreviousPhase, [Step = {scenario, start, Pattern, _RowNum} | Steps]) -> + case PreviousPhase of + scenario -> + apply(Module, teardown_scenario, [ScenarioState]); + _ -> + 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}] + 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)]; + Term -> + [{Term, Step}] + end; + +run(Module, _FeatureState, ScenarioState, _PreviousPhase, [{scenario_outline, 'end', _Pattern, _RowNum}]) -> + apply(Module, teardown_scenario, [ScenarioState]), + []; + +run(Module, FeatureState, ScenarioState, PreviousPhase, [{scenario_outline, start, Pattern, RowNum} | Steps]) -> + case PreviousPhase of + scenario -> + apply(Module, teardown_scenario, [ScenarioState]); + _ -> + ok + end, + run_outline(Module, FeatureState, ScenarioState, PreviousPhase, Steps, [{start, Pattern, RowNum}]); + +run(Module, FeatureState, ScenarioState, PreviousPhase, [{scenario_outline, Action, Pattern, RowNum} | Steps]) -> + run_outline(Module, FeatureState, ScenarioState, PreviousPhase, Steps, [{Action, Pattern, RowNum}]); + +run(Module, FeatureState, ScenarioState, _PreviousPhase, [Step = {Phase, _Action, _Pattern, _RowNum} | Steps]) -> + [{ok, Step} | run(Module, FeatureState, ScenarioState, Phase, Steps)]. + +run_outline(Module, FeatureState, ScenarioState, _PreviousPhase, [{scenario_outline, examples, [Headers | Rows], _RowNum} | Steps], ScenarioOutline) -> + TaggedHeaders = [<<"<", Header/binary, ">">> || Header <- Headers], + Results = lists:map(fun(Row) -> + Example = lists:zip(TaggedHeaders, Row), + Scenario = lists:foldl(fun({Action, Pattern, RowNum}, ScenarioAcc) -> + ReplacedPattern = 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]). + +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. + +apply(Module, 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). + diff --git a/src/gurka_formatter_plain.erl b/src/gurka_formatter_plain.erl new file mode 100644 index 0000000000000000000000000000000000000000..19094d09c0eaed93c417c9db2372da046c70b453 --- /dev/null +++ b/src/gurka_formatter_plain.erl @@ -0,0 +1,82 @@ +-module(gurka_formatter_plain). + +-export([format/1, format/2]). + +format(Result) -> + format(Result, []). + +format(Result, _Opts) -> + format_steps(Result, undefined). + +format_steps([], _LastAction) -> + []; + +format_steps([Step | Steps], LastAction) when is_list(Step) -> + format_steps(Step, LastAction) ++ format_steps(Steps, LastAction); + +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_pattern(Pattern, RowNum, format_phase(Phase, Action, LastAction)), Action}; + +format_step({Result, {Phase, Action, Pattern, 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, start, _LastAction) -> + "Feature: "; + +format_phase(feature, desc, _LastAction) -> + " "; + +format_phase(background, start, _LastAction) -> + "\n Background: "; + +format_phase(scenario, start, _LastAction) -> + "\n Scenario: "; + +format_phase(_Phase, Action, Action) -> + " And "; + +format_phase(_Phase, given, _LastAction) -> + " Given "; + +format_phase(_Phase, 'when', _LastAction) -> + " When "; + +format_phase(_Phase, 'then', _LastAction) -> + " Then "; + +format_phase(Phase, Action, _LastAction) -> + io_lib:format(" ~p:~p ", [Phase, Action]). + +format_pattern([], RowNum, [$\n | Acc]) -> + io_lib:format("~n~s~4B~n", [unicode:characters_to_binary(string:left(Acc, 76, $\s)), RowNum]); + +format_pattern([], RowNum, Acc) -> + io_lib:format("~s~4B~n", [unicode:characters_to_binary(string:left(Acc, 76, $\s)), RowNum]); + +format_pattern([{table, Table} | Pattern], RowNum, Acc) -> + Sizes = [[length(unicode:characters_to_list(Col, utf8)) || Col <- Row] || Row <- Table], + Max = lists:foldl(fun(Row, MaxAcc) -> + lists:zipwith(fun(X, Y) -> max(X, Y) end, Row, MaxAcc) + end, hd(Sizes), tl(Sizes)), + FTable = [ + " " ++ + lists:zipwith(fun(V, M) -> + "| " ++ string:left(binary_to_list(V), M + 1, $\s) + end, Row, Max) ++ "|\n" || Row <- Table + ], + format_pattern(Pattern, RowNum, Acc) ++ io_lib:format("~s", [FTable]); + +format_pattern([{docstring, Docstring} | Pattern], RowNum, Acc) -> + IndentedDocString = binary:replace(Docstring, <<"\n">>, <<"\n ">>, [global]), + format_pattern(Pattern, RowNum, Acc) ++ io_lib:format(" \"\"\"~n ~s~n \"\"\"~n", [IndentedDocString]); + +format_pattern([Term | Pattern], RowNum, Acc) -> + format_pattern(Pattern, RowNum, Acc ++ unicode:characters_to_list(Term, utf8) ++ " "). + + diff --git a/src/gurka_parser.erl b/src/gurka_parser.erl new file mode 100644 index 0000000000000000000000000000000000000000..ceae568fbaa04a8fb3c1bab91f7a2756e1923e5d --- /dev/null +++ b/src/gurka_parser.erl @@ -0,0 +1,144 @@ +-module(gurka_parser). + +-export([parse/1, tokens/1]). + +-export_type([phase/0, action/0, step/0, feature/0]). + +-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()}]. + +-spec parse(File) -> {ok, Feature} | {error, Reason} when + File :: file:name(), + Feature :: feature(), + Reason :: file:posix() | badarg | terminated | system_limit. +parse(File) -> + case file:read_file(File) of + {ok, Binary} -> + {_, 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)))}; + {error, Reason} -> + {error, Reason} + end. + +-spec process(Phase, Action, Lines) -> Feature when + Phase :: phase(), + Action :: action(), + Lines :: lines(), + Feature :: feature(). +process(_Phase, _Action, []) -> + []; +process(Phase, Action, [{_Row, _Line, []} | Lines]) -> + 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)]; +process(Phase, Action, [{Row, _Line, [<<"\"\"\"">> | _Tokens]} | Lines]) -> + {Lines2, Docstring} = build_docstring(Lines, []), + [{Phase, docstring, Docstring, 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)]; + "but" -> + [{Phase, Action, Tokens, Row} | process(Phase, Action, Lines)]; + "given" -> + [{Phase, given, Tokens, Row} | process(Phase, given, Lines)]; + "when" -> + [{Phase, 'when', Tokens, Row} | process(Phase, 'when', Lines)]; + "then" -> + [{Phase, then, Tokens, Row} | process(Phase, then, Lines)]; + "examples" -> + [{Phase, examples, Tokens, Row} | process(Phase, examples, Lines)]; + "feature" -> + [{feature, start, Tokens, Row} | process(feature, undefined, Lines)]; + "background" -> + [{background, start, Tokens, Row} | process(background, undefined, Lines)]; + "scenario" -> + case normalize_token(hd(Tokens)) of + "outline" -> + [{scenario_outline, start, tl(Tokens), Row} | process(scenario_outline, undefined, Lines)]; + _ -> + [{scenario, start, Tokens, Row} | process(scenario, undefined, Lines)] + end; + _ -> + [{Phase, desc, [FirstToken | Tokens], Row} | process(Phase, Action, Lines)] + end. + +compact(Lines) -> + compact(lists:reverse(Lines), []). + +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]). + +build_table([], Table) -> + {[], lists:reverse(Table)}; +build_table([{_Row, Line, [<<"|">> | _Tokens]} | Lines], Table) -> + Cells = [list_to_binary(string:strip(Field)) || Field <- string:tokens(string:strip(binary_to_list(Line)), "|")], + build_table(Lines, [Cells | Table]); +build_table(Lines, Table) -> + {Lines, lists:reverse(Table)}. + +build_docstring([], Docstring) -> + {[], Docstring}; +build_docstring([{_Row, _Line, [<<"\"\"\"">> | _Tokens]} | Lines], Docstring) -> + {Lines, list_to_binary(Docstring)}; +build_docstring([{_Row, Line, _Tokens} | Lines], []) -> + build_docstring(Lines, string:strip(binary_to_list(Line))); +build_docstring([{_Row, Line, _Tokens} | Lines], Docstring) -> + build_docstring(Lines, Docstring ++ "\n" ++ string:strip(binary_to_list(Line))). + +normalize_token(Token) -> + hd(string:tokens(string:to_lower(binary_to_list(Token)), ":")). + +tokens(Line) -> + [strip_quotes(Token) || Token <- tokens(Line, [], <<>>)]. + +tokens(<<>>, Tokens, <<>>) -> + Tokens; +tokens(<<>>, [<<$", $">>], <<$">>) -> + [<<$", $", $">>]; +tokens(<<>>, Tokens, Buffer) -> + Tokens ++ [Buffer]; +tokens(<<$\s, Line/binary>>, Tokens, <<>>) -> + tokens(Line, Tokens, <<>>); +tokens(<<$\s, Line/binary>>, Tokens, <<$", Buffer/binary>>) -> + tokens(Line, Tokens, <<$", Buffer/binary, $\s>>); +tokens(<<$\s, Line/binary>>, Tokens, Buffer) -> + tokens(<<$\s, Line/binary>>, Tokens ++ [Buffer], <<>>); +tokens(<<$\", Line/binary>>, Tokens, <<>>) -> + tokens(Line, Tokens, <<$">>); +tokens(<<$\", Line/binary>>, Tokens, <<$", Buffer/binary>>) -> + tokens(Line, Tokens ++ [<<$", Buffer/binary, $">>], <<>>); +tokens(<<$\", Line/binary>>, Tokens, Buffer) -> + tokens(Line, Tokens ++ [Buffer], <<$">>); +tokens(<<Character, Line/binary>>, Tokens, Buffer) -> + tokens(Line, Tokens, <<Buffer/binary, Character>>). + +strip_quotes(<<>>) -> + <<>>; +strip_quotes(<<$", $", $">>) -> + <<$", $", $">>; +strip_quotes(<<$", Token/binary>>) -> + case binary:last(Token) of + $" -> + binary:part(Token, {0, byte_size(Token) - 1}); + _ -> + Token + end; +strip_quotes(Token) -> + Token. diff --git a/src/gurka_transform.erl b/src/gurka_transform.erl new file mode 100644 index 0000000000000000000000000000000000000000..32b3aa6aed1e6321fae82dab96d16fa39eae6db6 --- /dev/null +++ b/src/gurka_transform.erl @@ -0,0 +1,57 @@ +-module(gurka_transform). +-export([parse_transform/2]). + +parse_transform(Forms, _Options) -> + forms(Forms). + +forms([]) -> + []; +forms([Form | Forms]) -> + [form(Form) | forms(Forms)]. + +form({function, Line, Name, Arity, Clauses}) when Name =:= setup_feature; Name =:= setup_scenario; Name =:= given; Name =:= 'when'; Name =:= then -> + function(Line, Name, Arity, Clauses); +form(Form) -> + Form. + +function(Line, Name, Arity, Clauses) -> + {function, Line, Name, Arity, clauses(Clauses)}. + +clauses([]) -> + []; +clauses([Clause | Clauses]) -> + [clause(Clause) | clauses(Clauses)]. + +clause({clause, ClauseLine, [{bin, _, [{bin_element, _, {string, StringLine, String}, default, default}]} | Head], Guard, Exprs}) -> + {clause, ClauseLine, [process_string(StringLine, String) | Head], Guard, Exprs}; + +clause({clause, ClauseLine, [{string, StringLine, String} | Head], Guard, Exprs}) -> + {clause, ClauseLine, [process_string(StringLine, String) | Head], Guard, Exprs}; + +clause({clause, ClauseLine, [{tuple, _, [{bin, _, [{bin_element, _, {string, StringLine, String}, default, default}]}, {cons, _, _, _}]} | Head], Guard, Exprs}) -> + {clause, ClauseLine, [process_string(StringLine, String) | Head], Guard, Exprs}; + +clause({clause, ClauseLine, [{tuple, _, [{string, StringLine, String}, {cons, _, _, _}]} | Head], Guard, Exprs}) -> + {clause, ClauseLine, [process_string(StringLine, String) | Head], Guard, Exprs}; + +clause({clause, Line, Head, Guard, Exprs}) -> + {clause, Line, Head, Guard, Exprs}; + +clause(Clause) -> + Clause. + +process_string(Row, String) -> + Tokens = gurka_parser:tokens(list_to_binary(String)), + Pattern = build_pattern(Row, Tokens), + Pattern. + +build_pattern(Row, []) -> + {nil, Row}; + +build_pattern(Row, [<<$\$, Token/binary>> | Tokens]) -> + Var = {var, Row, binary_to_atom(Token, utf8)}, + {cons, Row, Var, build_pattern(Row, Tokens)}; + +build_pattern(Row, [Token | Tokens]) -> + Bin = {bin, Row, [{bin_element, Row, {string, Row, binary_to_list(Token)}, default, default}]}, + {cons, Row, Bin, build_pattern(Row, Tokens)}. diff --git a/test/feature_parser.erl b/test/feature_parser.erl new file mode 100644 index 0000000000000000000000000000000000000000..457735d1ceaeb57fe9d8adbfc3e7b1912ad7f846 --- /dev/null +++ b/test/feature_parser.erl @@ -0,0 +1,58 @@ +-compile({parse_transform, gurka_transform}). +-module(feature_parser). + +-export([setup_feature/1, setup_scenario/2, teardown_feature/1, teardown_scenario/1, given/2, 'when'/2, then/2]). + +-record(state, {feature_dir, feature}). + +setup_feature(_Tokens) -> + {ok, #state{}}. + +teardown_feature(_State) -> + ok. + +setup_scenario(_Tokens, State) -> + {ok, State#state{}}. + +teardown_scenario(_State) -> + ok. + +given("feature files are loaded from $Directory", State) -> + case filelib:is_dir(Directory) of + true -> {ok, State#state{feature_dir = Directory}}; + _ -> {error, <<Directory/binary, " is a not a directory">>} + end; + +given("I parse the feature file $File", State) -> + case gurka_parser:parse(filename:join(State#state.feature_dir, File)) of + {ok, Feature} -> + {ok, State#state{feature = Feature}}; + Error -> + Error + end; + +given(Tokens, State) -> + io:format("given ~p~n", [Tokens]), + {ok, State}. + +'when'(Tokens, State) -> + io:format("when ~p~n", [Tokens]), + {ok, State}. + +then("the parsed result should have $StepCount $StepType steps", State) -> + ExpectedCount = binary_to_integer(StepCount), + Type = binary_to_atom(StepType, utf8), + RealCount = lists:foldl( + fun({_, Action, _, _}, Sum) when Action == Type -> Sum + 1; + (_, Sum) -> Sum + end, 0, State#state.feature), + if + ExpectedCount == RealCount -> + {ok, State}; + true -> + {error, lists:flatten(io_lib:format("Expected ~p ~p steps, but got ~p", [ExpectedCount, Type, RealCount]))} + end; + +then(Tokens, State) -> + io:format("then ~p~n", [Tokens]), + {ok, State}. diff --git a/test/features.erl b/test/features.erl new file mode 100644 index 0000000000000000000000000000000000000000..689cbe2c469568700813491324d57314eb390d14 --- /dev/null +++ b/test/features.erl @@ -0,0 +1,31 @@ +-module(features). + +-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. +