Skip to content

Commit 2bb9b35

Browse files
author
Steve Yen
committed
Merge pull request #12 from ericbmerritt/master
Add missing file and better error messages
2 parents e7936b3 + 9a204eb commit 2bb9b35

File tree

3 files changed

+212
-2
lines changed

3 files changed

+212
-2
lines changed

src/cucumber_parser.erl

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
-module(cucumber_parser).
2+
3+
-include_lib("eunit/include/eunit.hrl").
4+
-include("cucumberl.hrl").
5+
6+
-export([parse/1]).
7+
8+
parse(FilePath) ->
9+
StepMod = list_to_atom(filename:basename(FilePath, ".feature")),
10+
{StepMod, process_lines(lines(FilePath))}.
11+
12+
process_lines(Lines) ->
13+
NumberedLines = numbered_lines(Lines),
14+
{Tree, _} =
15+
lists:foldl(fun process_line/2,
16+
{[], {undefined, undefined}},
17+
expanded_lines(NumberedLines)),
18+
lists:reverse(Tree).
19+
20+
21+
expanded_lines(NumberedLines) ->
22+
%% Expand "Scenario Outlines" or tables.
23+
{_, _, ExpandedLines} =
24+
lists:foldl(
25+
fun({_LineNum, Line} = LNL,
26+
{LastScenarioOutline, RowHeader, Out}) ->
27+
case {LastScenarioOutline, RowHeader, string_to_atoms(Line)} of
28+
{undefined, _, ['scenario', 'outline:' | _]} ->
29+
{[LNL], undefined, Out};
30+
{undefined, _, _} ->
31+
{undefined, undefined, [LNL | Out]};
32+
{LSO, _, ['examples:' | _]} ->
33+
{lists:reverse(LSO), undefined, Out};
34+
{LSO, undefined, ['|' | _] = Row} ->
35+
{LSO, evens(Row), Out};
36+
{LSO, _, ['|' | _] = Row} ->
37+
ESO = lists:reverse(
38+
expand_scenario_outline(LSO, RowHeader,
39+
evens(Row))),
40+
{LSO, RowHeader, ESO ++ Out};
41+
{_, _, []} ->
42+
{undefined, undefined, [LNL | Out]};
43+
{LSO, _, _} ->
44+
{[LNL | LSO], RowHeader, Out}
45+
end
46+
end,
47+
{undefined, undefined, []},
48+
NumberedLines),
49+
lists:reverse(ExpandedLines).
50+
51+
expand_scenario_outline(ScenarioLines, RowHeader, RowTokens) ->
52+
KeyValList = lists:zip(RowHeader, RowTokens),
53+
lists:map(fun ({LineNum, Line}) ->
54+
{Strs, Placeholders} =
55+
unzip_odd_even(string:tokens(Line, "<>")),
56+
Replacements =
57+
lists:map(
58+
fun (Placeholder) ->
59+
K = list_to_atom(Placeholder),
60+
case lists:keysearch(K, 1, KeyValList) of
61+
{value, {K, Val}} -> atom_to_list(Val)
62+
end
63+
end,
64+
Placeholders),
65+
Line2 =
66+
lists:foldl(fun (X, Acc) -> Acc ++ X end,
67+
"", zip_odd_even(Strs, Replacements)),
68+
{LineNum, Line2}
69+
end,
70+
ScenarioLines).
71+
72+
process_line({LineNum, Line}, {Acc, {Section0, GWT0}}) ->
73+
%% GWT stands for given-when-then.
74+
%% GWT is the previous line's given-when-then atom.
75+
76+
%% Handle quoted sections by spliting by "\"" first.
77+
{TokenStrs, QuotedStrs} =
78+
unzip_odd_even(string:tokens(Line, "\"")),
79+
80+
%% Atomize the unquoted sections.
81+
TokenAtoms = lists:map(fun string_to_atoms/1, TokenStrs),
82+
83+
%% Zip it back together into a Tokens list that might look like...
84+
%% [given, i, have, entered, "Joe Armstrong", as, my, name]
85+
%% or
86+
%% ['when', i, have, installed, erlang]
87+
%% or
88+
%% ['then', i, should, see, someone, calling, me]
89+
%%
90+
%% Some atoms are reserved words in erlang ('when', 'if', 'then')
91+
%% and need single quoting.
92+
%%
93+
Tokens = flat_zip_odd_even(TokenAtoms, QuotedStrs),
94+
95+
%% Run through the FeatureModule steps, only if we are in a scenario
96+
%% section, otherwise, skip the line.
97+
{Parsed, Section1, GWT1} =
98+
case {Section0, Tokens} of
99+
{_, ['feature:' | _]} ->
100+
{{feature, LineNum, Tokens}, undefined, GWT0};
101+
{_, ['scenario:' | _]} ->
102+
{{scenario, LineNum, Tokens}, senario, GWT0};
103+
{_, ['scenario', 'outline:' | _]} ->
104+
{{senario_outline, LineNum, Tokens, Line},
105+
senario, GWT0};
106+
{_, []} ->
107+
{{desc, LineNum, Tokens, Line}, undefined, GWT0};
108+
{undefined, _} ->
109+
{{desc, LineNum, Tokens, Line}, undefined, GWT0};
110+
{scenario, ['#' | _]} ->
111+
{{desc, LineNum, Tokens, Line}, Section0, GWT0};
112+
{scenario, [TokensHead | TokensTail]} ->
113+
G = case {GWT0, TokensHead} of
114+
{undefined, _} -> TokensHead;
115+
{_, 'and'} -> GWT0;
116+
{GWT0, TokensHead} -> TokensHead
117+
end,
118+
{{action, LineNum, G, TokensTail, Line}, Section0, G}
119+
end,
120+
{[Parsed | Acc], Section1, GWT1}.
121+
122+
123+
numbered_lines(Lines) ->
124+
NLines = length(Lines),
125+
lists:zip(lists:seq(1, NLines, 1), Lines).
126+
127+
lines(FilePath) ->
128+
case file:read_file(FilePath) of
129+
{ok, FB} -> lines(binary_to_list(FB), [], []);
130+
Err -> io:format("error: could not open file ~p~n", [FilePath]),
131+
exit(Err)
132+
end.
133+
134+
lines([], CurrLine, Lines) ->
135+
lists:reverse([lists:reverse(CurrLine) | Lines]);
136+
lines([$\n | Rest], CurrLine, Lines) ->
137+
lines(Rest, [], [lists:reverse(CurrLine) | Lines]);
138+
lines([X | Rest], CurrLine, Lines) ->
139+
lines(Rest, [X | CurrLine], Lines).
140+
141+
%% This flat_zip_odd_even() also does flattening of Odds,
142+
%% since each Odd might be a list of atoms.
143+
144+
flat_zip_odd_even(Odds, Evens) ->
145+
zip_odd_even(flat, Odds, Evens, 1, []).
146+
147+
zip_odd_even(Odds, Evens) ->
148+
zip_odd_even(reg, Odds, Evens, 1, []).
149+
150+
zip_odd_even(_, [], [], _F, Acc) ->
151+
lists:reverse(Acc);
152+
zip_odd_even(K, [], [Even | Evens], F, Acc) ->
153+
zip_odd_even(K, [], Evens, F, [Even | Acc]);
154+
155+
zip_odd_even(reg, [Odd | Odds], [], F, Acc) ->
156+
zip_odd_even(reg, Odds, [], F, [Odd | Acc]);
157+
zip_odd_even(flat, [Odd | Odds], [], F, Acc) ->
158+
zip_odd_even(flat, Odds, [], F, lists:reverse(Odd) ++ Acc);
159+
160+
zip_odd_even(reg, [Odd | Odds], Evens, 1, Acc) ->
161+
zip_odd_even(reg, Odds, Evens, 0, [Odd | Acc]);
162+
zip_odd_even(flat, [Odd | Odds], Evens, 1, Acc) ->
163+
zip_odd_even(flat, Odds, Evens, 0, lists:reverse(Odd) ++ Acc);
164+
165+
zip_odd_even(K, Odds, [Even | Evens], 0, Acc) ->
166+
zip_odd_even(K, Odds, Evens, 1, [Even | Acc]).
167+
168+
unzip_odd_even(Tokens) ->
169+
{Odds, Evens, _F} =
170+
lists:foldl(fun (X, {Odds, Evens, F}) ->
171+
case F of
172+
1 -> {[X | Odds], Evens, 0};
173+
0 -> {Odds, [X | Evens], 1}
174+
end
175+
end,
176+
{[], [], 1}, Tokens),
177+
{lists:reverse(Odds), lists:reverse(Evens)}.
178+
179+
evens(L) ->
180+
{_Odds, Evens} = unzip_odd_even(L),
181+
Evens.
182+
183+
string_to_atoms(StrWords) ->
184+
lists:map(fun (Y) -> list_to_atom(string:to_lower(Y)) end,
185+
string:tokens(StrWords, " ")).
186+
187+
%% ------------------------------------
188+
189+
unzip_test() ->
190+
?assertMatch({[], []}, unzip_odd_even([])),
191+
?assertMatch({[1], []}, unzip_odd_even([1])),
192+
?assertMatch({[1], [2]}, unzip_odd_even([1, 2])),
193+
?assertMatch({[1, 3], [2]}, unzip_odd_even([1, 2, 3])),
194+
?assertMatch({[1, 3, 5], [2, 4, 6]},
195+
unzip_odd_even([1, 2, 3, 4, 5, 6])).
196+
197+
zip_test() ->
198+
?assertMatch([1, 2, 3, 4, 5, 6],
199+
zip_odd_even([1, 3, 5], [2, 4, 6])),
200+
?assertMatch([1, 2, 3, 4, 5, 6],
201+
flat_zip_odd_even([[1], [3], [5]], [2, 4, 6])).
202+
203+
string_to_atoms_test() ->
204+
?assertMatch([], string_to_atoms("")),
205+
?assertMatch([a, bb, ccc],
206+
string_to_atoms("a bb ccc")),
207+
?assertMatch([a, bb, ccc],
208+
string_to_atoms(" a bb ccc ")).

src/cucumberl.app.src

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{application, cucumberl,
22
[
33
{description, "A pure-erlang implementation of Cucumber."},
4-
{vsn, "0.0.4"},
4+
{vsn, "0.0.5"},
55
{applications, [
66
kernel,
77
stdlib

src/cucumberl.erl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ process_line({Type, LineNum, Tokens, Line},
107107
catch
108108
error:function_clause ->
109109
%% we don't have a matching function clause
110-
undefined;
110+
io:format("~nSTEP ~s is *not* implemented: ~p ~n",
111+
[Line, Tokens]),
112+
{failed, {unimplemented, Tokens}};
111113
Err:Reason ->
112114
io:format("~nSTEP: ~s FAILED: ~n ~p:~p ~p~n",
113115
[Line, Err, Reason,

0 commit comments

Comments
 (0)