├── .gitignore ├── COPYING ├── README.md ├── examples ├── complex_sample │ ├── features │ │ ├── complex_sample.feature │ │ ├── complex_sample_more.feature │ │ └── complex_sample_table.feature │ ├── rebar.config │ └── src │ │ ├── complex_sample.app.src │ │ ├── complex_sample.erl │ │ ├── complex_sample_more.erl │ │ ├── complex_sample_support.erl │ │ └── complex_sample_table.erl ├── failures │ ├── Makefile │ ├── features │ │ └── fail.feature │ ├── rebar.config │ └── src │ │ ├── fail.erl │ │ └── failures.app.src └── simple_sample │ ├── features │ ├── simple_sample.feature │ └── simple_sample_table.feature │ ├── rebar.config │ └── src │ ├── simple_sample.erl │ ├── simple_sample_table.erl │ └── simple_samples.app.src ├── include └── cucumberl.hrl ├── rebar.config ├── rebar3 └── src ├── cucumber_parser.erl ├── cucumberl.app.src ├── cucumberl.erl ├── cucumberl_cli.erl ├── cucumberl_gen.erl └── cucumberl_parser.erl /.gitignore: -------------------------------------------------------------------------------- 1 | ebin 2 | erl_crash.dump 3 | **.cov.html 4 | *~ 5 | cucumberl 6 | rebar.lock 7 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Steve Yen - NorthScale, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cucumberl 2 | 3 | A pure-erlang, open-source, implementation of Cucumber 4 | (http://cukes.info). This provides a subset of the Cucumber feature 5 | definition language. 6 | 7 | ## Quick Start 8 | 9 | You'll need erlang, of course. 10 | 11 | To do a build, do... 12 | 13 | ./rebar3 compile 14 | 15 | To run unit tests, do... 16 | 17 | ./rebar3 eunit 18 | 19 | There's are sample feature files (examples/complex_sample/features and 20 | examples/complex_sample/features) and step definitions (in 21 | examples/src). Running `make test` will execute these too. 22 | 23 | You can also run them by hand, for example... 24 | 25 | examples $ ../cucumberl 26 | Feature: Addition :1 27 | In order to avoid silly mistakes :2 28 | As a math idiot :3 29 | I want to be told the sum of two numbers :4 30 | :5 31 | Scenario: Add two numbers :6 32 | Given I have entered 50 into the calculator :7 33 | And I have entered 70 into the calculator :8 34 | When I press add :9 35 | Then the result should be 120 on the screen :10 ok 36 | :11 37 | 38 | etc..... 39 | 40 | ## Slow Start 41 | 42 | So you want to write your own step definitions? No problem. Any 43 | erlang module that implements step definitions should export a step/2 44 | function, with this kind of call signature... 45 | 46 | Action(TokenList, State, Info) 47 | 48 | Where Action is: 49 | 50 | - given 51 | - 'when' 52 | - then 53 | 54 | The TokenList parameter is a list of either atoms or strings, such as... 55 | 56 | [i, have, entered, "Joe Armstrong", into, the, authors, field] 57 | 58 | So for example, the previous TokenList would also be accepted in 59 | a function definition like this: 60 | 61 | given([i, have, entered, Name, into, the, authors, field], State, _) -> 62 | {ok, NewState}. 63 | 64 | The State parameter is the state the last step function returned in the state field of the tuple. In the above example, this is NewState. 65 | 66 | The Info parameter is a tuple of helpful debug information, such as 67 | the {LineText, LineNum}, of what cucumberl is currently processing. 68 | The Info parameter is usually ignored unless you're deep into 69 | debugging your scenario/steps. 70 | 71 | Here's how you'd write a few step definition functions, using erlang's 72 | pattern matching. 73 | 74 | given([i, have, entered, N, into, the, calculator], _State, _Info) -> 75 | % Your step implementation here. 76 | todo. 77 | 78 | 'when'([, i, press, add], _, _) -> 79 | % Your step implementation here. 80 | todo. 81 | 82 | then([the, result, should, be, Result, on, the, screen], _, _) -> 83 | % Your step implementation here. 84 | todo. 85 | 86 | Notice that all the tokens have been atomized (and turned lowercase). 87 | 88 | - The atoms `true` and `ok` in the state tuple represent *success* and 89 | print *ok* on the console 90 | - A two-tuple of the form `{failed, Reason}` indicates failure 91 | 92 | The above step definitions will match a scenario like the following... 93 | 94 | Scenario: Add two numbers 95 | Given I have entered 50 into the calculator 96 | And I have entered 70 into the calculator 97 | When I press add 98 | Then the result should be 120 on the screen 99 | 100 | ## Running cucumberl 101 | 102 | Running cucumberl on the command line is very simple. Just execute the 103 | cucumberl self-contained escript. 104 | 105 | To run a feature file through cucumberl using the erlang API... 106 | 107 | cucumberl:run(PathToFeatureFile). 108 | 109 | For example... 110 | 111 | cucumberl:run("./features/sample.feature"). 112 | 113 | or 114 | 115 | cucumberl:run("./features/sample.feature", FeatureDefinitionModule). 116 | 117 | The FeatureDefinitionModule parameter is an optional module that 118 | implements the feature and contains the step callbacks. However, it is 119 | only needed when the name of the step implementation is different then 120 | the name of the feature. For example... 121 | 122 | cucumberl:run("./features/auction.feature", 123 | auction). 124 | 125 | is exactly equivalent to 126 | 127 | cucumberl:run("./features/auction.feature"). 128 | 129 | However, you may want to implement the feature in a different module, 130 | such as ... 131 | 132 | cucumberl:run("./features/auction.feature", some_other_module). 133 | 134 | perfectly acceptable but not recommended. 135 | 136 | ## Scenario Outlines 137 | 138 | There's basic support for Scenario Outlines, aka Example Tables, in 139 | cucumberl. However, placeholders names should be all lowercase, and 140 | there shouldn't be any blank lines before the "Examples:" label. For 141 | example... 142 | 143 | Scenario Outline: 144 | Given I have cleared the calculator 145 | And I have entered into the calculator 146 | And I have entered into the calculator 147 | When I press 148 | Then the result should be on the screen 149 | Examples: 150 | | a | b | ab | op | 151 | | 1 | 1 | 2 | add | 152 | | 1 | 3 | 3 | multiply | 153 | | 2 | 3 | 6 | multiply | 154 | | 10 | 1 | 11 | add | 155 | 156 | See the files examples/simple_sample/src/simple_sample_table.erl and 157 | examples/simple_sample/features/simple_sample_table.feature for more details. 158 | 159 | ## License 160 | 161 | MIT - We made this for you! 162 | 163 | ## Feedback, or getting in touch 164 | 165 | Improvements and patches welcomed -- info@northscale.com 166 | 167 | Cheers, 168 | Steve Yen 169 | -------------------------------------------------------------------------------- /examples/complex_sample/features/complex_sample.feature: -------------------------------------------------------------------------------- 1 | Feature: Addition 2 | In order to avoid silly mistakes 3 | As a math idiot 4 | I want to be told the sum of two numbers 5 | 6 | Scenario: Add two numbers 7 | Given I have entered 50 into the calculator 8 | And I have entered 70 into the calculator 9 | When I press add 10 | Then the result should be 120 on the screen 11 | -------------------------------------------------------------------------------- /examples/complex_sample/features/complex_sample_more.feature: -------------------------------------------------------------------------------- 1 | Feature: Multiplication 2 | In order to avoid silly mistakes 3 | As a math idiot 4 | I want to be told the product of two numbers 5 | 6 | Scenario: Multiply two numbers 7 | Given I have entered 50 into the calculator 8 | And I have entered 70 into the calculator 9 | When I press multiply 10 | Then the result should be 3500 on the screen 11 | -------------------------------------------------------------------------------- /examples/complex_sample/features/complex_sample_table.feature: -------------------------------------------------------------------------------- 1 | Feature: Multiple Operations 2 | In order to avoid silly mistakes 3 | As a math idiot 4 | I want a calculator to do multiple operations 5 | 6 | Scenario Outline: 7 | Given I have cleared the calculator 8 | And I have entered into the calculator 9 | And I have entered into the calculator 10 | When I press 11 | Then the result should be on the screen 12 | Examples: 13 | | a | b | ab | op | 14 | | 1 | 1 | 2 | add | 15 | | 1 | 3 | 3 | multiply | 16 | | 2 | 3 | 6 | multiply | 17 | | 10 | 1 | 11 | add | 18 | 19 | Scenario: Add two numbers 20 | Given I have cleared the calculator 21 | And I have entered 5 into the calculator 22 | And I have entered 7 into the calculator 23 | When I press add 24 | Then the result should be 12 on the screen 25 | -------------------------------------------------------------------------------- /examples/complex_sample/rebar.config: -------------------------------------------------------------------------------- 1 | 2 | {post_hooks, [ 3 | {eunit, "../../cucumberl"} 4 | ]}. 5 | -------------------------------------------------------------------------------- /examples/complex_sample/src/complex_sample.app.src: -------------------------------------------------------------------------------- 1 | {application, complex_sample, 2 | [ 3 | {description, "Complex, Cucumber Samples"}, 4 | {vsn, git}, 5 | {applications, [ 6 | kernel, 7 | stdlib 8 | ]} 9 | ]}. 10 | -------------------------------------------------------------------------------- /examples/complex_sample/src/complex_sample.erl: -------------------------------------------------------------------------------- 1 | -module(complex_sample). 2 | 3 | -export([setup/0, given/3, 'when'/3, then/3, main/0]). 4 | 5 | setup() -> 6 | []. 7 | 8 | %% Step definitions for the sample calculator Addition feature. 9 | 10 | given(Step, State, _) -> 11 | complex_sample_support:given(Step, State). 12 | 13 | 'when'(Step, State, _) -> 14 | complex_sample_support:'when'(Step, State). 15 | 16 | then(Step, State, _) -> 17 | complex_sample_support:then(Step, State). 18 | 19 | main() -> 20 | cucumberl:run("./features/complex_sample.feature"). 21 | 22 | -------------------------------------------------------------------------------- /examples/complex_sample/src/complex_sample_more.erl: -------------------------------------------------------------------------------- 1 | -module(complex_sample_more). 2 | 3 | -export([setup/0, 4 | given/3, 'when'/3, then/3, main/0]). 5 | 6 | setup() -> 7 | []. 8 | 9 | %% Step definitions for the sample calculator Multiplication feature. 10 | given(Step, State, _) -> 11 | complex_sample_support:given(Step, State). 12 | 13 | 'when'(Step, State, _) -> 14 | complex_sample_support:'when'(Step, State). 15 | 16 | then(Step, State, _) -> 17 | complex_sample_support:then(Step, State). 18 | 19 | %% A main() to kick it all off... 20 | 21 | main() -> 22 | cucumberl:run("./features/complex_sample_more.feature"), 23 | cucumberl:run("./features/complex_sample.feature", ?MODULE). 24 | 25 | -------------------------------------------------------------------------------- /examples/complex_sample/src/complex_sample_support.erl: -------------------------------------------------------------------------------- 1 | -module(complex_sample_support). 2 | 3 | -export([given/2, 'when'/2, then/2, enter/2, press/2]). 4 | 5 | %% Step definitions for the sample calculator Addition feature. 6 | 7 | given([i, have, entered, N, into, the, calculator], State) -> 8 | {ok, enter(State, list_to_integer(atom_to_list(N)))}; 9 | given([i, have, cleared, the, calculator], _) -> 10 | {ok, []}. 11 | 12 | 'when'([i, press, Op], State) -> 13 | {ok, press(State, Op)}. 14 | 15 | then([the, result, should, be, Result, on, the, screen], State) -> 16 | list_to_integer(atom_to_list(Result)) =:= State. 17 | 18 | %% Implementing a simple model here... 19 | 20 | enter(State, N) -> 21 | [N|State]. 22 | 23 | press([X, Y], add) -> 24 | X + Y; 25 | press([X, Y], multiply) -> 26 | X * Y. 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/complex_sample/src/complex_sample_table.erl: -------------------------------------------------------------------------------- 1 | -module(complex_sample_table). 2 | 3 | -export([setup/0, given/3, 'when'/3, then/3, main/0]). 4 | 5 | setup() -> 6 | []. 7 | 8 | %% Step definitions for the sample calculator Multiplication feature. 9 | 10 | given([i, have, cleared, the, calculator], _State, _) -> 11 | {ok, []}; 12 | given(Step, State, _) -> 13 | complex_sample_support:given(Step, State). 14 | 15 | 'when'(Step, State, _) -> 16 | complex_sample_support:'when'(Step, State). 17 | 18 | then(Step, State, _) -> 19 | complex_sample_support:then(Step, State). 20 | 21 | %% A main() to kick it all off... 22 | 23 | main() -> 24 | cucumberl:run("./features/complex_sample_table.feature"). 25 | 26 | -------------------------------------------------------------------------------- /examples/failures/Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env make 2 | 3 | test: 4 | ../../rebar3 clean compile eunit 5 | -------------------------------------------------------------------------------- /examples/failures/features/fail.feature: -------------------------------------------------------------------------------- 1 | Feature: Reporting Failures 2 | In order to make silly mistakes visible 3 | As a cucumberl user 4 | I want to be told about failing steps 5 | 6 | Scenario: Fail a given step 7 | Given a step that works 8 | When I come across a failing step 9 | Then cucumberl should show this as a failure 10 | -------------------------------------------------------------------------------- /examples/failures/rebar.config: -------------------------------------------------------------------------------- 1 | 2 | {clean_files, ["ebin"]}. 3 | {post_hooks, [ 4 | {eunit, "../../cucumberl"} 5 | ]}. 6 | -------------------------------------------------------------------------------- /examples/failures/src/fail.erl: -------------------------------------------------------------------------------- 1 | -module(fail). 2 | 3 | -compile(export_all). 4 | 5 | given([a, step, that, works], _) -> ok. 6 | 7 | 'when'([i, come, across, a, failing, step], _) -> ok. 8 | 9 | then([cucumberl, should, show, this, as, a, failure], _) -> 10 | {failed, "Some reason or other...."}. 11 | 12 | step(_, _) -> undefined. 13 | -------------------------------------------------------------------------------- /examples/failures/src/failures.app.src: -------------------------------------------------------------------------------- 1 | {application, failures, 2 | [ 3 | {description, "Example of failing tests"}, 4 | {vsn, git}, 5 | {applications, [ 6 | kernel, 7 | stdlib 8 | ]} 9 | ]}. 10 | -------------------------------------------------------------------------------- /examples/simple_sample/features/simple_sample.feature: -------------------------------------------------------------------------------- 1 | Feature: Addition 2 | In order to avoid silly mistakes 3 | As a math idiot 4 | I want to be told the sum of two numbers 5 | 6 | Scenario: Add two numbers 7 | Given I have entered 50 into the calculator 8 | And I have entered 70 into the calculator 9 | When I press add 10 | Then the result should be 120 on the screen 11 | -------------------------------------------------------------------------------- /examples/simple_sample/features/simple_sample_table.feature: -------------------------------------------------------------------------------- 1 | Feature: Multiple Operations 2 | In order to avoid silly mistakes 3 | As a math idiot 4 | I want a calculator to do multiple operations 5 | 6 | Scenario Outline: 7 | Given I have cleared the calculator 8 | And I have entered into the calculator 9 | And I have entered into the calculator 10 | When I press 11 | Then the result should be on the screen 12 | Examples: 13 | | a | b | ab | op | 14 | | 1 | 1 | 2 | add | 15 | | 1 | 3 | 3 | multiply | 16 | | 2 | 3 | 6 | multiply | 17 | | 10 | 1 | 11 | add | 18 | 19 | Scenario: Add two numbers 20 | Given I have cleared the calculator 21 | And I have entered 5 into the calculator 22 | And I have entered 7 into the calculator 23 | When I press add 24 | Then the result should be 12 on the screen 25 | -------------------------------------------------------------------------------- /examples/simple_sample/rebar.config: -------------------------------------------------------------------------------- 1 | 2 | {post_hooks, [ 3 | {eunit, "../../cucumberl"} 4 | ]}. 5 | -------------------------------------------------------------------------------- /examples/simple_sample/src/simple_sample.erl: -------------------------------------------------------------------------------- 1 | -module(simple_sample). 2 | 3 | -export([setup/0, teardown/1, 4 | given/3, 'when'/3, then/3, main/0]). 5 | 6 | -export([enter/2, press/2]). 7 | 8 | setup() -> 9 | []. 10 | 11 | %% Step definitions for the sample calculator Addition feature. 12 | given([i, have, entered, N, into, the, calculator], State, _) -> 13 | {ok, enter(State, list_to_integer(atom_to_list(N)))}. 14 | 15 | 'when'([i, press, Op], State, _) -> 16 | {ok, press(State, Op)}. 17 | 18 | then([the, result, should, be, Result, on, the, screen], 19 | State, _) -> 20 | list_to_integer(atom_to_list(Result)) =:=State. 21 | 22 | teardown(_State) -> 23 | ok. 24 | 25 | %% Implementing a simple model here... 26 | 27 | enter(State, N) -> 28 | [N|State]. 29 | 30 | press(State, add) -> 31 | add(State); 32 | press(State, multiply) -> 33 | multiply(State). 34 | 35 | add([X, Y]) -> 36 | X + Y. 37 | 38 | multiply([X, Y]) -> 39 | X * Y. 40 | 41 | 42 | 43 | %% A main() to kick it all off... 44 | 45 | main() -> 46 | cucumberl:run("./features/simple_sample.feature"). 47 | 48 | -------------------------------------------------------------------------------- /examples/simple_sample/src/simple_sample_table.erl: -------------------------------------------------------------------------------- 1 | -module(simple_sample_table). 2 | 3 | -export([setup/0, teardown/1, 4 | given/3, 'when'/3, then/3, main/0]). 5 | 6 | setup() -> 7 | []. 8 | 9 | teardown(_State) -> 10 | ok. 11 | 12 | %% Step definitions for the sample calculator Addition feature. 13 | given([i, have, entered, N, into, the, calculator], State, _) -> 14 | {ok, simple_sample:enter(State, list_to_integer(atom_to_list(N)))}; 15 | given([i, have, cleared, the, calculator], _State, _) -> 16 | {ok, []}. 17 | 18 | 'when'([i, press, Op], State, _) -> 19 | {ok, simple_sample:press(State, Op)}. 20 | 21 | then([the, result, should, be, Result, on, the, screen], 22 | State, _) -> 23 | list_to_integer(atom_to_list(Result)) =:= State. 24 | 25 | %% A main() to kick it all off... 26 | 27 | main() -> 28 | cucumberl:run("./features/simple_sample_table.feature"). 29 | 30 | -------------------------------------------------------------------------------- /examples/simple_sample/src/simple_samples.app.src: -------------------------------------------------------------------------------- 1 | {application, simple_samples, 2 | [ 3 | {description, "Simple Cucumber Samples"}, 4 | {vsn, git}, 5 | {applications, [ 6 | kernel, 7 | stdlib 8 | ]} 9 | ]}. 10 | -------------------------------------------------------------------------------- /include/cucumberl.hrl: -------------------------------------------------------------------------------- 1 | 2 | -record(cucumberl_stats, {scenarios = 0, 3 | steps = 0, 4 | failures = []}). 5 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | 2 | %% process the examples directory 3 | {sub_dirs, ["examples/sample"]}. 4 | 5 | %% clean up 6 | {clean_files, ["ebin", "examples/ebin", "erl_crash.dump*", "tmp/*.cov.html"]}. 7 | 8 | %% compile options 9 | {erl_opts, [warnings_as_errors]}. 10 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membase/cucumberl/80f5cfabcbacddd751be603241eefb29b132838c/rebar3 -------------------------------------------------------------------------------- /src/cucumber_parser.erl: -------------------------------------------------------------------------------- 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 ")). 209 | -------------------------------------------------------------------------------- /src/cucumberl.app.src: -------------------------------------------------------------------------------- 1 | {application, cucumberl, 2 | [ 3 | {description, "A pure-erlang implementation of Cucumber."}, 4 | {vsn, "0.0.5"}, 5 | {applications, [ 6 | kernel, 7 | stdlib 8 | ]}, 9 | {registered, []}, 10 | {env, []} 11 | ]}. 12 | -------------------------------------------------------------------------------- /src/cucumberl.erl: -------------------------------------------------------------------------------- 1 | -module(cucumberl). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include("cucumberl.hrl"). 5 | 6 | -export([main/1, run/1, run/2]). 7 | 8 | main(Args) -> 9 | cucumberl_cli:main(Args). 10 | 11 | %% Cucumber parser & driver in erlang, in a single file, 12 | %% implementing a subset of the cucumber/gherkin DSL. 13 | %% 14 | %% Example step implementation pattern in erlang... 15 | %% 16 | %% step([given, i, have, entered, N, into, the, calculator], _Info) -> 17 | %% % Your step implementation here. 18 | %% anything_but_undefined. 19 | %% 20 | %% The Info is a {Line, LineNum} tuple. 21 | %% 22 | %% Example run... 23 | %% 24 | %% cucumberl:run("./features/sample.feature"). 25 | %% 26 | run(FilePath) 27 | when is_list(FilePath) -> 28 | {FeatureModule, Tree} = cucumberl_parser:parse(FilePath), 29 | run_tree(Tree, FeatureModule). 30 | 31 | run(FilePath, FeatureModule) 32 | when is_list(FilePath), is_atom(FeatureModule) -> 33 | {_, Tree} = cucumber_parser:parse(FilePath), 34 | run_tree(Tree, FeatureModule). 35 | 36 | run_tree(Tree, FeatureModule) -> 37 | case code:ensure_loaded(FeatureModule) of 38 | {module, FeatureModule} -> 39 | ok; 40 | Error -> 41 | throw(Error) 42 | end, 43 | Result = 44 | try 45 | State = call_setup(FeatureModule), 46 | {_, _, Stats} = 47 | lists:foldl(fun(Entry, Acc) -> 48 | process_line(Entry, Acc, FeatureModule) 49 | end, 50 | {false, State, #cucumberl_stats{}}, 51 | Tree), 52 | call_teardown(FeatureModule, State), 53 | Stats 54 | catch 55 | Err:Reason -> 56 | %% something else went wrong, which means fail 57 | io:format("Feature Failed: ~p:~p ~p", 58 | [Err, Reason, 59 | erlang:get_stacktrace()]), 60 | failed 61 | end, 62 | case Result of 63 | #cucumberl_stats{scenarios = NScenarios, 64 | steps = NSteps, 65 | failures = []} -> 66 | io:format("~n~p scenarios~n~p steps~n~n", 67 | [NScenarios, NSteps]), 68 | {ok, Result}; 69 | #cucumberl_stats{scenarios = NScenarios, 70 | steps = NSteps, 71 | failures = Failures} -> 72 | io:format("~n~p scenarios~n~p steps~n~p failures ~n~n", 73 | [NScenarios, NSteps, erlang:length(Failures)]), 74 | {failed, Result}; 75 | _ -> 76 | failed 77 | end. 78 | 79 | process_line({Type, LineNum, Tokens, Line}, 80 | {SkipScenario, State, 81 | #cucumberl_stats{scenarios = NScenarios, 82 | steps = NSteps, 83 | failures = FailedSoFar } = Stats}, 84 | FeatureModule) -> 85 | %% GWT stands for given-when-then. 86 | %% GWT is the previous line's given-when-then atom. 87 | io:format("~s:~s ~n", 88 | [string:left(Line, 65), 89 | string:left(integer_to_list(LineNum), 4)]), 90 | 91 | %% Run through the FeatureModule steps, only if we are in a scenario 92 | %% section, otherwise, skip the line. 93 | {SkipScenario2, Result, Stats2} = 94 | case {SkipScenario, Type} of 95 | {_, feature} -> 96 | {false, {ok, State}, Stats}; 97 | {_, scenario} -> 98 | {false, {ok, State}, 99 | Stats#cucumberl_stats{scenarios = NScenarios + 1}}; 100 | {_, scenario_outline} -> 101 | {false, {ok, State}, 102 | Stats#cucumberl_stats{scenarios = NScenarios + 1}}; 103 | {false, {action, G}} -> 104 | R = try 105 | apply_step(FeatureModule, G, State, Tokens, 106 | Line, LineNum) 107 | catch 108 | error:function_clause -> 109 | %% we don't have a matching function clause 110 | io:format("~nSTEP ~s is *not* implemented: ~p ~n", 111 | [Line, Tokens]), 112 | {failed, {unimplemented, Tokens}}; 113 | Err:Reason -> 114 | io:format("~nSTEP: ~s FAILED: ~n ~p:~p ~p~n", 115 | [Line, Err, Reason, 116 | erlang:get_stacktrace()]), 117 | %% something else went wrong, which means fail 118 | {failed, {Err, Reason, erlang:get_stacktrace()}} 119 | end, 120 | 121 | {SkipScenario, R, 122 | Stats#cucumberl_stats{steps = NSteps + 1}}; 123 | {true, {action, _}} -> 124 | {SkipScenario, skipped, 125 | Stats#cucumberl_stats{steps = NSteps + 1}}; 126 | {_, desc} -> 127 | {false, {ok, State}, Stats} 128 | end, 129 | 130 | %% Emit result and our accumulator for our calling foldl. 131 | case {Type, Result} of 132 | {{action, G1}, Result} -> 133 | case check_step(Result) of 134 | {passed, PossibleState} -> 135 | {SkipScenario2, PossibleState, Stats2}; 136 | skipped -> 137 | {SkipScenario2, State, Stats2}; 138 | missing -> 139 | io:format("---------NO-STEP--------~n~n"), 140 | io:format("a step definition snippet...~n"), 141 | format_missing_step(G1, Tokens), 142 | {true, undefined, undefined, State, 143 | Stats2#cucumberl_stats{failures = [{missing, G1} 144 | | FailedSoFar] }}; 145 | FailedResult -> 146 | io:format("-------FAIL------- ~n~n"), 147 | {true, State, 148 | Stats2#cucumberl_stats{ failures = [{FailedResult, Result} 149 | |FailedSoFar] }} 150 | end; 151 | _ -> 152 | %% TODO: is this an error case - should it fail when this happens? 153 | {SkipScenario, State, Stats2} 154 | end. 155 | 156 | apply_step(FeatureModule, G, State, Tokens, Line, LineNum) -> 157 | case erlang:function_exported(FeatureModule, G, 3) of 158 | true -> 159 | apply(FeatureModule, G, [Tokens, 160 | State, 161 | {Line, LineNum}]); 162 | false -> 163 | step_undefined 164 | end. 165 | 166 | check_step(true) -> {passed, undefined}; 167 | check_step(ok) -> {passed, undefined}; 168 | check_step({ok, State}) -> {passed, State}; 169 | check_step({true, State}) -> {passed, State}; 170 | check_step(skipped) -> skipped; 171 | check_step(step_undefined) -> missing; 172 | check_step(false) -> failed; 173 | check_step({failed, _}) -> failed; 174 | check_step(_) -> invalid_result. 175 | 176 | format_missing_step('when', Tokens) -> 177 | io:format("'when'(~p, State, _) ->~n undefined.~n~n", [Tokens]); 178 | format_missing_step(GWT, Tokens) -> 179 | io:format("~p(~p, State, _) ->~n undefined.~n~n", [GWT, Tokens]). 180 | 181 | call_setup(FeatureModule) -> 182 | case erlang:function_exported(FeatureModule, setup, 0) of 183 | true -> 184 | FeatureModule:setup(); 185 | false -> 186 | undefined 187 | end. 188 | 189 | call_teardown(FeatureModule, State) -> 190 | case erlang:function_exported(FeatureModule, teardown, 0) of 191 | true -> 192 | FeatureModule:teardown(State); 193 | false -> 194 | undefined 195 | end. 196 | -------------------------------------------------------------------------------- /src/cucumberl_cli.erl: -------------------------------------------------------------------------------- 1 | -module(cucumberl_cli). 2 | -include("cucumberl.hrl"). 3 | -export([main/1]). 4 | 5 | %% TODO: introduce command line arguments to control things like 6 | %% (a) features directory 7 | %% (b) extra sources/directories to add to code:path 8 | %% (c) choice of feature(s) to be run 9 | %% (d) support for code coverage.... 10 | 11 | main(_) -> 12 | code:add_pathz("ebin"), 13 | Features = find_files("features", ".*\\.feature\$"), 14 | Outcomes = [ run_feature(F) || F <- Features ], 15 | case lists:all(fun(X) -> X =:= ok end, Outcomes) of 16 | true -> ok; 17 | false -> halt(1) 18 | end. 19 | 20 | run_feature(FeatureFile) -> 21 | %% is there a mod *named* for this feature? 22 | StepMod = ensure_loaded(list_to_atom(filename:basename(FeatureFile, 23 | ".feature"))), 24 | {ok, #cucumberl_stats{failures=Failed}} = 25 | cucumberl:run(FeatureFile, StepMod), 26 | case Failed of 27 | [] -> 28 | ok; 29 | _ -> 30 | io:format("~p failed steps.~n", [length(Failed)]), 31 | {failed, Failed} 32 | end. 33 | 34 | ensure_loaded(Mod) when is_atom(Mod) -> 35 | case code:ensure_loaded(Mod) of 36 | {error, _}=E -> 37 | throw(E); 38 | _ -> 39 | Mod 40 | end. 41 | 42 | find_files(Dir, Regex) -> 43 | filelib:fold_files(Dir, Regex, true, fun(F, Acc) -> [F | Acc] end, []). 44 | -------------------------------------------------------------------------------- /src/cucumberl_gen.erl: -------------------------------------------------------------------------------- 1 | -module(cucumberl_gen). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include("cucumberl.hrl"). 5 | 6 | -export([gen/2, gen/3]). 7 | 8 | gen(FeaturePath, OutputPath) 9 | when is_list(FeaturePath), is_list(OutputPath) -> 10 | {FeatureModule, Tree} = cucumberl_parser:parse(FeaturePath), 11 | gen_tree(OutputPath, Tree, FeatureModule). 12 | 13 | gen(FeaturePath, OutputPath, ImplementationModule) 14 | when is_list(FeaturePath), is_list(OutputPath) -> 15 | {_FeatureModule, Tree} = cucumberl_parser:parse(FeaturePath), 16 | gen_tree(OutputPath, Tree, ImplementationModule). 17 | 18 | gen_tree(OutputPath, Tree, FeatureModule) -> 19 | TargetPath = filename:join([OutputPath, 20 | atom_to_list(FeatureModule) ++ ".erl"]), 21 | Dict = lists:foldl(fun(Entry, Acc) -> 22 | process_line(Entry, Acc) 23 | end, 24 | dict:new(), 25 | Tree), 26 | Given = process_clauses(given, 27 | lists:reverse(sets:to_list(dict:fetch(given, Dict)))), 28 | When = process_clauses('when', 29 | lists:reverse(sets:to_list(dict:fetch('when', Dict)))), 30 | Then = process_clauses(then, 31 | lists:reverse(sets:to_list(dict:fetch(then, Dict)))), 32 | 33 | file:write_file(TargetPath, 34 | io_lib:format("-module(~s).~n~n" 35 | "-export([given/3, 'when'/3, then/3]).~n~n" 36 | "~s~n~n" 37 | "~s~n~n" 38 | "~s~n~n", 39 | [atom_to_list(FeatureModule), Given, 40 | When, Then])). 41 | 42 | process_line({Type, _, Tokens, _}, Dict0) -> 43 | 44 | %% Run through the FeatureModule steps, only if we are in a scenario 45 | %% section, otherwise, skip the line. 46 | case Type of 47 | {action, G} -> 48 | add_to_dict(G, Tokens, Dict0); 49 | _ -> 50 | Dict0 51 | end. 52 | 53 | format_step('when', Tokens) -> 54 | io_lib:format("'when'(~p, State, _) ->~n undefined", [Tokens]); 55 | format_step(GWT, Tokens) -> 56 | io_lib:format("~p(~p, State, _) ->~n undefined", [GWT, Tokens]). 57 | 58 | 59 | add_to_dict(Key, Value, Dict) -> 60 | case dict:find(Key, Dict) of 61 | {ok, OldValue} -> 62 | dict:store(Key, sets:add_element(Value, OldValue), Dict); 63 | error -> 64 | dict:store(Key, sets:from_list([Value]), Dict) 65 | end. 66 | 67 | process_clauses(G, [Clause | Rest]) -> 68 | process_clauses(G, Rest, [format_step(G, Clause)]). 69 | 70 | process_clauses(G, [Clause | Rest], Acc) -> 71 | process_clauses(G, Rest, [format_step(G, Clause), ";\n" | Acc]); 72 | process_clauses(_G, [], Acc) -> 73 | lists:reverse(["." | Acc]). 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/cucumberl_parser.erl: -------------------------------------------------------------------------------- 1 | -module(cucumberl_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, Line}, undefined, GWT0}; 101 | {_, ['scenario:' | _]} -> 102 | {{scenario, LineNum, Tokens, Line}, scenario, GWT0}; 103 | {_, ['scenario', 'outline:' | _]} -> 104 | {{scenario_outline, LineNum, Tokens, Line}, 105 | scenario, 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, G}, LineNum, 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 ")). 209 | --------------------------------------------------------------------------------