-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12 from ericbmerritt/master
Add missing file and better error messages
- Loading branch information
Showing
3 changed files
with
212 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
-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 ")). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters