├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── debug.mk ├── erlang.mk ├── plugins.mk ├── rebar.config ├── src ├── ecucumber_ct.erl └── ecucumber_ct_context.erl └── test ├── assert.hrl └── ecucumber_ct_context_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar/ 2 | .erlang.mk/ 3 | log/ 4 | logs/ 5 | deps/ 6 | .eunit 7 | ebin 8 | *.o 9 | *.beam 10 | *.plt 11 | erl_crash.dump 12 | *.d 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Jabberbees SAS 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = ecucumber 2 | 3 | PROJECT_DESCRIPTION = An open source port of Cucumber for Erlang 4 | 5 | DEPS = egherkin 6 | 7 | dep_egherkin = git https://github.com/jabberbees/egherkin 8 | 9 | include $(if $(ERLANG_MK_FILENAME),$(ERLANG_MK_FILENAME),erlang.mk) 10 | 11 | include debug.mk 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecucumber 2 | 3 | ecucumber is an open source port of [Cucumber](https://cucumber.io/) for Erlang. 4 | 5 | Define automatic tests in Gherkin and execute them using Common Test. 6 | 7 | ## Goals 8 | 9 | ecucumber aims at providing a *simple* and *productive* BDD experience combining the best parts of Cucumber and Erlang. 10 | 11 | Testing a feature using ecucumber should be as *easy* as writing a Common Test test suite. 12 | 13 | ecucumber is *clean* and *well tested* Erlang code. 14 | 15 | ## How to use 16 | 17 | ecucumber only supports [erlang.mk](https://erlang.mk/) as a build tool for the moment. 18 | 19 | You only need to add ecucumber as a test dependency in your Makefile, before including erlang.mk. 20 | 21 | TEST_DEPS = ecucumber 22 | DEP_PLUGINS = ecucumber 23 | dep_ecucumber = git https://github.com/jabberbees/ecucumber.git 24 | 25 | ## Tutorial: the BDD workflow with ecucumber 26 | 27 | ### Create a new Erlang project 28 | 29 | Create an empty Erlang project 30 | 31 | Add erlang.mk to your project. 32 | 33 | Check [here](https://erlang.mk/guide/getting_started.html) if need you help to do this. 34 | 35 | ### Add the following Makefile 36 | 37 | PROJECT = test 38 | 39 | TEST_DEPS = ecucumber 40 | 41 | DEP_PLUGINS = ecucumber 42 | 43 | dep_ecucumber = git https://github.com/jabberbees/ecucumber.git 44 | 45 | include erlang.mk 46 | 47 | ### Add the first test 48 | 49 | File: features/addition.feature 50 | 51 | Feature: Addition 52 | 53 | Scenario: Add two numbers 54 | Given I have entered 50 into the calculator 55 | And I have entered 70 into the calculator 56 | When I press add 57 | Then the result should be 120 on the screen 58 | 59 | Please look at the reference guide below if you need more information on how to write tests. 60 | 61 | Run make once to have ecucumber detect the new test. 62 | 63 | make 64 | 65 | FYI you need to run make once each time you add new Gherkin tests. 66 | 67 | ### Execute tests 68 | At this stage your test is executable even though you have not provided any Erlang code. 69 | 70 | make ct 71 | 72 | The test fails but what did you expect? ;-) 73 | 74 | You need to provide the Erlang test code. 75 | 76 | ecucumber helps you by providing code templates for the missing step definitions. 77 | 78 | Common Test starting 79 | ... 80 | TEST INFO: 1 test(s), 1 case(s) in 1 suite(s) 81 | ... 82 | failed to find step definition! 83 | please add following implementation in one of your step definitions modules: 84 | 85 | step_def(given_keyword, [<<"I">>,<<"have">>,<<"entered">>,<<"50">>,<<"into">>, 86 | <<"the">>,<<"calculator">>], Context) -> 87 | Context. 88 | 89 | step_def(given_keyword, [<<"I">>,<<"have">>,<<"entered">>,<<"70">>,<<"into">>, 90 | <<"the">>,<<"calculator">>], Context) -> 91 | Context. 92 | 93 | step_def(when_keyword, [<<"I">>,<<"press">>,<<"add">>], Context) -> 94 | Context. 95 | 96 | step_def(then_keyword, [<<"the">>,<<"result">>,<<"should">>,<<"be">>, 97 | <<"120">>,<<"on">>,<<"the">>,<<"screen">>], Context) -> 98 | Context. 99 | ... 100 | Testing testing.test.feature_addition_SUITE: *** FAILED test case 1 of 1 *** 101 | Testing testing.test.feature_addition_SUITE: TEST COMPLETE, 0 ok, 1 failed of 1 test cases 102 | 103 | You just need to copy and paste these step definitions into a step definitions Erlang module. 104 | 105 | ### Provide step definitions modules 106 | Here is the complete step definitions module for the above test. 107 | 108 | File: test/maths_step_defs.erl 109 | 110 | -module(maths_step_defs). 111 | 112 | -export([setup/1, cleanup/1, step_def/3]). 113 | 114 | setup(Context) -> Context. 115 | 116 | cleanup(Context) -> Context. 117 | 118 | step_def(given_keyword, [<<"I">>,<<"have">>,<<"entered">>,Number,<<"into">>, 119 | <<"the">>,<<"calculator">>], Context) -> 120 | Value = binary_to_integer(Number), 121 | ecucumber_ct_context:add_value(numbers, Value, Context); 122 | step_def(when_keyword, [<<"I">>,<<"press">>,<<"add">>], Context) -> 123 | Numbers = ecucumber_ct_context:get_value(numbers, Context, []), 124 | Result = maths:add(Numbers), 125 | ecucumber_ct_context:set_value(result, Result, Context); 126 | step_def(then_keyword, [<<"the">>,<<"result">>,<<"should">>,<<"be">>, 127 | Expected,<<"on">>,<<"the">>,<<"screen">>], Context) -> 128 | Actual = ecucumber_ct_context:get_value(result, Context, undefined), 129 | Actual = binary_to_integer(Expected), 130 | Context; 131 | step_def(_GWT, _Pattern, _Context) -> 132 | nomatch. 133 | 134 | Notice that we use Erlang pattern matching to bind some parts to variables. 135 | 136 | Notice also that one of the step definitions is calling maths:add/1 which we still have not provided. This is the actual business code that we are writing tests for. 137 | 138 | ### Bind step definitions 139 | If you run the test again, you will get the same errors (missing step definitions) because we have not told ecucumber how to find our new step definitions. 140 | 141 | For this, you need to add @mod:\ Gherkin tags above the *Feature:* or relevant *Scenario:* keywords to activate your step definitions modules. 142 | 143 | File to modify: features/addition.feature 144 | 145 | @mod:maths_steps_defs 146 | Feature: Addition 147 | 148 | Scenario: Add two numbers 149 | Given I have entered 50 into the calculator 150 | And I have entered 70 into the calculator 151 | When I press add 152 | Then the result should be 120 on the screen 153 | 154 | ### Provide the test to code 155 | If you run the test now, you will get an *undef* error because one of the step definitions is calling maths:add/1 which we still have not provided. 156 | 157 | Testing testing.test.feature_addition_SUITE: Starting test, 1 test cases 158 | - - - - - - - - - - - - - - - - - - - - - - - - - - 159 | maths:add failed 160 | Reason: undef 161 | - - - - - - - - - - - - - - - - - - - - - - - - - - 162 | Testing testing.test.feature_addition_SUITE: *** FAILED test case 1 of 1 *** 163 | Testing testing.test.feature_addition_SUITE: TEST COMPLETE, 0 ok, 1 failed of 1 test cases 164 | 165 | maths is the actual business code that we are writing tests for. 166 | 167 | Here it is. 168 | 169 | File: src/maths.erl 170 | 171 | -module(maths). 172 | 173 | -export([add/1]). 174 | 175 | add(Numbers) -> 176 | lists:foldl(fun(N, A) -> A+N end, 0, Numbers). 177 | 178 | ### Run tests 179 | make ct 180 | 181 | And voilà! Your test passed! 182 | 183 | TEST INFO: 1 test(s), 1 case(s) in 1 suite(s) 184 | 185 | Testing testing.test.feature_addition_SUITE: Starting test, 1 test cases 186 | Testing testing.test.feature_addition_SUITE: TEST COMPLETE, 1 ok, 0 failed of 1 test cases 187 | 188 | ### Iterate 189 | And now the fun starts: 190 | - Add more tests 191 | - Add more step definitions 192 | - Add more business logic 193 | - Test, implement, modify, refactor until all tests pass 194 | 195 | ## Reference guide: writing tests 196 | 197 | ### Rules 198 | Write your tests using the [Gherkin syntax](https://docs.cucumber.io/gherkin/reference/). 199 | 200 | ecucumber will automatically process Gherkin files placed in the features/ sub-directory of your project with the .feature extension. 201 | 202 | ecucumber will generate a Common Test Erlang test module for each .feature file so lowercase filenames are highly recommended. 203 | 204 | ecucumber only supports english Gherkin keywords for the moment. This is a limitation of egherkin, the Gherkin parser used by ecucumber. 205 | 206 | ### Step definitions 207 | Testing code must be placed in step definitions Erlang modules. 208 | 209 | Step definitions modules must implement the following callbacks: 210 | 211 | setup(Context) -> Context 212 | 213 | Initialization code, called before each scenario or once at the very beginning depending on where the activation tag was placed. 214 | 215 | cleanup(Context) -> Context 216 | 217 | Cleanup code, called after each scenario or once at the very end depending on where the activation tag was placed. 218 | 219 | step_def(GWT, StepParts, Context) -> Context | nomatch 220 | 221 | Where: 222 | 223 | GWT = given_keyword | when_keyword | then_keyword 224 | StepParts = [StepPart] 225 | StepPart = binary() | DocString | DataTable 226 | DocString = {docstring, [Line :: binary()]} 227 | DataTable = {datatable, 228 | [RowName :: binary()], 229 | [{RowName :: binary(), Value :: binary()]} 230 | 231 | Use GWT and StepParts to match a step pattern. 232 | This is where you place your test code that gets called by ecucumber during execution. 233 | Either return a Context (modified or unchanged) when the step was handled or nomatch when no match was found. 234 | When nomatch is returned, the execution proceeds with following step definitions modules, otherwise the execution proceeds to the next step in the scenario with the new Context. 235 | 236 | ### Binding step definitions with tests 237 | Step definitions are associated with tests using *@mod:\* Gherkin tags. 238 | 239 | Example: the following tag will activate evaluation of step definitions in the maths_step_defs Erlang module 240 | 241 | @mod:maths_step_defs 242 | 243 | ### Step definitions evaluation 244 | 245 | All step definitions activated by a *@mod:\* Gherkin tag are evaluated by calling the *\:step_def/3* callback until one step definitions module returns a new context instead of nomatch. 246 | 247 | The order of evaluation is the following: 248 | - Scenario tags in order of declaration 249 | - Feature tags in order of 250 | declaration 251 | 252 | Example: 253 | 254 | @mod:common @mod:extras 255 | Feature: Addition 256 | 257 | @mod:addition @mod:calculator 258 | Scenario: Add two numbers 259 | Given I have entered 50 into the calculator 260 | And I have entered 70 into the calculator 261 | When I press add 262 | Then the result should be 120 on the screen 263 | 264 | Each step of the above scenario will trigger evaluation of the following step definitions modules, in this order: 265 | - addition 266 | - calculator 267 | - common 268 | - extras 269 | 270 | ## ecucumber and Git 271 | 272 | ecucumber generates Common Test suites in the test/ directory. 273 | 274 | You should have Git ignore these files. 275 | 276 | Add the following to your .gitignore file: 277 | 278 | test/feature_*.erl 279 | 280 | ## Next steps 281 | * Adding support for rebar 282 | * Adding support for eunit 283 | 284 | Please contribute! 285 | 286 | ## How to contribute 287 | You can contribute by using ecucumber. 288 | 289 | You can contribute by providing feedback through tickets. 290 | 291 | You can contribute by submitting code and then it's business as usual: 292 | - fork the project’s repository on GitHub by clicking on the Fork button. 293 | - clone your forked repository locally 294 | - create your local branch based on master 295 | - modify/extend the code and provide tests (untested changes will not be merged) 296 | - run all tests (commits with failing tests will not be merged) 297 | 298 | make ct 299 | 300 | - add your changes, commit and push 301 | - submit the pull request using the GitHub interface with an explanatory message 302 | 303 | ## Compatibility 304 | 305 | ecucumber was developed and tested with **Erlang/OTP R16B03-1** on Windows 10. 306 | 307 | The code only uses basic Erlang syntax and standard modules. It should work on all platforms and Erlang versions. 308 | 309 | Please report any compatibility issue you encounter by opening a ticket. 310 | -------------------------------------------------------------------------------- /debug.mk: -------------------------------------------------------------------------------- 1 | define debug-ct.erl 2 | ModuleSource = fun(Module) -> 3 | ModuleInfo = Module:module_info(), 4 | case lists:keyfind(compile, 1, ModuleInfo) of 5 | {compile, Compile} -> 6 | case lists:keyfind(source, 1, Compile) of 7 | {source, Source} -> 8 | Source; 9 | _ -> 10 | unavailable 11 | end; 12 | _ -> 13 | unavailable 14 | end 15 | end, 16 | BreakOn = fun(Flags) -> 17 | i:iaa(Flags) 18 | end, 19 | GetIaa = fun() -> 20 | case int:auto_attach() of 21 | false -> []; 22 | {Flags, _} -> Flags 23 | end 24 | end, 25 | AddBreakpoint = fun 26 | (Module) when is_atom(Module) -> 27 | io:format("Adding breakpoint: modude ~s~n", [Module]), 28 | i:im(), 29 | Src = ModuleSource(Module), 30 | case i:ii(Src) of 31 | {module, _} -> 32 | BreakOn(GetIaa() ++ [init]), 33 | ok; 34 | error -> error 35 | end; 36 | ({Module, LineOrLines}) when is_atom(Module) -> 37 | io:format("Adding breakpoint: modude ~s, lines ~p~n", [Module, LineOrLines]), 38 | i:im(), 39 | Src = ModuleSource(Module), 40 | case i:ii(Src) of 41 | {module, _} -> 42 | case LineOrLines of 43 | Line when is_integer(Line) -> i:ib(Module, Line); 44 | Lines when is_list(Lines) -> [i:ib(Module, Line) || Line <- Lines] 45 | end, 46 | BreakOn(GetIaa() ++ [break]), 47 | ok; 48 | error -> 49 | error 50 | end 51 | end, 52 | Breakpoints = [ecucumber_ct_context], 53 | [AddBreakpoint(Breakpoint) || Breakpoint <- Breakpoints], 54 | 55 | CTOpts = [ 56 | {auto_compile, false}, 57 | {dir, "test"}, 58 | {logdir, "logs"}, 59 | {suite, ecucumber_ct_context_SUITE}, 60 | {testcase, add_value_appends_value} 61 | ], 62 | case ct:run_test(CTOpts) of 63 | {Ok, Failed, {UserSkipped, AutoSkipped}} -> 64 | io:format("~p ok, ~p failed, ~p user skipped, ~p auto skipped~n", [Ok, Failed, UserSkipped, AutoSkipped]); 65 | {error, Reason} -> 66 | io:format("error: ~p~n", [Reason]) 67 | end, 68 | halt() 69 | endef 70 | 71 | CT_ERL_OPTS = -noinput \ 72 | -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ 73 | $(CT_EXTRA) \ 74 | $(CT_OPTS) 75 | 76 | debug-ct: test-build 77 | $(gen_verbose) $(call erlang,$(call debug-ct.erl,['$(t)']),$(CT_ERL_OPTS)) 78 | -------------------------------------------------------------------------------- /plugins.mk: -------------------------------------------------------------------------------- 1 | feature_verbose_0 = @echo " FEATURE " $(filter %.feature,$(?F)); 2 | feature_verbose = $(feature_verbose_$(V)) 3 | 4 | define compile_features 5 | $(verbose) mkdir -p ebin/ $(TEST_DIR) 6 | $(feature_verbose) $(call erlang,$(call compile_features.erl,$(1))) 7 | endef 8 | 9 | define compile_features.erl 10 | [begin 11 | case ecucumber_ct:generate_source(F, 12 | [{output_src_dir, "$(call core_native_path,$(TEST_DIR))"}]) of 13 | {ok, Filename} -> 14 | io:format(" -> ~s~n", [Filename]); 15 | {error, Reason} -> 16 | io:format(" error: ~p~n", [Reason]); 17 | {failed, Line, Reason} -> 18 | io:format(" error: ~p, line ~p~n", [Reason, Line]) 19 | end 20 | end || F <- string:tokens("$(1)", " ")], 21 | halt(). 22 | endef 23 | 24 | ifneq ($(wildcard features/),) 25 | ecucumber-features: $(sort $(call core_find,features/,*.feature)) 26 | $(if $(strip $?),$(call compile_features,$?)) 27 | 28 | ebin/$(PROJECT).app:: test-deps ecucumber-features 29 | endif 30 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [ 2 | {egherkin,".*",{git,"https://github.com/jabberbees/egherkin",""}} 3 | ]}. 4 | {erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard]}. 5 | -------------------------------------------------------------------------------- /src/ecucumber_ct.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2018, Jabberbees SAS 2 | 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | %% @author Emmanuel Boutin 16 | 17 | -module(ecucumber_ct). 18 | 19 | -export([ 20 | generate_source/2 21 | ]). 22 | 23 | -define(TAB, <<" ">>). 24 | -define(NL, <<"\n">>). 25 | 26 | generate_source(Filename, Options) -> 27 | case egherkin:parse_file(Filename) of 28 | {error, _} = Error -> 29 | Error; 30 | {failed, Line, Error} -> 31 | {error, {gherkin, Line, Error}}; 32 | Feature -> 33 | OutputFilename = ct_suite_filename(Filename, Options), 34 | generate_ct_suite(OutputFilename, Filename, Feature) 35 | end. 36 | 37 | ct_suite_filename(Filename, Options) -> 38 | OutputSrcDir = proplists:get_value(output_src_dir, Options, "./test"), 39 | Basename = filename:basename(Filename, ".feature"), 40 | filename:join(OutputSrcDir, "feature_" ++ Basename ++ "_SUITE.erl"). 41 | 42 | generate_ct_suite(Filename, SourceFilename, Feature) -> 43 | {AllCode, TestCaseCode} = generate_testcases(Feature), 44 | Source = [ 45 | generate_header(SourceFilename, Filename), 46 | generate_ct_callbacks(Feature), 47 | AllCode, 48 | TestCaseCode 49 | ], 50 | case file:write_file(Filename, Source) of 51 | ok -> {ok, Filename}; 52 | Else -> Else 53 | end. 54 | 55 | generate_header(SourceFilename, Filename) -> 56 | [ 57 | <<"% this file was generated by ecucumber">>, ?NL, 58 | <<"% from: ">>, SourceFilename, ?NL, 59 | ?NL, 60 | <<"-module(">>, filename:basename(Filename, ".erl"), <<").">>, ?NL, 61 | <<"-compile(export_all).">>, ?NL, 62 | ?NL, 63 | <<"-include_lib(\"common_test/include/ct.hrl\").">>, ?NL, 64 | ?NL 65 | ]. 66 | 67 | generate_ct_callbacks(Feature) -> 68 | FeatureName = egherkin_feature:name(Feature), 69 | Background = egherkin_feature:background(Feature), 70 | {ModList, TagList} = parse_tags(egherkin_feature:tag_names(Feature)), 71 | {SetupN, Setup} = generate_mod_calls(<<"setup">>, 0, ModList), 72 | {CleanupN, Cleanup} = generate_mod_calls(<<"cleanup">>, 0, lists:reverse(ModList)), 73 | {BackgroundN, BackgroundSrc} = generate_background(Background, 0), 74 | [ 75 | <<"init_per_suite(Config) ->">>, ?NL, 76 | ?TAB, s(0), <<" = ecucumber_ct_context:enter_feature(">>, b(FeatureName), <<", Config),">>, ?NL, 77 | Setup, 78 | ?TAB, s(SetupN), <<".">>, ?NL, 79 | ?NL, 80 | <<"end_per_suite(Config) ->">>, ?NL, 81 | ?TAB, s(0), <<" = ecucumber_ct_context:leave_feature(Config),">>, ?NL, 82 | Cleanup, 83 | ?TAB, s(CleanupN), <<".">>, ?NL, 84 | ?NL, 85 | <<"init_per_testcase(_TestCase, ">>, s(0), <<") ->">>, ?NL, 86 | BackgroundSrc, 87 | ?TAB, s(BackgroundN), <<".">>, ?NL, 88 | ?NL, 89 | <<"end_per_testcase(_TestCase, Config) ->">>, ?NL, 90 | ?TAB, <<"Config.">>, ?NL, 91 | ?NL, 92 | <<"feature_mods() ->">>, ?NL, 93 | ?TAB, a(ModList), <<".">>, ?NL, 94 | ?NL, 95 | <<"feature_tags() ->">>, ?NL, 96 | ?TAB, a(TagList), <<".">>, ?NL, 97 | ?NL 98 | ]. 99 | 100 | generate_testcases(Feature) -> 101 | Scenarios = egherkin_feature:scenarios(Feature), 102 | TestCases = lists:map(fun generate_testcase/1, Scenarios), 103 | {Names, Code} = lists:unzip(TestCases), 104 | AllCode = [ 105 | <<"all() -> [">>, ?NL, 106 | case Names of 107 | [] -> 108 | []; 109 | [First | Following] -> 110 | [ 111 | [?TAB, First], 112 | [[<<",">>, ?NL, ?TAB, Name] || Name <- Following], 113 | ?NL 114 | ] 115 | end, 116 | <<"].">>, ?NL, 117 | ?NL 118 | ], 119 | {AllCode, Code}. 120 | 121 | generate_background(undefined, N0) -> 122 | {N0, []}; 123 | generate_background(Background, N0) -> 124 | Steps = egherkin_background:steps(Background), 125 | {N, StepsSrc} = generate_steps(N0, Steps), 126 | Source = [ 127 | ?TAB, <<"Mods = feature_mods(),">>, ?NL, 128 | StepsSrc 129 | ], 130 | {N, Source}. 131 | 132 | generate_testcase(Scenario) -> 133 | Name = egherkin_scenario:name(Scenario), 134 | TestCaseName = atom_source(Name), 135 | {ModList, TagList} = parse_tags(egherkin_scenario:tag_names(Scenario)), 136 | Steps = egherkin_scenario:steps(Scenario), 137 | {N1, SetupSrc} = generate_mod_calls(<<"setup">>, 0, ModList), 138 | {N2, StepsSrc} = generate_steps(N1, Steps), 139 | {N3, CleanupSrc} = generate_mod_calls(<<"cleanup">>, N2, lists:reverse(ModList)), 140 | Source = [ 141 | TestCaseName, <<"(Config) ->">>, ?NL, 142 | ?TAB, <<"Tags = feature_tags() ++ ">>, a(TagList), <<",">>, ?NL, 143 | ?TAB, <<"Mods = feature_mods() ++ ">>, a(ModList), <<",">>, ?NL, 144 | ?TAB, s(0), <<" = ecucumber_ct_context:enter_scenario(">>, b(Name), <<", Tags, Config),">>, ?NL, 145 | SetupSrc, 146 | StepsSrc, 147 | CleanupSrc, 148 | ?TAB, <<"ecucumber_ct_context:leave_scenario(">>, s(N3), <<"),">>, ?NL, 149 | ?TAB, <<"ok.">>, ?NL, 150 | ?NL 151 | ], 152 | {TestCaseName, Source}. 153 | 154 | generate_mod_calls(Function, Start, ModList) -> 155 | Code = [ 156 | [?TAB, s(I+1), <<" = ">>, Mod, <<":">>, Function, <<"(">>, s(I), <<"),">>, ?NL] 157 | || {I, Mod} <- zipn(Start, ModList) 158 | ], 159 | {Start+length(ModList), Code}. 160 | 161 | generate_steps(Start, Steps) -> 162 | {_, Code} = lists:foldl(fun({I, {Line, GWTAB, StepParts}}, {LastGWT, Src}) -> 163 | GWT = case GWTAB of 164 | and_keyword -> LastGWT; 165 | but_keyword -> LastGWT; 166 | _ -> GWTAB 167 | end, 168 | PartsSrc = io_lib:format("~p", [StepParts]), 169 | StepSrc = [ 170 | ?TAB, s(I+1), <<" = ecucumber_ct_context:execute_step_def(">>, ?NL, 171 | ?TAB, ?TAB, integer_to_binary(Line), <<",">>, ?NL, 172 | ?TAB, ?TAB, atom_to_binary(GWT, latin1), <<",">>, ?NL, 173 | ?TAB, ?TAB, PartsSrc, <<",">>, ?NL, 174 | ?TAB, ?TAB, <<"Mods, ">>, s(I), <<"),">>, ?NL 175 | ], 176 | {GWT, [StepSrc | Src]} 177 | end, {undefined, []}, zipn(Start, Steps)), 178 | {Start+length(Steps), lists:reverse(Code)}. 179 | 180 | parse_tags(Tags) -> 181 | {ModList, TagList} = lists:foldl(fun(Tag, {M, T}) -> 182 | case parse_tag(Tag) of 183 | {mod, Module} -> {[Module | M], T}; 184 | {tag, Name} -> {M, [Name | T]} 185 | end 186 | end, {[], []}, Tags), 187 | {lists:reverse(ModList), lists:reverse(TagList)}. 188 | 189 | parse_tag(<<"mod:", Module/binary>>) -> {mod, Module}; 190 | parse_tag(Tag) -> {tag, Tag}. 191 | 192 | atom_source(Name) -> 193 | <<$', Name/binary, $'>>. 194 | 195 | a([]) -> 196 | <<"[]">>; 197 | a([One]) -> 198 | [<<"[">>, One, <<"]">>]; 199 | a([One | More]) -> 200 | [<<"[">>, One, [[<<", ">>, Item] || Item <- More], <<"]">>]. 201 | 202 | b(Name) -> 203 | <<$<, $<, $", Name/binary, $", $>, $>>>. 204 | 205 | s(I) -> 206 | N = integer_to_binary(I), 207 | <<$S, N/binary>>. 208 | 209 | zipn(Start, List) -> 210 | Ns = lists:seq(Start, Start+length(List)-1), 211 | lists:zip(Ns, List). 212 | -------------------------------------------------------------------------------- /src/ecucumber_ct_context.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2018, Jabberbees SAS 2 | 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | %% @author Emmanuel Boutin 16 | 17 | -module(ecucumber_ct_context). 18 | 19 | -export([ 20 | new/0, 21 | new/1, 22 | 23 | enter_feature/2, 24 | leave_feature/1, 25 | 26 | enter_scenario/3, 27 | leave_scenario/1, 28 | 29 | execute_step_def/5, 30 | 31 | is_defined/2, 32 | get_value/3, 33 | set_value/3, 34 | add_value/3, 35 | delete_value/2, 36 | delete_values/2 37 | ]). 38 | 39 | new() -> 40 | []. 41 | 42 | new(PropList) -> 43 | PropList. 44 | 45 | enter_feature(FeatureName, Context) -> 46 | ct:pal("Entering feature: ~s", [FeatureName]), 47 | Context. 48 | 49 | leave_feature(Context) -> 50 | Context. 51 | 52 | enter_scenario(ScenarioName, Tags, Context) -> 53 | ct:pal("Entering scenario: ~s", [ScenarioName]), 54 | [{ecucumber_tags, Tags} | Context]. 55 | 56 | leave_scenario(Context) -> 57 | case proplists:get_value(ecucumber_failed, Context, false) of 58 | true -> ct:fail(missing_step_definitions), Context; 59 | false -> Context 60 | end. 61 | 62 | execute_step_def(Line, GWT, StepParts, Mods, Context) -> 63 | ct:log(info, 64 | "[~p] ~s ~s", 65 | [Line, egherkin_lib:format_gwt(GWT), egherkin_lib:format_step_parts(StepParts)] 66 | ), 67 | Context1 = set_value(ecucumber_line, Line, Context), 68 | execute_step_def(GWT, StepParts, Mods, Context1). 69 | 70 | execute_step_def(GWT, StepParts, [], Context) -> 71 | ct:pal(error, 72 | "failed to find step definition!~n" 73 | "please add following implementation in one of your step definitions modules:~n" 74 | "~n" 75 | "step_def(~s, ~p, Context) ->~n" 76 | " Context." 77 | "~n", 78 | [GWT, StepParts] 79 | ), 80 | [{ecucumber_failed, true} | Context]; 81 | execute_step_def(GWT, StepParts, [Mod | Mods], Context) -> 82 | case Mod:step_def(GWT, StepParts, Context) of 83 | nomatch -> 84 | execute_step_def(GWT, StepParts, Mods, Context); 85 | NewContext -> 86 | ct:log(info, "done"), 87 | NewContext 88 | end. 89 | 90 | is_defined(Key, Context) -> 91 | proplists:is_defined(Key, Context). 92 | 93 | get_value(Key, Context, Default) -> 94 | proplists:get_value(Key, Context, Default). 95 | 96 | set_value(Key, Value, Context) -> 97 | Context2 = lists:keydelete(Key, 1, Context), 98 | [{Key, Value} | Context2]. 99 | 100 | add_value(Key, Value, Context) -> 101 | case lists:keytake(Key, 1, Context) of 102 | {value, {_, List}, Context2} -> [{Key, List ++ [Value]} | Context2]; 103 | false -> [{Key, [Value]} | Context] 104 | end. 105 | 106 | delete_value(Key, Context) -> 107 | lists:keydelete(Key, 1, Context). 108 | 109 | delete_values(Keys, Context) -> 110 | lists:foldl(fun(Key, Acc) -> 111 | lists:keydelete(Key, 1, Acc) 112 | end, Context, Keys). 113 | -------------------------------------------------------------------------------- /test/assert.hrl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2018, Jabberbees SAS 2 | 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | %% @author Emmanuel Boutin 16 | 17 | -ifndef(ASSERT). 18 | -define(ASSERT, 1). 19 | 20 | -define(debugVal(Expr), 21 | begin 22 | ct:pal( 23 | "module: ~s (line ~p)~n" 24 | "expression: ~s~n" 25 | "value: ~p~n", 26 | [?MODULE, ?LINE, (??Expr), Expr]) 27 | end). 28 | 29 | -define(assert(Expr), 30 | begin 31 | ((fun (__X) -> 32 | case (Expr) of 33 | __X -> ok; 34 | __V -> 35 | ct:fail( 36 | "assertion failed: assert~n" 37 | "module: ~s (line ~p)~n" 38 | "expression: ~s~n" 39 | "expected: ~p~n" 40 | "actual: ~p~n", 41 | [?MODULE, ?LINE, (??Expr), __X, __V]) 42 | end 43 | end)(true)) 44 | end). 45 | 46 | -define(assertEqual(Expected, Expr), 47 | begin 48 | ((fun (__X) -> 49 | case (Expr) of 50 | __X -> ok; 51 | __V -> 52 | ct:fail( 53 | "assertion failed: assertEqual~n" 54 | "module: ~s (line ~p)~n" 55 | "expression: ~s~n" 56 | "expected: ~p~n" 57 | "actual: ~p~n", 58 | [?MODULE, ?LINE, (??Expr), __X, __V]) 59 | end 60 | end)(Expected)) 61 | end). 62 | 63 | -define(assertMatch(Guard, Expr), 64 | begin 65 | ((fun () -> 66 | case (Expr) of 67 | Guard -> ok; 68 | __V -> 69 | ct:fail( 70 | "assertion failed: assertMatch~n" 71 | "module: ~s (line ~p)~n" 72 | "expression: ~s~n" 73 | "pattern: ~s~n" 74 | "actual: ~p~n", 75 | [?MODULE, ?LINE, (??Expr), (??Guard), __V]) 76 | end 77 | end)()) 78 | end). 79 | 80 | -define(assertLength(Expected, Expr), 81 | begin 82 | ((fun (__X) -> 83 | case length(Expr) of 84 | __X -> ok; 85 | __V -> 86 | ct:fail( 87 | "assertion failed: assertLength~n" 88 | "module: ~s (line ~p)~n" 89 | "expression: ~s~n" 90 | "expected: ~p~n" 91 | "actual: ~p~n", 92 | [?MODULE, ?LINE, (??Expr), __X, __V]) 93 | end 94 | end)(Expected)) 95 | end). 96 | 97 | -define(assertKeyValue(Expected, Key, PropList), 98 | begin 99 | ((fun (__X) -> 100 | case proplists:get_value(Key, PropList) of 101 | __X -> ok; 102 | __V -> 103 | ct:fail( 104 | "assertion failed: assertKeyValue~n" 105 | "module: ~s (line ~p)~n" 106 | "key: ~s~n" 107 | "expected: ~p~n" 108 | "actual: ~p~n", 109 | [?MODULE, ?LINE, Key, __X, __V]) 110 | end 111 | end)(Expected)) 112 | end). 113 | 114 | -define(assertKeyExists(Key, PropList), 115 | begin 116 | ((fun (__X) -> 117 | case proplists:is_defined(Key, PropList) of 118 | __X -> ok; 119 | __V -> 120 | ct:fail( 121 | "assertion failed: assertKeyExists~n" 122 | "module: ~s (line ~p)~n" 123 | "key: ~s~n" 124 | "exists: ~s~n", 125 | [?MODULE, ?LINE, Key, __V]) 126 | end 127 | end)(true)) 128 | end). 129 | 130 | -endif. 131 | -------------------------------------------------------------------------------- /test/ecucumber_ct_context_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2018, Jabberbees SAS 2 | 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | %% @author Emmanuel Boutin 16 | 17 | -module(ecucumber_ct_context_SUITE). 18 | -compile(export_all). 19 | 20 | -include_lib("common_test/include/ct.hrl"). 21 | -include_lib("assert.hrl"). 22 | 23 | init_per_suite(Config) -> 24 | Config. 25 | 26 | end_per_suite(Config) -> 27 | Config. 28 | 29 | init_per_testcase(_TestCase, Config) -> 30 | Config. 31 | 32 | end_per_testcase(_TestCase, Config) -> 33 | Config. 34 | 35 | all() -> [ 36 | is_defined_returns_false, 37 | is_defined_returns_true, 38 | 39 | set_value_sets_value, 40 | set_value_overrides_previous_value, 41 | 42 | add_value_sets_value, 43 | add_value_appends_value, 44 | 45 | delete_value_removes_value, 46 | delete_value_does_nothing, 47 | 48 | delete_values_removes_value, 49 | delete_values_does_nothing 50 | ]. 51 | 52 | %%region is_defined 53 | 54 | is_defined_returns_false(_) -> 55 | C = [], 56 | ?assertEqual(false, ecucumber_ct_context:is_defined(items, C)), 57 | ok. 58 | 59 | is_defined_returns_true(_) -> 60 | C = ecucumber_ct_context:add_value(items, 42, []), 61 | ?assertEqual(true, ecucumber_ct_context:is_defined(items, C)), 62 | ok. 63 | 64 | %%endregion 65 | 66 | %%region set_value 67 | 68 | set_value_sets_value(_) -> 69 | C = ecucumber_ct_context:set_value(items, 42, []), 70 | ?assertEqual(42, ecucumber_ct_context:get_value(items, C, [])), 71 | ok. 72 | 73 | set_value_overrides_previous_value(_) -> 74 | C1 = ecucumber_ct_context:set_value(items, 42, []), 75 | C2 = ecucumber_ct_context:set_value(items, 314, C1), 76 | ?assertEqual(314, ecucumber_ct_context:get_value(items, C2, 0)), 77 | ok. 78 | 79 | %%endregion 80 | 81 | %%region add_value 82 | 83 | add_value_sets_value(_) -> 84 | C = ecucumber_ct_context:add_value(items, 42, []), 85 | ?assertEqual([42], ecucumber_ct_context:get_value(items, C, [])), 86 | ok. 87 | 88 | add_value_appends_value(_) -> 89 | C1 = ecucumber_ct_context:add_value(items, 42, []), 90 | C2 = ecucumber_ct_context:add_value(items, 314, C1), 91 | ?assertEqual([42, 314], ecucumber_ct_context:get_value(items, C2, [])), 92 | ok. 93 | 94 | %%endregion 95 | 96 | %%region delete_value 97 | 98 | delete_value_removes_value(_) -> 99 | C1 = ecucumber_ct_context:set_value(items, 42, []), 100 | C2 = ecucumber_ct_context:delete_value(items, C1), 101 | ?assertEqual(false, ecucumber_ct_context:is_defined(items, C2)), 102 | ok. 103 | 104 | delete_value_does_nothing(_) -> 105 | C = ecucumber_ct_context:set_value(stuff, 42, []), 106 | ?assertEqual(C, ecucumber_ct_context:delete_value(items, C)), 107 | ok. 108 | 109 | %%endregion 110 | 111 | %%region delete_values 112 | 113 | delete_values_removes_value(_) -> 114 | C1 = ecucumber_ct_context:set_value(items, 42, []), 115 | C2 = ecucumber_ct_context:delete_values([items], C1), 116 | ?assertEqual(false, ecucumber_ct_context:is_defined(items, C2)), 117 | ok. 118 | 119 | delete_values_does_nothing(_) -> 120 | C = ecucumber_ct_context:set_value(stuff, 42, []), 121 | ?assertEqual(C, ecucumber_ct_context:delete_values([items], C)), 122 | ok. 123 | 124 | %%endregion 125 | --------------------------------------------------------------------------------