diff --git a/.gitignore b/.gitignore index 442d6c6..3666c6d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ ebin erl_crash.dump **.cov.html *~ -cucumberl \ No newline at end of file +cucumberl +.eunit +*.swp diff --git a/Makefile b/Makefile index d091de6..386cf4f 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,10 @@ all: build build: - rebar compile escriptize + ./rebar compile escriptize clean: - rebar clean + ./rebar clean test: - rebar eunit + ./rebar eunit diff --git a/rebar b/rebar index fcbff30..ccec121 100755 Binary files a/rebar and b/rebar differ diff --git a/rebar.config b/rebar.config index d40ae1c..0507d7e 100644 --- a/rebar.config +++ b/rebar.config @@ -1,6 +1,6 @@ %% process the examples directory -{sub_dirs, ["examples/sample"]}. +{sub_dirs, ["examples/simple_sample", "examples/complex_sample"]}. %% clean up {clean_files, ["ebin", "examples/ebin", "erl_crash.dump*", "tmp/*.cov.html"]}. diff --git a/src/cucumber_parser.erl b/src/cucumber_parser.erl deleted file mode 100644 index cdf0402..0000000 --- a/src/cucumber_parser.erl +++ /dev/null @@ -1,208 +0,0 @@ --module(cucumber_parser). - --include_lib("eunit/include/eunit.hrl"). --include("cucumberl.hrl"). - --export([parse/1]). - -parse(FilePath) -> - StepMod = list_to_atom(filename:basename(FilePath, ".feature")), - {StepMod, process_lines(lines(FilePath))}. - -process_lines(Lines) -> - NumberedLines = numbered_lines(Lines), - {Tree, _} = - lists:foldl(fun process_line/2, - {[], {undefined, undefined}}, - expanded_lines(NumberedLines)), - lists:reverse(Tree). - - -expanded_lines(NumberedLines) -> - %% Expand "Scenario Outlines" or tables. - {_, _, ExpandedLines} = - lists:foldl( - fun({_LineNum, Line} = LNL, - {LastScenarioOutline, RowHeader, Out}) -> - case {LastScenarioOutline, RowHeader, string_to_atoms(Line)} of - {undefined, _, ['scenario', 'outline:' | _]} -> - {[LNL], undefined, Out}; - {undefined, _, _} -> - {undefined, undefined, [LNL | Out]}; - {LSO, _, ['examples:' | _]} -> - {lists:reverse(LSO), undefined, Out}; - {LSO, undefined, ['|' | _] = Row} -> - {LSO, evens(Row), Out}; - {LSO, _, ['|' | _] = Row} -> - ESO = lists:reverse( - expand_scenario_outline(LSO, RowHeader, - evens(Row))), - {LSO, RowHeader, ESO ++ Out}; - {_, _, []} -> - {undefined, undefined, [LNL | Out]}; - {LSO, _, _} -> - {[LNL | LSO], RowHeader, Out} - end - end, - {undefined, undefined, []}, - NumberedLines), - lists:reverse(ExpandedLines). - -expand_scenario_outline(ScenarioLines, RowHeader, RowTokens) -> - KeyValList = lists:zip(RowHeader, RowTokens), - lists:map(fun ({LineNum, Line}) -> - {Strs, Placeholders} = - unzip_odd_even(string:tokens(Line, "<>")), - Replacements = - lists:map( - fun (Placeholder) -> - K = list_to_atom(Placeholder), - case lists:keysearch(K, 1, KeyValList) of - {value, {K, Val}} -> atom_to_list(Val) - end - end, - Placeholders), - Line2 = - lists:foldl(fun (X, Acc) -> Acc ++ X end, - "", zip_odd_even(Strs, Replacements)), - {LineNum, Line2} - end, - ScenarioLines). - -process_line({LineNum, Line}, {Acc, {Section0, GWT0}}) -> - %% GWT stands for given-when-then. - %% GWT is the previous line's given-when-then atom. - - %% Handle quoted sections by spliting by "\"" first. - {TokenStrs, QuotedStrs} = - unzip_odd_even(string:tokens(Line, "\"")), - - %% Atomize the unquoted sections. - TokenAtoms = lists:map(fun string_to_atoms/1, TokenStrs), - - %% Zip it back together into a Tokens list that might look like... - %% [given, i, have, entered, "Joe Armstrong", as, my, name] - %% or - %% ['when', i, have, installed, erlang] - %% or - %% ['then', i, should, see, someone, calling, me] - %% - %% Some atoms are reserved words in erlang ('when', 'if', 'then') - %% and need single quoting. - %% - Tokens = flat_zip_odd_even(TokenAtoms, QuotedStrs), - - %% Run through the FeatureModule steps, only if we are in a scenario - %% section, otherwise, skip the line. - {Parsed, Section1, GWT1} = - case {Section0, Tokens} of - {_, ['feature:' | _]} -> - {{feature, LineNum, Tokens}, undefined, GWT0}; - {_, ['scenario:' | _]} -> - {{scenario, LineNum, Tokens}, senario, GWT0}; - {_, ['scenario', 'outline:' | _]} -> - {{senario_outline, LineNum, Tokens, Line}, - senario, GWT0}; - {_, []} -> - {{desc, LineNum, Tokens, Line}, undefined, GWT0}; - {undefined, _} -> - {{desc, LineNum, Tokens, Line}, undefined, GWT0}; - {scenario, ['#' | _]} -> - {{desc, LineNum, Tokens, Line}, Section0, GWT0}; - {scenario, [TokensHead | TokensTail]} -> - G = case {GWT0, TokensHead} of - {undefined, _} -> TokensHead; - {_, 'and'} -> GWT0; - {GWT0, TokensHead} -> TokensHead - end, - {{action, LineNum, G, TokensTail, Line}, Section0, G} - end, - {[Parsed | Acc], Section1, GWT1}. - - -numbered_lines(Lines) -> - NLines = length(Lines), - lists:zip(lists:seq(1, NLines, 1), Lines). - -lines(FilePath) -> - case file:read_file(FilePath) of - {ok, FB} -> lines(binary_to_list(FB), [], []); - Err -> io:format("error: could not open file ~p~n", [FilePath]), - exit(Err) - end. - -lines([], CurrLine, Lines) -> - lists:reverse([lists:reverse(CurrLine) | Lines]); -lines([$\n | Rest], CurrLine, Lines) -> - lines(Rest, [], [lists:reverse(CurrLine) | Lines]); -lines([X | Rest], CurrLine, Lines) -> - lines(Rest, [X | CurrLine], Lines). - -%% This flat_zip_odd_even() also does flattening of Odds, -%% since each Odd might be a list of atoms. - -flat_zip_odd_even(Odds, Evens) -> - zip_odd_even(flat, Odds, Evens, 1, []). - -zip_odd_even(Odds, Evens) -> - zip_odd_even(reg, Odds, Evens, 1, []). - -zip_odd_even(_, [], [], _F, Acc) -> - lists:reverse(Acc); -zip_odd_even(K, [], [Even | Evens], F, Acc) -> - zip_odd_even(K, [], Evens, F, [Even | Acc]); - -zip_odd_even(reg, [Odd | Odds], [], F, Acc) -> - zip_odd_even(reg, Odds, [], F, [Odd | Acc]); -zip_odd_even(flat, [Odd | Odds], [], F, Acc) -> - zip_odd_even(flat, Odds, [], F, lists:reverse(Odd) ++ Acc); - -zip_odd_even(reg, [Odd | Odds], Evens, 1, Acc) -> - zip_odd_even(reg, Odds, Evens, 0, [Odd | Acc]); -zip_odd_even(flat, [Odd | Odds], Evens, 1, Acc) -> - zip_odd_even(flat, Odds, Evens, 0, lists:reverse(Odd) ++ Acc); - -zip_odd_even(K, Odds, [Even | Evens], 0, Acc) -> - zip_odd_even(K, Odds, Evens, 1, [Even | Acc]). - -unzip_odd_even(Tokens) -> - {Odds, Evens, _F} = - lists:foldl(fun (X, {Odds, Evens, F}) -> - case F of - 1 -> {[X | Odds], Evens, 0}; - 0 -> {Odds, [X | Evens], 1} - end - end, - {[], [], 1}, Tokens), - {lists:reverse(Odds), lists:reverse(Evens)}. - -evens(L) -> - {_Odds, Evens} = unzip_odd_even(L), - Evens. - -string_to_atoms(StrWords) -> - lists:map(fun (Y) -> list_to_atom(string:to_lower(Y)) end, - string:tokens(StrWords, " ")). - -%% ------------------------------------ - -unzip_test() -> - ?assertMatch({[], []}, unzip_odd_even([])), - ?assertMatch({[1], []}, unzip_odd_even([1])), - ?assertMatch({[1], [2]}, unzip_odd_even([1, 2])), - ?assertMatch({[1, 3], [2]}, unzip_odd_even([1, 2, 3])), - ?assertMatch({[1, 3, 5], [2, 4, 6]}, - unzip_odd_even([1, 2, 3, 4, 5, 6])). - -zip_test() -> - ?assertMatch([1, 2, 3, 4, 5, 6], - zip_odd_even([1, 3, 5], [2, 4, 6])), - ?assertMatch([1, 2, 3, 4, 5, 6], - flat_zip_odd_even([[1], [3], [5]], [2, 4, 6])). - -string_to_atoms_test() -> - ?assertMatch([], string_to_atoms("")), - ?assertMatch([a, bb, ccc], - string_to_atoms("a bb ccc")), - ?assertMatch([a, bb, ccc], - string_to_atoms(" a bb ccc ")). diff --git a/src/cucumberl.erl b/src/cucumberl.erl index 3abb014..4411f82 100644 --- a/src/cucumberl.erl +++ b/src/cucumberl.erl @@ -30,7 +30,7 @@ run(FilePath) run(FilePath, FeatureModule) when is_list(FilePath), is_atom(FeatureModule) -> - {_, Tree} = cucumber_parser:parse(FilePath), + {_, Tree} = cucumberl_parser:parse(FilePath), run_tree(Tree, FeatureModule). run_tree(Tree, FeatureModule) -> @@ -76,6 +76,7 @@ run_tree(Tree, FeatureModule) -> failed end. + process_line({Type, LineNum, Tokens, Line}, {SkipScenario, State, #cucumberl_stats{scenarios = NScenarios, @@ -95,10 +96,10 @@ process_line({Type, LineNum, Tokens, Line}, {_, feature} -> {false, {ok, State}, Stats}; {_, scenario} -> - {false, {ok, State}, + {false, {ok, call_scenario_setup(FeatureModule)}, Stats#cucumberl_stats{scenarios = NScenarios + 1}}; {_, scenario_outline} -> - {false, {ok, State}, + {false, {ok, call_scenario_setup(FeatureModule)}, Stats#cucumberl_stats{scenarios = NScenarios + 1}}; {false, {action, G}} -> R = try @@ -187,9 +188,17 @@ call_setup(FeatureModule) -> end. call_teardown(FeatureModule, State) -> - case erlang:function_exported(FeatureModule, teardown, 0) of + case erlang:function_exported(FeatureModule, teardown, 1) of true -> FeatureModule:teardown(State); false -> undefined end. + +call_scenario_setup(FeatureModule) -> + case erlang:function_exported(FeatureModule, scenario_setup, 0) of + true -> + FeatureModule:scenario_setup(); + false -> + undefined + end.