|
| 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 ")). |
0 commit comments