├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CONTRIBUTE.md ├── Gruntfile.js ├── History.md ├── LICENSE ├── README.md ├── bin ├── parallel-cucumber-js └── parallel-cucumber-js-worker ├── features ├── background.feature ├── cucumber_events.feature ├── custom_cucumber.feature ├── custom_formatter.feature ├── dry_run.feature ├── empty.feature ├── environment_variables.feature ├── failing.feature ├── json_formatter.feature ├── parallel.feature ├── passing.feature ├── profile_environment_variable.feature ├── progress_formatter.feature ├── require.feature ├── retries.feature ├── scenario_outline.feature ├── step_definitions │ └── bin.js ├── support │ └── world.js └── tags.feature ├── lib ├── cucumber │ └── runtime │ │ ├── event_broadcaster.js │ │ └── features_runner.js ├── parallel_cucumber.js ├── parallel_cucumber │ ├── cli.js │ ├── cli │ │ ├── configuration.js │ │ └── main.js │ ├── formatters.js │ ├── formatters │ │ ├── formatter.js │ │ ├── json_formatter.js │ │ └── progress_formatter.js │ ├── runtime.js │ ├── runtime │ │ ├── feature_finder.js │ │ ├── support_code_finder.js │ │ └── worker_pool.js │ └── workers.js ├── parallel_cucumber_worker.js └── parallel_cucumber_worker │ ├── cli.js │ └── cli │ └── main.js ├── package.json └── test_assets ├── features ├── background.feature ├── empty.feature ├── environment_variables.feature ├── failing.feature ├── parallel │ ├── blue.feature │ ├── purple.feature │ └── red.feature ├── passing.feature ├── pending.feature ├── profile_environment_variable.feature ├── retries.feature ├── scenario_outline.feature ├── step_definitions │ ├── environment_variables.js │ ├── failing.js │ ├── passing.js │ ├── pending.js │ └── retries.js ├── support │ ├── hooks.js │ └── world.js ├── tags.feature └── undefined.feature └── lib ├── custom_cucumber.js └── formatters ├── custom_formatter.js └── null_formatter.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules 3 | npm-debug.log 4 | /tmp/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase":true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "immed": true, 7 | "indent": 2, 8 | "latedef": "nofunc", 9 | "newcap": false, 10 | "noarg": true, 11 | "nonew": true, 12 | "quotmark": true, 13 | "undef": true, 14 | "unused": true, 15 | "trailing": true, 16 | 17 | "eqnull": true, 18 | "sub": true, 19 | 20 | "node": true 21 | } 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | temp.js 4 | tmp/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6.9.4" 5 | - "5" 6 | - "4" 7 | - "0.12" 8 | - "0.10" 9 | 10 | before_install: 11 | - node --version 12 | - npm --version 13 | 14 | install: 15 | - npm install 16 | - npm install cucumber@~1.3.3 17 | 18 | notifications: 19 | email: 20 | - simon@simondean.org 21 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | ## Release checklist 2 | 3 | Here are the steps to follow when making a release. 4 | 5 | * Update `History.md` 6 | * Bump version in `package.json` 7 | * Add new contributors to `package.json`, if any 8 | * Commit those changes as "*Release 0.1.2*" (where *0.1.2* is the actual version, of course) 9 | * `$ git commit -m "Release 0.1.2"` 10 | * Tag commit as "v0.1.2" with short description of main changes 11 | * `$ git tag -a v0.1.2 -m "Description of changes"` 12 | * Push to main repo on GitHub 13 | * `$ git push origin master` 14 | * `$ git push origin v0.1.2` 15 | * Wait for build to go green 16 | * Publish to NPM 17 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | 6 | jshint: { 7 | all: [ 8 | 'Gruntfile.js', 9 | 'features/**/*.js', 10 | 'example/**/*.js', 11 | 'lib/**/*.js' 12 | ], 13 | options: { 14 | jshintrc: '.jshintrc' 15 | } 16 | }, 17 | 18 | clean: { 19 | }, 20 | 21 | shell: { 22 | features: { 23 | command: 'node bin/parallel-cucumber-js --workers 4' 24 | } 25 | } 26 | 27 | }); 28 | 29 | grunt.loadNpmTasks('grunt-contrib-jshint'); 30 | grunt.loadNpmTasks('grunt-contrib-clean'); 31 | grunt.loadNpmTasks('grunt-shell'); 32 | 33 | grunt.registerTask('features', ['shell:features']); 34 | 35 | grunt.registerTask('test', ['jshint', 'features']); 36 | grunt.registerTask('default', ['test']); 37 | 38 | }; -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # parallel-cucumber changelog 2 | 3 | ## [v1.1.x](https://github.com/simondean/parallel-cucumber-js/compare/v1.1.0...master) 4 | 5 | ### [master (unreleased)](https://github.com/simondean/parallel-cucumber-js/compare/v1.1.0...master) 6 | 7 | ### [v1.1.0](https://github.com/simondean/parallel-cucumber-js/compare/v1.0.1...v1.1.0) 8 | 9 | #### New features 10 | * Upgraded to cucumber-js v1.3.3 (#26) 11 | 12 | ## [v1.0.x](https://github.com/simondean/parallel-cucumber-js/compare/v1.0.0...v1.1.0) 13 | 14 | ### [v1.0.1](https://github.com/simondean/parallel-cucumber-js/compare/v1.0.0...v1.0.1) 15 | 16 | #### Fixes 17 | * Backgrounds no longer a failure (closes #20) 18 | 19 | ### [v1.0.0](https://github.com/simondean/parallel-cucumber-js/compare/v0.1.7...v1.0.0) 20 | 21 | #### New features 22 | * Upgraded to latest version of Cucumber, v1.0.0 (closes #16) 23 | * Executes scenarios in parallel rather than just executing features in parallel (closes #14) 24 | 25 | ## [v0.1.x](https://github.com/simondean/parallel-cucumber-js/compare/v0.1.0...v1.0.0) 26 | 27 | ### [v0.1.7](https://github.com/simondean/parallel-cucumber-js/compare/v0.1.6...v0.1.7) 28 | 29 | #### New features 30 | * Additional tests for retries 31 | * Removed retry count from feature and scenario ids and added it to the feature names 32 | 33 | #### Fixes 34 | * Formatters could not emit error events 35 | 36 | ### [v0.1.6](https://github.com/simondean/parallel-cucumber-js/compare/v0.1.5...v0.1.6) 37 | 38 | #### Fixes 39 | * Retries should not fail when an element in the report does not have an id 40 | 41 | ### [v0.1.5](https://github.com/simondean/parallel-cucumber-js/compare/v0.1.4...v0.1.5) 42 | 43 | #### New features 44 | * Retry failed features (closes #11) 45 | 46 | ### [v0.1.4](https://github.com/simondean/parallel-cucumber-js/compare/v0.1.3...v0.1.4) 47 | 48 | #### New features 49 | * Removed the -c command line argument (closes #7) 50 | * Included the tests in the npm package 51 | * Restructured the tests 52 | * The tests are less reliant on tags 53 | * Additional tests for tags 54 | 55 | #### Fixes 56 | * BeforeFeatures and AfterFeatures events should not fire for every feature (closes #8) 57 | 58 | ### [v0.1.3](https://github.com/simondean/parallel-cucumber-js/compare/v0.1.2...v0.1.3) 59 | 60 | #### New features 61 | * Updated dependencies 62 | 63 | ### [v0.1.2](https://github.com/simondean/parallel-cucumber-js/compare/v0.1.1...v0.1.2) 64 | 65 | #### New features 66 | * Use Travis CI for builds (closes #3) 67 | * Setting environment variables on a profile (closes #6) 68 | 69 | #### Fixes 70 | * Allow cucumber-js 0.4.x to be used (closes #2) 71 | * Error with progress formatter when a step has no result (closes #4) 72 | 73 | ### [v0.1.1](https://github.com/simondean/parallel-cucumber-js/compare/v0.1.0...v0.1.1) 74 | 75 | #### New features 76 | * Can now specify a customized version of cucumber-js to load. Can specify a relative path or custom module name 77 | * Implemented a work around for cucumber-js lacking a dry run mode. The workaround uses an environment variable called PARALLEL_CUCUMBER_DRY_RUN 78 | * Improved the output of the progress formatter. Nested JSON in error messages is now converted to YAML, which makes it much easier to read the error messages 79 | * Added additional tests for the --profiles.NAME.tags command line arguments 80 | 81 | #### Fixes 82 | * Fixed a bug where passing only one -r/--require command line argument did not work. Specifying more than one of those arguments was working 83 | 84 | ### [v0.1.0](https://github.com/simondean/parallel-cucumber-js/tree/v0.1.0) 85 | 86 | Initial release 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2014 Simon Dean 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parallel-cucumber-js 2 | [![Build Status](https://travis-ci.org/simondean/parallel-cucumber-js.png?branch=master)](https://travis-ci.org/simondean/parallel-cucumber-js) 3 | [![Dependencies](https://david-dm.org/simondean/parallel-cucumber-js.png)](https://david-dm.org/simondean/parallel-cucumber-js) 4 | [![Code Climate](https://codeclimate.com/github/simondean/parallel-cucumber-js.png)](https://codeclimate.com/github/simondean/parallel-cucumber-js) 5 | 6 | [![NPM](https://nodei.co/npm/parallel-cucumber.png?stars&downloads)](https://nodei.co/npm/parallel-cucumber/) 7 | [![NPM](https://nodei.co/npm-dl/parallel-cucumber.png)](https://nodei.co/npm/parallel-cucumber/) 8 | 9 | Executes Cucumber scenarios in parallel, reducing the amount of time tests take to execute. 10 | 11 | parallel-cucumber-js uses multiple node.js processes to execute more than one Cucumber feature at a time. This can 12 | greatly reduce the time it takes for a test suite to execute. However, a test suite needs to be built with 13 | parallization in mind; especially when the Cucumber features are accessing shared resources like a database-backed 14 | web service. 15 | 16 | ## Usage 17 | 18 | ### Install 19 | 20 | parallel-cucumber-js is available as an npm module called parallel-cucumber. 21 | 22 | parallel-cucumber-js should be added to your test codebase as a dev dependency. You can do this with: 23 | 24 | ``` shell 25 | $ npm install --save-dev parallel-cucumber 26 | ``` 27 | 28 | Alternatively you can manually add it to your package.json file: 29 | 30 | ``` json 31 | { 32 | "devDependencies" : { 33 | "parallel-cucumber": "latest" 34 | } 35 | } 36 | ``` 37 | 38 | then install with: 39 | 40 | ``` shell 41 | $ npm install --dev 42 | ``` 43 | 44 | ### Run 45 | 46 | parallel-cucumber-js can be ran from a terminal as follows: 47 | 48 | ``` shell 49 | $ node_modules/.bin/parallel-cucumber-js 50 | ``` 51 | 52 | By default parallel-cucumber will look for features files under a directory called `./features` 53 | 54 | ### Number of Workers 55 | 56 | The number of features that will be executed in parallel can be set by passing the `-w` argument: 57 | 58 | ``` shell 59 | $ node_modules/.bin/parallel-cucumber-js -w 4 60 | ``` 61 | 62 | Setting the number of workers controls the amount of parallization. The larger the number of workers, the more 63 | Cucumber feature will be executed in parallel. By default the number of workers is set to the number of CPU cores in 64 | the machine running parallel-cucumber. 65 | 66 | ### Profiles 67 | 68 | parallel-cucumber can execute the same scenario multiple times. This can be useful for things like executing the same 69 | tests against both a desktop browser and mobile browser. 70 | 71 | ``` shell 72 | $ node_modules/.bin/parallel-cucumber-js --profile.desktop.tags ~@mobile-only --profile.mobile.tags ~@desktop-only 73 | ``` 74 | 75 | parallel-cucumber sets an environment variable called PARALLEL_CUCUMBER_PROFILE which can be used within the 76 | Cucumber step defs and support code to determine which profile is currently executing. 77 | 78 | ### Environment variables 79 | 80 | Environment variables can be specified for a profile and those environment variables will be set when the Cucumber scenarios 81 | are executed for the profile: 82 | 83 | ``` shell 84 | $ node_modules/.bin/parallel-cucumber-js --profile.desktop.env.EXAMPLE_NAME example_value --profile.desktop.env.EXAMPLE_NAME_2 example_value_2 85 | ``` 86 | 87 | ### Formats 88 | 89 | Two output formats are supported: json and progress. The progress format is the default. The `-f` argument is used 90 | to configure a different format: 91 | 92 | ``` shell 93 | $ node_modules/.bin/parallel-cucumber-js -f json 94 | ``` 95 | 96 | By default, output is sent to the console but you can also send it to a file: 97 | 98 | ``` shell 99 | $ node_modules/.bin/parallel-cucumber-js -f json:./output.json 100 | ``` 101 | 102 | You can configure multiple formats, with each format configured to output to the console or a file: 103 | 104 | ``` shell 105 | $ node_modules/.bin/parallel-cucumber-js -f json:./output.json -f progress 106 | ``` 107 | 108 | ### Example 109 | 110 | See https://github.com/simondean/parallel-cucumber-js-example for an example 111 | test codebase that uses parallel-cucumber-js. 112 | -------------------------------------------------------------------------------- /bin/parallel-cucumber-js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var ParallelCucumber = require('../lib/parallel_cucumber'); 6 | var main = ParallelCucumber.Cli.Main(); 7 | main.start(); 8 | -------------------------------------------------------------------------------- /bin/parallel-cucumber-js-worker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var ParallelCucumberWorker = require('../lib/parallel_cucumber_worker'); 6 | var main = ParallelCucumberWorker.Cli.Main(); 7 | main.start(); 8 | -------------------------------------------------------------------------------- /features/background.feature: -------------------------------------------------------------------------------- 1 | Feature: Background 2 | 3 | Scenario: Background 4 | Given the 'background' feature 5 | And a 'json' formatter 6 | When executing the parallel-cucumber-js bin 7 | Then the exit code should be '0' 8 | And stdout should contain JSON matching: 9 | """ 10 | [ 11 | { 12 | "id": "background", 13 | "keyword": "Feature", 14 | "line": 1, 15 | "name": "Background", 16 | "tags": [], 17 | "uri": "{uri}/features/background.feature", 18 | "elements": [ 19 | { 20 | "id": "background;passing", 21 | "keyword": "Scenario", 22 | "line": 6, 23 | "name": "Passing", 24 | "steps": [ 25 | { 26 | "arguments": [], 27 | "keyword": "Given ", 28 | "name": "a passing pre-condition", 29 | "result": { 30 | "status": "passed", 31 | "duration": "{duration}" 32 | }, 33 | "line": 4, 34 | "match": { 35 | "location": "{location}" 36 | } 37 | }, 38 | { 39 | "arguments": [], 40 | "keyword": "When ", 41 | "name": "a passing action is executed", 42 | "result": { 43 | "status": "passed", 44 | "duration": "{duration}" 45 | }, 46 | "line": 7, 47 | "match": { 48 | "location": "{location}" 49 | } 50 | }, 51 | { 52 | "arguments": [], 53 | "keyword": "Then ", 54 | "name": "a post-condition passes", 55 | "result": { 56 | "status": "passed", 57 | "duration": "{duration}" 58 | }, 59 | "line": 8, 60 | "match": { 61 | "location": "{location}" 62 | } 63 | } 64 | ], 65 | "tags": [], 66 | "type": "scenario" 67 | } 68 | ], 69 | "profile": "default", 70 | "retry": 0 71 | } 72 | ] 73 | """ 74 | And stderr should be empty 75 | -------------------------------------------------------------------------------- /features/cucumber_events.feature: -------------------------------------------------------------------------------- 1 | Feature: Cucumber events 2 | 3 | Scenario: BeforeFeatures and AfterFeatures only fired once 4 | Given the environment variable 'LOG_CUCUMBER_EVENTS' is set to 'true' 5 | And the 'passing' feature 6 | And a profile called 'test_profile_1' 7 | And a profile called 'test_profile_2' 8 | And a './lib/formatters/null_formatter' formatter 9 | And '1' worker 10 | When executing the parallel-cucumber-js bin 11 | Then the exit code should be '0' 12 | And stdout should contain text matching: 13 | """ 14 | Before features 15 | Before feature 16 | Before scenario 17 | Before step 18 | Step result 19 | After step 20 | Before step 21 | Step result 22 | After step 23 | Before step 24 | Step result 25 | After step 26 | After scenario 27 | After feature 28 | Before feature 29 | Before scenario 30 | Before step 31 | Step result 32 | After step 33 | Before step 34 | Step result 35 | After step 36 | Before step 37 | Step result 38 | After step 39 | After scenario 40 | After feature 41 | After features 42 | """ 43 | And stderr should be empty 44 | 45 | Scenario: BeforeFeatures and AfterFeatures are not fired when there are no features 46 | Given the environment variable 'LOG_CUCUMBER_EVENTS' is set to 'true' 47 | And the 'empty' feature 48 | And a profile called 'test_profile' 49 | And the 'test_profile' profile has the tag '@does-not-exist' 50 | And a './lib/formatters/null_formatter' formatter 51 | And '1' worker 52 | When executing the parallel-cucumber-js bin 53 | Then the exit code should be '0' 54 | And stdout should contain text matching: 55 | """ 56 | """ 57 | And stderr should be empty 58 | 59 | Scenario: BeforeFeatures and AfterFeatures are not fired when a worker has no work 60 | Given the environment variable 'LOG_CUCUMBER_EVENTS' is set to 'true' 61 | And the 'empty' feature 62 | And a profile called 'test_profile' 63 | And the 'test_profile' profile has the tag '@does-not-exist' 64 | And a './lib/formatters/null_formatter' formatter 65 | And '2' worker 66 | When executing the parallel-cucumber-js bin 67 | Then the exit code should be '0' 68 | And stdout should contain text matching: 69 | """ 70 | """ 71 | And stderr should be empty 72 | -------------------------------------------------------------------------------- /features/custom_cucumber.feature: -------------------------------------------------------------------------------- 1 | Feature: Custom Cucumber 2 | 3 | Scenario: Custom Cucumber 4 | Given the 'passing' feature 5 | And a 'json' formatter 6 | And the './lib/custom_cucumber' custom version of Cucumber 7 | When executing the parallel-cucumber-js bin 8 | Then the exit code should be '0' 9 | And stdout should contain JSON matching: 10 | """ 11 | [ 12 | { 13 | "custom_cucumber": true, 14 | "id": "passing", 15 | "name": "Passing", 16 | "tags": [], 17 | "line": 1, 18 | "keyword": "Feature", 19 | "uri": "{uri}/features/passing.feature", 20 | "elements": [], 21 | "profile": "default", 22 | "retry": 0 23 | } 24 | ] 25 | """ 26 | And stderr should be empty 27 | -------------------------------------------------------------------------------- /features/custom_formatter.feature: -------------------------------------------------------------------------------- 1 | Feature: Custom formatter 2 | 3 | Scenario: Custom formatter 4 | Given the 'passing' feature 5 | And a './lib/formatters/custom_formatter' formatter 6 | When executing the parallel-cucumber-js bin 7 | Then the exit code should be '0' 8 | And stdout should contain text matching: 9 | """ 10 | Start 11 | Feature passing 12 | End 13 | """ 14 | And stderr should be empty 15 | -------------------------------------------------------------------------------- /features/dry_run.feature: -------------------------------------------------------------------------------- 1 | Feature: Dry run 2 | 3 | Scenario: Dry run does not execute step definitions 4 | Given the 'failing' feature 5 | And a 'json' formatter 6 | And dry run mode 7 | When executing the parallel-cucumber-js bin 8 | Then the exit code should be '0' 9 | And stdout should contain JSON matching: 10 | """ 11 | [ 12 | { 13 | "id": "failing", 14 | "name": "Failing", 15 | "tags": [], 16 | "line": 1, 17 | "keyword": "Feature", 18 | "uri": "{uri}/features/failing.feature", 19 | "elements": [ 20 | { 21 | "name": "Failing", 22 | "id": "failing;failing", 23 | "line": 3, 24 | "keyword": "Scenario", 25 | "tags": [], 26 | "type": "scenario", 27 | "steps": [ 28 | { 29 | "arguments": [], 30 | "name": "a passing pre-condition", 31 | "line": 4, 32 | "keyword": "Given ", 33 | "result": { 34 | "duration": "{duration}", 35 | "status": "passed" 36 | }, 37 | "match": { 38 | "location": "{location}" 39 | } 40 | }, 41 | { 42 | "arguments": [], 43 | "name": "a failing action is executed", 44 | "line": 5, 45 | "keyword": "When ", 46 | "result": { 47 | "duration": "{duration}", 48 | "status": "passed" 49 | }, 50 | "match": { 51 | "location": "{location}" 52 | } 53 | }, 54 | { 55 | "arguments": [], 56 | "name": "a post-condition passes", 57 | "line": 6, 58 | "keyword": "Then ", 59 | "result": { 60 | "duration": "{duration}", 61 | "status": "passed" 62 | }, 63 | "match": { 64 | "location": "{location}" 65 | } 66 | } 67 | ] 68 | } 69 | ], 70 | "profile": "default", 71 | "retry": 0 72 | } 73 | ] 74 | """ 75 | And stderr should be empty 76 | -------------------------------------------------------------------------------- /features/empty.feature: -------------------------------------------------------------------------------- 1 | Feature: Empty 2 | 3 | Scenario: Empty 4 | Given the 'empty' feature 5 | Given the '@does-not-exist' tag 6 | And a 'json' formatter 7 | When executing the parallel-cucumber-js bin 8 | Then the exit code should be '0' 9 | And stdout should contain JSON matching: 10 | """ 11 | [ 12 | ] 13 | """ 14 | And stderr should be empty 15 | 16 | -------------------------------------------------------------------------------- /features/environment_variables.feature: -------------------------------------------------------------------------------- 1 | Feature: Environment variables 2 | 3 | Scenario: Environment variables 4 | Given the 'environment_variables' feature 5 | And a profile called 'test_profile_1' 6 | And the 'test_profile_1' profile has the tag '@no-environment-variable' 7 | And a profile called 'test_profile_2' 8 | And the 'test_profile_2' profile has the tag '@environment-variable' 9 | And the 'test_profile_2' profile has the environment variable 'EXAMPLE_NAME' set to 'example_value' 10 | And a profile called 'test_profile_3' 11 | And the 'test_profile_3' profile has the tag '@no-environment-variable' 12 | And a 'json' formatter 13 | And '1' worker 14 | When executing the parallel-cucumber-js bin 15 | Then the exit code should be '0' 16 | And stdout should contain JSON matching: 17 | """ 18 | [ 19 | { 20 | "id": "environment-variables", 21 | "name": "Environment variables", 22 | "tags": [], 23 | "line": 1, 24 | "keyword": "Feature", 25 | "uri": "{uri}/features/environment_variables.feature", 26 | "elements": [ 27 | { 28 | "name": "Environment variable has not been set", 29 | "id": "environment-variables;environment-variable-has-not-been-set", 30 | "line": 4, 31 | "keyword": "Scenario", 32 | "tags": [], 33 | "type": "scenario", 34 | "tags": [ 35 | { 36 | "name": "@no-environment-variable", 37 | "line": 3 38 | } 39 | ], 40 | "steps": [ 41 | { 42 | "arguments": [], 43 | "name": "the environment variable 'EXAMPLE_NAME' is not set", 44 | "line": 5, 45 | "keyword": "Then ", 46 | "result": { 47 | "duration": "{duration}", 48 | "status": "passed" 49 | }, 50 | "match": { 51 | "location": "{location}" 52 | } 53 | } 54 | ] 55 | } 56 | ], 57 | "profile": "test_profile_1", 58 | "retry": 0 59 | }, 60 | { 61 | "id": "environment-variables", 62 | "name": "Environment variables", 63 | "tags": [], 64 | "line": 1, 65 | "keyword": "Feature", 66 | "uri": "{uri}/features/environment_variables.feature", 67 | "elements": [ 68 | { 69 | "name": "Environment variable has been set", 70 | "id": "environment-variables;environment-variable-has-been-set", 71 | "line": 8, 72 | "keyword": "Scenario", 73 | "tags": [], 74 | "type": "scenario", 75 | "tags": [ 76 | { 77 | "name": "@environment-variable", 78 | "line": 7 79 | } 80 | ], 81 | "steps": [ 82 | { 83 | "arguments": [], 84 | "name": "the environment variable 'EXAMPLE_NAME' equals 'example_value'", 85 | "line": 9, 86 | "keyword": "Then ", 87 | "result": { 88 | "duration": "{duration}", 89 | "status": "passed" 90 | }, 91 | "match": { 92 | "location": "{location}" 93 | } 94 | } 95 | ] 96 | } 97 | ], 98 | "profile": "test_profile_2", 99 | "retry": 0 100 | }, 101 | { 102 | "id": "environment-variables", 103 | "name": "Environment variables", 104 | "tags": [], 105 | "line": 1, 106 | "keyword": "Feature", 107 | "uri": "{uri}/features/environment_variables.feature", 108 | "elements": [ 109 | { 110 | "name": "Environment variable has not been set", 111 | "id": "environment-variables;environment-variable-has-not-been-set", 112 | "line": 4, 113 | "keyword": "Scenario", 114 | "tags": [], 115 | "type": "scenario", 116 | "tags": [ 117 | { 118 | "name": "@no-environment-variable", 119 | "line": 3 120 | } 121 | ], 122 | "steps": [ 123 | { 124 | "arguments": [], 125 | "name": "the environment variable 'EXAMPLE_NAME' is not set", 126 | "line": 5, 127 | "keyword": "Then ", 128 | "result": { 129 | "duration": "{duration}", 130 | "status": "passed" 131 | }, 132 | "match": { 133 | "location": "{location}" 134 | } 135 | } 136 | ] 137 | } 138 | ], 139 | "profile": "test_profile_3", 140 | "retry": 0 141 | } 142 | ] 143 | """ 144 | And stderr should be empty 145 | 146 | Scenario: Environment variables go back to original value 147 | Given the environment variable 'EXAMPLE_NAME' is set to 'old_example_value' 148 | And a profile called 'test_profile_1' 149 | And the 'test_profile_1' profile has the tag '@old-environment-variable' 150 | And a profile called 'test_profile_2' 151 | And the 'test_profile_2' profile has the tag '@environment-variable' 152 | And the 'test_profile_2' profile has the environment variable 'EXAMPLE_NAME' set to 'example_value' 153 | And a profile called 'test_profile_3' 154 | And the 'test_profile_3' profile has the tag '@old-environment-variable' 155 | And a 'json' formatter 156 | And '1' worker 157 | When executing the parallel-cucumber-js bin 158 | Then the exit code should be '0' 159 | And stdout should contain JSON matching: 160 | """ 161 | [ 162 | { 163 | "id": "environment-variables", 164 | "name": "Environment variables", 165 | "tags": [], 166 | "line": 1, 167 | "keyword": "Feature", 168 | "uri": "{uri}/features/environment_variables.feature", 169 | "elements": [ 170 | { 171 | "name": "Environment variable has old value", 172 | "id": "environment-variables;environment-variable-has-old-value", 173 | "line": 12, 174 | "keyword": "Scenario", 175 | "type": "scenario", 176 | "tags": [ 177 | { 178 | "name": "@old-environment-variable", 179 | "line": 11 180 | } 181 | ], 182 | "steps": [ 183 | { 184 | "arguments": [], 185 | "name": "the environment variable 'EXAMPLE_NAME' equals 'old_example_value'", 186 | "line": 13, 187 | "keyword": "Then ", 188 | "result": { 189 | "duration": "{duration}", 190 | "status": "passed" 191 | }, 192 | "match": { 193 | "location": "{location}" 194 | } 195 | } 196 | ] 197 | } 198 | ], 199 | "profile": "test_profile_1", 200 | "retry": 0 201 | }, 202 | { 203 | "id": "environment-variables", 204 | "name": "Environment variables", 205 | "tags": [], 206 | "line": 1, 207 | "keyword": "Feature", 208 | "uri": "{uri}/features/environment_variables.feature", 209 | "elements": [ 210 | { 211 | "name": "Environment variable has been set", 212 | "id": "environment-variables;environment-variable-has-been-set", 213 | "line": 8, 214 | "keyword": "Scenario", 215 | "type": "scenario", 216 | "tags": [ 217 | { 218 | "name": "@environment-variable", 219 | "line": 7 220 | } 221 | ], 222 | "steps": [ 223 | { 224 | "arguments": [], 225 | "name": "the environment variable 'EXAMPLE_NAME' equals 'example_value'", 226 | "line": 9, 227 | "keyword": "Then ", 228 | "result": { 229 | "duration": "{duration}", 230 | "status": "passed" 231 | }, 232 | "match": { 233 | "location": "{location}" 234 | } 235 | } 236 | ] 237 | } 238 | ], 239 | "profile": "test_profile_2", 240 | "retry": 0 241 | }, 242 | { 243 | "id": "environment-variables", 244 | "name": "Environment variables", 245 | "tags": [], 246 | "line": 1, 247 | "keyword": "Feature", 248 | "uri": "{uri}/features/environment_variables.feature", 249 | "elements": [ 250 | { 251 | "name": "Environment variable has old value", 252 | "id": "environment-variables;environment-variable-has-old-value", 253 | "line": 12, 254 | "keyword": "Scenario", 255 | "type": "scenario", 256 | "tags": [ 257 | { 258 | "name": "@old-environment-variable", 259 | "line": 11 260 | } 261 | ], 262 | "steps": [ 263 | { 264 | "arguments": [], 265 | "name": "the environment variable 'EXAMPLE_NAME' equals 'old_example_value'", 266 | "line": 13, 267 | "keyword": "Then ", 268 | "result": { 269 | "duration": "{duration}", 270 | "status": "passed" 271 | }, 272 | "match": { 273 | "location": "{location}" 274 | } 275 | } 276 | ] 277 | } 278 | ], 279 | "profile": "test_profile_3", 280 | "retry": 0 281 | } 282 | ] 283 | """ 284 | And stderr should be empty 285 | -------------------------------------------------------------------------------- /features/failing.feature: -------------------------------------------------------------------------------- 1 | Feature: Failing 2 | 3 | Scenario: Failing 4 | Given the 'failing' feature 5 | And a 'json' formatter 6 | When executing the parallel-cucumber-js bin 7 | Then the exit code should be '1' 8 | And stdout should contain JSON matching: 9 | """ 10 | [ 11 | { 12 | "id": "failing", 13 | "name": "Failing", 14 | "tags": [], 15 | "line": 1, 16 | "keyword": "Feature", 17 | "uri": "{uri}/features/failing.feature", 18 | "elements": [ 19 | { 20 | "name": "Failing", 21 | "id": "failing;failing", 22 | "line": 3, 23 | "keyword": "Scenario", 24 | "tags": [], 25 | "type": "scenario", 26 | "steps": [ 27 | { 28 | "arguments": [], 29 | "name": "a passing pre-condition", 30 | "line": 4, 31 | "keyword": "Given ", 32 | "result": { 33 | "duration": "{duration}", 34 | "status": "passed" 35 | }, 36 | "match": { 37 | "location": "{location}" 38 | } 39 | }, 40 | { 41 | "arguments": [], 42 | "name": "a failing action is executed", 43 | "line": 5, 44 | "keyword": "When ", 45 | "result": { 46 | "error_message": "Failed", 47 | "duration": "{duration}", 48 | "status": "failed" 49 | }, 50 | "match": { 51 | "location": "{location}" 52 | } 53 | }, 54 | { 55 | "arguments": [], 56 | "name": "a post-condition passes", 57 | "line": 6, 58 | "keyword": "Then ", 59 | "result": { 60 | "status": "skipped" 61 | }, 62 | "match": { 63 | "location": "{location}" 64 | } 65 | } 66 | ] 67 | } 68 | ], 69 | "profile": "default", 70 | "retry": 0 71 | } 72 | ] 73 | """ 74 | And stderr should be empty 75 | 76 | -------------------------------------------------------------------------------- /features/json_formatter.feature: -------------------------------------------------------------------------------- 1 | Feature: JSON formatter 2 | 3 | Scenario: JSON formatter 4 | Given the 'passing' feature 5 | And a 'json' formatter 6 | When executing the parallel-cucumber-js bin 7 | Then the exit code should be '0' 8 | And stdout should contain JSON matching: 9 | """ 10 | [ 11 | { 12 | "id": "passing", 13 | "name": "Passing", 14 | "tags": [], 15 | "line": 1, 16 | "keyword": "Feature", 17 | "uri": "{uri}/features/passing.feature", 18 | "elements": [ 19 | { 20 | "name": "Passing", 21 | "id": "passing;passing", 22 | "line": 3, 23 | "keyword": "Scenario", 24 | "tags": [], 25 | "type": "scenario", 26 | "steps": [ 27 | { 28 | "arguments": [], 29 | "name": "a passing pre-condition", 30 | "line": 4, 31 | "keyword": "Given ", 32 | "result": { 33 | "duration": "{duration}", 34 | "status": "passed" 35 | }, 36 | "match": { 37 | "location": "{location}" 38 | } 39 | }, 40 | { 41 | "arguments": [], 42 | "name": "a passing action is executed", 43 | "line": 5, 44 | "keyword": "When ", 45 | "result": { 46 | "duration": "{duration}", 47 | "status": "passed" 48 | }, 49 | "match": { 50 | "location": "{location}" 51 | } 52 | }, 53 | { 54 | "arguments": [], 55 | "name": "a post-condition passes", 56 | "line": 6, 57 | "keyword": "Then ", 58 | "result": { 59 | "duration": "{duration}", 60 | "status": "passed" 61 | }, 62 | "match": { 63 | "location": "{location}" 64 | } 65 | } 66 | ] 67 | } 68 | ], 69 | "profile": "default", 70 | "retry": 0 71 | } 72 | ] 73 | """ 74 | And stderr should be empty 75 | 76 | -------------------------------------------------------------------------------- /features/parallel.feature: -------------------------------------------------------------------------------- 1 | Feature: Parallel 2 | 3 | Scenario: Parallel 4 | Given the 'parallel' features 5 | And a profile called 'blue' 6 | And the 'blue' profile has the tag '@blue' 7 | And a profile called 'red' 8 | And the 'red' profile has the tag '@red' 9 | And a 'json' formatter 10 | When executing the parallel-cucumber-js bin 11 | Then the exit code should be '0' 12 | And stdout should contain JSON matching: 13 | """ 14 | [ 15 | { 16 | "id": "blue", 17 | "name": "Blue", 18 | "tags": [], 19 | "line": 1, 20 | "keyword": "Feature", 21 | "uri": "{uri}/features/parallel/blue.feature", 22 | "elements": [ 23 | { 24 | "name": "Blue", 25 | "id": "blue;blue", 26 | "line": 4, 27 | "keyword": "Scenario", 28 | "type": "scenario", 29 | "tags": [ 30 | { 31 | "name": "@blue", 32 | "line": 3 33 | } 34 | ], 35 | "steps": [ 36 | { 37 | "arguments": [], 38 | "name": "a passing action is executed", 39 | "line": 5, 40 | "keyword": "When ", 41 | "result": { 42 | "duration": "{duration}", 43 | "status": "passed" 44 | }, 45 | "match": { 46 | "location": "{location}" 47 | } 48 | } 49 | ] 50 | } 51 | ], 52 | "profile": "blue", 53 | "retry": 0 54 | }, 55 | { 56 | "id": "purple", 57 | "name": "Purple", 58 | "tags": [], 59 | "line": 1, 60 | "keyword": "Feature", 61 | "uri": "{uri}/features/parallel/purple.feature", 62 | "elements": [ 63 | { 64 | "name": "Purple", 65 | "id": "purple;purple", 66 | "line": 4, 67 | "keyword": "Scenario", 68 | "type": "scenario", 69 | "tags": [ 70 | { 71 | "name": "@blue", 72 | "line": 3 73 | }, 74 | { 75 | "name": "@red", 76 | "line": 3 77 | } 78 | ], 79 | "steps": [ 80 | { 81 | "arguments": [], 82 | "name": "a passing action is executed", 83 | "line": 5, 84 | "keyword": "When ", 85 | "result": { 86 | "duration": "{duration}", 87 | "status": "passed" 88 | }, 89 | "match": { 90 | "location": "{location}" 91 | } 92 | } 93 | ] 94 | } 95 | ], 96 | "profile": "red", 97 | "retry": 0 98 | }, 99 | { 100 | "id": "purple", 101 | "name": "Purple", 102 | "tags": [], 103 | "line": 1, 104 | "keyword": "Feature", 105 | "uri": "{uri}/features/parallel/purple.feature", 106 | "elements": [ 107 | { 108 | "name": "Purple", 109 | "id": "purple;purple", 110 | "line": 4, 111 | "keyword": "Scenario", 112 | "type": "scenario", 113 | "tags": [ 114 | { 115 | "name": "@blue", 116 | "line": 3 117 | }, 118 | { 119 | "name": "@red", 120 | "line": 3 121 | } 122 | ], 123 | "steps": [ 124 | { 125 | "arguments": [], 126 | "name": "a passing action is executed", 127 | "line": 5, 128 | "keyword": "When ", 129 | "result": { 130 | "duration": "{duration}", 131 | "status": "passed" 132 | }, 133 | "match": { 134 | "location": "{location}" 135 | } 136 | } 137 | ] 138 | } 139 | ], 140 | "profile": "blue", 141 | "retry": 0 142 | }, 143 | { 144 | "id": "red", 145 | "name": "Red", 146 | "tags": [], 147 | "line": 1, 148 | "keyword": "Feature", 149 | "uri": "{uri}/features/parallel/red.feature", 150 | "elements": [ 151 | { 152 | "name": "Red", 153 | "id": "red;red", 154 | "line": 4, 155 | "keyword": "Scenario", 156 | "type": "scenario", 157 | "tags": [ 158 | { 159 | "name": "@red", 160 | "line": 3 161 | } 162 | ], 163 | "steps": [ 164 | { 165 | "arguments": [], 166 | "name": "a passing action is executed", 167 | "line": 5, 168 | "keyword": "When ", 169 | "result": { 170 | "duration": "{duration}", 171 | "status": "passed" 172 | }, 173 | "match": { 174 | "location": "{location}" 175 | } 176 | } 177 | ] 178 | } 179 | ], 180 | "profile": "red", 181 | "retry": 0 182 | } 183 | ] 184 | """ 185 | And stderr should be empty 186 | 187 | Scenario: Parallel, combining tags with ANDs and ORs 188 | Given the 'parallel' features 189 | And a profile called 'purple' 190 | And the 'purple' profile has the tag '@blue' 191 | And the 'purple' profile has the tag '@red' 192 | And a profile called 'red' 193 | And the 'red' profile has the tag '@red' 194 | And the 'red' profile has the tag '~@blue' 195 | And a 'json' formatter 196 | When executing the parallel-cucumber-js bin 197 | Then the exit code should be '0' 198 | And stdout should contain JSON matching: 199 | """ 200 | [ 201 | { 202 | "id": "purple", 203 | "name": "Purple", 204 | "tags": [], 205 | "line": 1, 206 | "keyword": "Feature", 207 | "uri": "{uri}/features/parallel/purple.feature", 208 | "elements": [ 209 | { 210 | "name": "Purple", 211 | "id": "purple;purple", 212 | "line": 4, 213 | "keyword": "Scenario", 214 | "type": "scenario", 215 | "tags": [ 216 | { 217 | "name": "@blue", 218 | "line": 3 219 | }, 220 | { 221 | "name": "@red", 222 | "line": 3 223 | } 224 | ], 225 | "steps": [ 226 | { 227 | "arguments": [], 228 | "name": "a passing action is executed", 229 | "line": 5, 230 | "keyword": "When ", 231 | "result": { 232 | "duration": "{duration}", 233 | "status": "passed" 234 | }, 235 | "match": { 236 | "location": "{location}" 237 | } 238 | } 239 | ] 240 | } 241 | ], 242 | "profile": "purple", 243 | "retry": 0 244 | }, 245 | { 246 | "id": "red", 247 | "name": "Red", 248 | "tags": [], 249 | "line": 1, 250 | "keyword": "Feature", 251 | "uri": "{uri}/features/parallel/red.feature", 252 | "elements": [ 253 | { 254 | "name": "Red", 255 | "id": "red;red", 256 | "line": 4, 257 | "keyword": "Scenario", 258 | "type": "scenario", 259 | "tags": [ 260 | { 261 | "name": "@red", 262 | "line": 3 263 | } 264 | ], 265 | "steps": [ 266 | { 267 | "arguments": [], 268 | "name": "a passing action is executed", 269 | "line": 5, 270 | "keyword": "When ", 271 | "result": { 272 | "duration": "{duration}", 273 | "status": "passed" 274 | }, 275 | "match": { 276 | "location": "{location}" 277 | } 278 | } 279 | ] 280 | } 281 | ], 282 | "profile": "red", 283 | "retry": 0 284 | } 285 | ] 286 | """ 287 | And stderr should be empty 288 | -------------------------------------------------------------------------------- /features/passing.feature: -------------------------------------------------------------------------------- 1 | Feature: Passing 2 | 3 | Scenario: Passing 4 | Given the 'passing' feature 5 | And a 'json' formatter 6 | When executing the parallel-cucumber-js bin 7 | Then the exit code should be '0' 8 | And stdout should contain JSON matching: 9 | """ 10 | [ 11 | { 12 | "id": "passing", 13 | "name": "Passing", 14 | "tags": [], 15 | "line": 1, 16 | "keyword": "Feature", 17 | "uri": "{uri}/features/passing.feature", 18 | "elements": [ 19 | { 20 | "name": "Passing", 21 | "id": "passing;passing", 22 | "line": 3, 23 | "keyword": "Scenario", 24 | "tags": [], 25 | "type": "scenario", 26 | "steps": [ 27 | { 28 | "arguments": [], 29 | "name": "a passing pre-condition", 30 | "line": 4, 31 | "keyword": "Given ", 32 | "result": { 33 | "duration": "{duration}", 34 | "status": "passed" 35 | }, 36 | "match": { 37 | "location": "{location}" 38 | } 39 | }, 40 | { 41 | "arguments": [], 42 | "name": "a passing action is executed", 43 | "line": 5, 44 | "keyword": "When ", 45 | "result": { 46 | "duration": "{duration}", 47 | "status": "passed" 48 | }, 49 | "match": { 50 | "location": "{location}" 51 | } 52 | }, 53 | { 54 | "arguments": [], 55 | "name": "a post-condition passes", 56 | "line": 6, 57 | "keyword": "Then ", 58 | "result": { 59 | "duration": "{duration}", 60 | "status": "passed" 61 | }, 62 | "match": { 63 | "location": "{location}" 64 | } 65 | } 66 | ] 67 | } 68 | ], 69 | "profile": "default", 70 | "retry": 0 71 | } 72 | ] 73 | """ 74 | And stderr should be empty 75 | 76 | -------------------------------------------------------------------------------- /features/profile_environment_variable.feature: -------------------------------------------------------------------------------- 1 | Feature: Profile environment variable 2 | 3 | Scenario: Profile environment variable 4 | Given the 'profile_environment_variable' feature 5 | And a profile called 'test_profile' 6 | And a 'json' formatter 7 | When executing the parallel-cucumber-js bin 8 | Then the exit code should be '0' 9 | And stdout should contain JSON matching: 10 | """ 11 | [ 12 | { 13 | "id": "profile-environment-variable", 14 | "name": "Profile environment variable", 15 | "tags": [], 16 | "line": 1, 17 | "keyword": "Feature", 18 | "uri": "{uri}/features/profile_environment_variable.feature", 19 | "elements": [ 20 | { 21 | "name": "Profile environment variable", 22 | "id": "profile-environment-variable;profile-environment-variable", 23 | "line": 3, 24 | "keyword": "Scenario", 25 | "tags": [], 26 | "type": "scenario", 27 | "steps": [ 28 | { 29 | "arguments": [], 30 | "name": "the environment variable 'PARALLEL_CUCUMBER_PROFILE' equals 'test_profile'", 31 | "line": 4, 32 | "keyword": "Then ", 33 | "result": { 34 | "duration": "{duration}", 35 | "status": "passed" 36 | }, 37 | "match": { 38 | "location": "{location}" 39 | } 40 | } 41 | ] 42 | } 43 | ], 44 | "profile": "test_profile", 45 | "retry": 0 46 | } 47 | ] 48 | """ 49 | And stderr should be empty 50 | -------------------------------------------------------------------------------- /features/progress_formatter.feature: -------------------------------------------------------------------------------- 1 | Feature: Progress formatter 2 | 3 | Scenario: Progress formatter 4 | Given the 'passing' feature 5 | And a 'progress' formatter 6 | When executing the parallel-cucumber-js bin 7 | Then the exit code should be '0' 8 | And stdout should contain new line separated YAML matching: 9 | """ 10 | {scenario: {worker: "{zeroOrGreaterNumber}", status: passed, profile: default, uri: features/passing_feature/Passing, duration: "{duration}"}} 11 | {feature: {worker: "{zeroOrGreaterNumber}", status: finished, profile: default, uri: features/passing_feature, duration: "{duration}"}} 12 | {summary: {status: finished, duration: "{duration}", elapsed: "{duration}", saved: "{duration}", savings: "{percentage}"}} 13 | """ 14 | And stderr should be empty 15 | 16 | Scenario: Progress formatter with undefined step 17 | Given the 'undefined' feature 18 | And a 'progress' formatter 19 | When executing the parallel-cucumber-js bin 20 | Then the exit code should be '0' 21 | And stdout should contain new line separated YAML matching: 22 | """ 23 | {scenario: {worker: "{zeroOrGreaterNumber}", status: undefined, profile: default, uri: features/undefined_feature/Undefined, duration: "{duration}"}} 24 | {feature: {worker: "{zeroOrGreaterNumber}", status: finished, profile: default, uri: features/undefined_feature, duration: "{duration}"}} 25 | {summary: {status: finished, duration: "{duration}", elapsed: "{duration}", saved: "{duration}", savings: "{percentage}"}} 26 | """ 27 | And stderr should be empty 28 | -------------------------------------------------------------------------------- /features/require.feature: -------------------------------------------------------------------------------- 1 | Feature: Require 2 | 3 | Scenario: Require one path 4 | Given the 'passing' feature 5 | And a 'json' formatter 6 | And './features/' is required 7 | When executing the parallel-cucumber-js bin 8 | Then the exit code should be '0' 9 | And stdout should contain JSON matching: 10 | """ 11 | [ 12 | { 13 | "id": "passing", 14 | "name": "Passing", 15 | "tags": [], 16 | "line": 1, 17 | "keyword": "Feature", 18 | "uri": "{uri}/features/passing.feature", 19 | "elements": [ 20 | { 21 | "name": "Passing", 22 | "id": "passing;passing", 23 | "line": 3, 24 | "keyword": "Scenario", 25 | "tags": [], 26 | "type": "scenario", 27 | "steps": [ 28 | { 29 | "arguments": [], 30 | "name": "a passing pre-condition", 31 | "line": 4, 32 | "keyword": "Given ", 33 | "result": { 34 | "duration": "{duration}", 35 | "status": "passed" 36 | }, 37 | "match": { 38 | "location": "{location}" 39 | } 40 | }, 41 | { 42 | "arguments": [], 43 | "name": "a passing action is executed", 44 | "line": 5, 45 | "keyword": "When ", 46 | "result": { 47 | "duration": "{duration}", 48 | "status": "passed" 49 | }, 50 | "match": { 51 | "location": "{location}" 52 | } 53 | }, 54 | { 55 | "arguments": [], 56 | "name": "a post-condition passes", 57 | "line": 6, 58 | "keyword": "Then ", 59 | "result": { 60 | "duration": "{duration}", 61 | "status": "passed" 62 | }, 63 | "match": { 64 | "location": "{location}" 65 | } 66 | } 67 | ] 68 | } 69 | ], 70 | "profile": "default", 71 | "retry": 0 72 | } 73 | ] 74 | """ 75 | And stderr should be empty 76 | 77 | Scenario: Require two paths 78 | Given the 'passing' feature 79 | And a 'json' formatter 80 | And './features/support/' is required 81 | And './features/step_definitions/' is required 82 | When executing the parallel-cucumber-js bin 83 | Then the exit code should be '0' 84 | And stdout should contain JSON matching: 85 | """ 86 | [ 87 | { 88 | "id": "passing", 89 | "name": "Passing", 90 | "tags": [], 91 | "line": 1, 92 | "keyword": "Feature", 93 | "uri": "{uri}/features/passing.feature", 94 | "elements": [ 95 | { 96 | "name": "Passing", 97 | "id": "passing;passing", 98 | "line": 3, 99 | "keyword": "Scenario", 100 | "tags": [], 101 | "type": "scenario", 102 | "steps": [ 103 | { 104 | "arguments": [], 105 | "name": "a passing pre-condition", 106 | "line": 4, 107 | "keyword": "Given ", 108 | "result": { 109 | "duration": "{duration}", 110 | "status": "passed" 111 | }, 112 | "match": { 113 | "location": "{location}" 114 | } 115 | }, 116 | { 117 | "arguments": [], 118 | "name": "a passing action is executed", 119 | "line": 5, 120 | "keyword": "When ", 121 | "result": { 122 | "duration": "{duration}", 123 | "status": "passed" 124 | }, 125 | "match": { 126 | "location": "{location}" 127 | } 128 | }, 129 | { 130 | "arguments": [], 131 | "name": "a post-condition passes", 132 | "line": 6, 133 | "keyword": "Then ", 134 | "result": { 135 | "duration": "{duration}", 136 | "status": "passed" 137 | }, 138 | "match": { 139 | "location": "{location}" 140 | } 141 | } 142 | ] 143 | } 144 | ], 145 | "profile": "default", 146 | "retry": 0 147 | } 148 | ] 149 | """ 150 | And stderr should be empty 151 | -------------------------------------------------------------------------------- /features/retries.feature: -------------------------------------------------------------------------------- 1 | Feature: Retries 2 | 3 | Scenario: Failing with no max retries 4 | Given the 'retries' feature 5 | And a 'json' formatter 6 | When executing the parallel-cucumber-js bin 7 | Then the exit code should be '1' 8 | And stdout should contain JSON matching: 9 | """ 10 | [ 11 | { 12 | "id": "retries", 13 | "name": "Retries", 14 | "tags": [], 15 | "line": 1, 16 | "keyword": "Feature", 17 | "uri": "{uri}/features/retries.feature", 18 | "elements": [ 19 | { 20 | "name": "Retries", 21 | "id": "retries;retries", 22 | "line": 3, 23 | "keyword": "Scenario", 24 | "tags": [], 25 | "type": "scenario", 26 | "steps": [ 27 | { 28 | "arguments": [], 29 | "name": "an action is executed that passes on retry '2'", 30 | "line": 4, 31 | "keyword": "When ", 32 | "result": { 33 | "error_message": "Failed on retry 0", 34 | "duration": "{duration}", 35 | "status": "failed" 36 | }, 37 | "match": { 38 | "location": "{location}" 39 | } 40 | } 41 | ] 42 | } 43 | ], 44 | "profile": "default", 45 | "retry": 0 46 | } 47 | ] 48 | """ 49 | And stderr should be empty 50 | 51 | Scenario: Failing with 0 max retries 52 | Given the 'retries' feature 53 | And '0' max retries 54 | And a 'json' formatter 55 | When executing the parallel-cucumber-js bin 56 | Then the exit code should be '1' 57 | And stdout should contain JSON matching: 58 | """ 59 | [ 60 | { 61 | "id": "retries", 62 | "name": "Retries", 63 | "tags": [], 64 | "line": 1, 65 | "keyword": "Feature", 66 | "uri": "{uri}/features/retries.feature", 67 | "elements": [ 68 | { 69 | "name": "Retries", 70 | "id": "retries;retries", 71 | "line": 3, 72 | "keyword": "Scenario", 73 | "tags": [], 74 | "type": "scenario", 75 | "steps": [ 76 | { 77 | "arguments": [], 78 | "name": "an action is executed that passes on retry '2'", 79 | "line": 4, 80 | "keyword": "When ", 81 | "result": { 82 | "error_message": "Failed on retry 0", 83 | "duration": "{duration}", 84 | "status": "failed" 85 | }, 86 | "match": { 87 | "location": "{location}" 88 | } 89 | } 90 | ] 91 | } 92 | ], 93 | "profile": "default", 94 | "retry": 0 95 | } 96 | ] 97 | """ 98 | And stderr should be empty 99 | 100 | Scenario: Failing with 1 max retries 101 | Given the 'retries' feature 102 | And '1' max retries 103 | And a 'json' formatter 104 | When executing the parallel-cucumber-js bin 105 | Then the exit code should be '1' 106 | And stdout should contain JSON matching: 107 | """ 108 | [ 109 | { 110 | "id": "retries", 111 | "name": "Retries", 112 | "tags": [], 113 | "line": 1, 114 | "keyword": "Feature", 115 | "uri": "{uri}/features/retries.feature", 116 | "elements": [ 117 | { 118 | "name": "Retries", 119 | "id": "retries;retries", 120 | "line": 3, 121 | "keyword": "Scenario", 122 | "tags": [], 123 | "type": "scenario", 124 | "steps": [ 125 | { 126 | "arguments": [], 127 | "name": "an action is executed that passes on retry '2'", 128 | "line": 4, 129 | "keyword": "When ", 130 | "result": { 131 | "error_message": "Failed on retry 0", 132 | "duration": "{duration}", 133 | "status": "failed" 134 | }, 135 | "match": { 136 | "location": "{location}" 137 | } 138 | } 139 | ] 140 | } 141 | ], 142 | "profile": "default", 143 | "retry": 0 144 | }, 145 | { 146 | "id": "retries", 147 | "name": "Retries - retry 1", 148 | "tags": [], 149 | "line": 1, 150 | "keyword": "Feature", 151 | "uri": "{uri}/features/retries.feature", 152 | "elements": [ 153 | { 154 | "name": "Retries", 155 | "id": "retries;retries", 156 | "line": 3, 157 | "keyword": "Scenario", 158 | "tags": [], 159 | "type": "scenario", 160 | "steps": [ 161 | { 162 | "arguments": [], 163 | "name": "an action is executed that passes on retry '2'", 164 | "line": 4, 165 | "keyword": "When ", 166 | "result": { 167 | "error_message": "Failed on retry 1", 168 | "duration": "{duration}", 169 | "status": "failed" 170 | }, 171 | "match": { 172 | "location": "{location}" 173 | } 174 | } 175 | ] 176 | } 177 | ], 178 | "profile": "default", 179 | "retry": 1 180 | } 181 | ] 182 | """ 183 | And stderr should be empty 184 | 185 | Scenario: Passing with 2 max retries 186 | Given the 'retries' feature 187 | And '2' max retries 188 | And a 'json' formatter 189 | When executing the parallel-cucumber-js bin 190 | Then the exit code should be '0' 191 | And stdout should contain JSON matching: 192 | """ 193 | [ 194 | { 195 | "id": "retries", 196 | "name": "Retries", 197 | "tags": [], 198 | "line": 1, 199 | "keyword": "Feature", 200 | "uri": "{uri}/features/retries.feature", 201 | "elements": [ 202 | { 203 | "name": "Retries", 204 | "id": "retries;retries", 205 | "line": 3, 206 | "keyword": "Scenario", 207 | "tags": [], 208 | "type": "scenario", 209 | "steps": [ 210 | { 211 | "arguments": [], 212 | "name": "an action is executed that passes on retry '2'", 213 | "line": 4, 214 | "keyword": "When ", 215 | "result": { 216 | "error_message": "Failed on retry 0", 217 | "duration": "{duration}", 218 | "status": "failed" 219 | }, 220 | "match": { 221 | "location": "{location}" 222 | } 223 | } 224 | ] 225 | } 226 | ], 227 | "profile": "default", 228 | "retry": 0 229 | }, 230 | { 231 | "id": "retries", 232 | "name": "Retries - retry 1", 233 | "tags": [], 234 | "line": 1, 235 | "keyword": "Feature", 236 | "uri": "{uri}/features/retries.feature", 237 | "elements": [ 238 | { 239 | "name": "Retries", 240 | "id": "retries;retries", 241 | "line": 3, 242 | "keyword": "Scenario", 243 | "tags": [], 244 | "type": "scenario", 245 | "steps": [ 246 | { 247 | "arguments": [], 248 | "name": "an action is executed that passes on retry '2'", 249 | "line": 4, 250 | "keyword": "When ", 251 | "result": { 252 | "error_message": "Failed on retry 1", 253 | "duration": "{duration}", 254 | "status": "failed" 255 | }, 256 | "match": { 257 | "location": "{location}" 258 | } 259 | } 260 | ] 261 | } 262 | ], 263 | "profile": "default", 264 | "retry": 1 265 | }, 266 | { 267 | "id": "retries", 268 | "name": "Retries - retry 2", 269 | "tags": [], 270 | "line": 1, 271 | "keyword": "Feature", 272 | "uri": "{uri}/features/retries.feature", 273 | "elements": [ 274 | { 275 | "name": "Retries", 276 | "id": "retries;retries", 277 | "line": 3, 278 | "keyword": "Scenario", 279 | "tags": [], 280 | "type": "scenario", 281 | "steps": [ 282 | { 283 | "arguments": [], 284 | "name": "an action is executed that passes on retry '2'", 285 | "line": 4, 286 | "keyword": "When ", 287 | "result": { 288 | "duration": "{duration}", 289 | "status": "passed" 290 | }, 291 | "match": { 292 | "location": "{location}" 293 | } 294 | } 295 | ] 296 | } 297 | ], 298 | "profile": "default", 299 | "retry": 2 300 | } 301 | ] 302 | """ 303 | And stderr should be empty 304 | 305 | Scenario: A feature with an undefined step is not retried 306 | Given the 'undefined' feature 307 | And a 'json' formatter 308 | When executing the parallel-cucumber-js bin 309 | Then the exit code should be '0' 310 | And stdout should contain JSON matching: 311 | """ 312 | [ 313 | { 314 | "id": "undefined", 315 | "name": "Undefined", 316 | "tags": [], 317 | "line": 1, 318 | "keyword": "Feature", 319 | "uri": "{uri}/features/undefined.feature", 320 | "elements": [ 321 | { 322 | "name": "Undefined", 323 | "id": "undefined;undefined", 324 | "line": 3, 325 | "keyword": "Scenario", 326 | "tags": [], 327 | "type": "scenario", 328 | "steps": [ 329 | { 330 | "arguments": [], 331 | "name": "an undefined action is executed", 332 | "line": 4, 333 | "keyword": "When ", 334 | "result": { 335 | "status": "undefined" 336 | } 337 | } 338 | ] 339 | } 340 | ], 341 | "profile": "default", 342 | "retry": 0 343 | } 344 | ] 345 | """ 346 | And stderr should be empty 347 | 348 | Scenario: A feature with a pending step is not retried 349 | Given the 'pending' feature 350 | And a 'json' formatter 351 | When executing the parallel-cucumber-js bin 352 | Then the exit code should be '0' 353 | And stdout should contain JSON matching: 354 | """ 355 | [ 356 | { 357 | "id": "pending", 358 | "name": "Pending", 359 | "tags": [], 360 | "line": 1, 361 | "keyword": "Feature", 362 | "uri": "{uri}/features/pending.feature", 363 | "elements": [ 364 | { 365 | "name": "Pending", 366 | "id": "pending;pending", 367 | "line": 3, 368 | "keyword": "Scenario", 369 | "tags": [], 370 | "type": "scenario", 371 | "steps": [ 372 | { 373 | "arguments": [], 374 | "name": "a pending action is executed", 375 | "line": 4, 376 | "keyword": "When ", 377 | "result": { 378 | "status": "pending" 379 | }, 380 | "match": { 381 | "location": "{location}" 382 | } 383 | } 384 | ] 385 | } 386 | ], 387 | "profile": "default", 388 | "retry": 0 389 | } 390 | ] 391 | """ 392 | And stderr should be empty 393 | -------------------------------------------------------------------------------- /features/scenario_outline.feature: -------------------------------------------------------------------------------- 1 | Feature: Scenario outline 2 | 3 | Scenario: Scenario outline 4 | Given the 'scenario_outline' feature 5 | And '1' max retries 6 | And a 'json' formatter 7 | When executing the parallel-cucumber-js bin 8 | Then the exit code should be '0' 9 | And stdout should contain JSON matching: 10 | """ 11 | [ 12 | { 13 | "id": "scenario-outline", 14 | "name": "Scenario outline", 15 | "tags": [], 16 | "line": 1, 17 | "keyword": "Feature", 18 | "uri": "{uri}/features/scenario_outline.feature", 19 | "elements": [ 20 | { 21 | "name": "Scenario outline", 22 | "id": "scenario-outline;scenario-outline", 23 | "line": 8, 24 | "keyword": "Scenario", 25 | "tags": [], 26 | "type": "scenario", 27 | "steps": [ 28 | { 29 | "arguments": [], 30 | "name": "an action is executed that passes on retry '0'", 31 | "line": 4, 32 | "keyword": "When ", 33 | "result": { 34 | "status": "passed", 35 | "duration": "{duration}" 36 | }, 37 | "match": { 38 | "location": "{location}" 39 | } 40 | } 41 | ] 42 | }, 43 | { 44 | "name": "Scenario outline", 45 | "id": "scenario-outline;scenario-outline", 46 | "line": 9, 47 | "keyword": "Scenario", 48 | "tags": [], 49 | "type": "scenario", 50 | "steps": [ 51 | { 52 | "arguments": [], 53 | "name": "an action is executed that passes on retry '1'", 54 | "line": 4, 55 | "keyword": "When ", 56 | "result": { 57 | "status": "failed", 58 | "duration": "{duration}", 59 | "error_message": "Failed on retry 0" 60 | }, 61 | "match": { 62 | "location": "{location}" 63 | } 64 | } 65 | ] 66 | } 67 | ], 68 | "profile": "default", 69 | "retry": 0 70 | }, 71 | { 72 | "id": "scenario-outline", 73 | "name": "Scenario outline - retry 1", 74 | "tags": [], 75 | "line": 1, 76 | "keyword": "Feature", 77 | "uri": "{uri}/features/scenario_outline.feature", 78 | "elements": [ 79 | { 80 | "name": "Scenario outline", 81 | "id": "scenario-outline;scenario-outline", 82 | "line": 9, 83 | "keyword": "Scenario", 84 | "tags": [], 85 | "type": "scenario", 86 | "steps": [ 87 | { 88 | "arguments": [], 89 | "name": "an action is executed that passes on retry '1'", 90 | "line": 4, 91 | "keyword": "When ", 92 | "result": { 93 | "status": "passed", 94 | "duration": "{duration}" 95 | }, 96 | "match": { 97 | "location": "{location}" 98 | } 99 | } 100 | ] 101 | } 102 | ], 103 | "profile": "default", 104 | "retry": 1 105 | } 106 | ] 107 | """ 108 | And stderr should be empty 109 | -------------------------------------------------------------------------------- /features/step_definitions/bin.js: -------------------------------------------------------------------------------- 1 | var ChildProcess = require('child_process'); 2 | var DeepDiff = require('deep-diff'); 3 | var Diff = require('diff'); 4 | var StripColorCodes = require('stripcolorcodes'); 5 | var JSYAML = require('js-yaml'); 6 | 7 | module.exports = function() { 8 | this.Given(/^the '(.*)' feature$/, function(feature, callback) { 9 | if (this.isDryRun()) { return callback(); } 10 | 11 | var world = this; 12 | 13 | if (!world.features) { 14 | world.features = []; 15 | } 16 | 17 | world.features.push('features/' + feature + '.feature'); 18 | 19 | callback(); 20 | }); 21 | 22 | this.Given(/^the '(.*)' features$/, function(features, callback) { 23 | if (this.isDryRun()) { return callback(); } 24 | 25 | var world = this; 26 | 27 | if (!world.features) { 28 | world.features = []; 29 | } 30 | 31 | world.features.push('features/' + features + '/'); 32 | 33 | callback(); 34 | }); 35 | 36 | this.Given(/^the '(.*)' tags?$/, function(tags, callback) { 37 | if (this.isDryRun()) { return callback(); } 38 | 39 | var world = this; 40 | 41 | world.tags = tags; 42 | 43 | callback(); 44 | }); 45 | 46 | this.Given(/^a profile called '(.*)'$/, function(name, callback) { 47 | if (this.isDryRun()) { return callback(); } 48 | 49 | var world = this; 50 | 51 | if (!world.profiles) { 52 | world.profiles = {}; 53 | } 54 | 55 | world.profiles[name] = {}; 56 | 57 | callback(); 58 | }); 59 | 60 | this.Given(/^the '(.*)' profile has the tags? '(.*)'$/, function(name, tags, callback) { 61 | if (this.isDryRun()) { return callback(); } 62 | 63 | var world = this; 64 | 65 | if (!world.profiles[name].tags) { 66 | world.profiles[name].tags = []; 67 | } 68 | 69 | world.profiles[name].tags.push(tags); 70 | 71 | callback(); 72 | }); 73 | 74 | this.Given(/^the '(.*)' profile has the environment variable '(.*)' set to '(.*)'$/, function(name, envName, envValue, callback) { 75 | if (this.isDryRun()) { return callback(); } 76 | 77 | var world = this; 78 | 79 | if (!world.profiles[name].env) { 80 | world.profiles[name].env = {}; 81 | } 82 | 83 | world.profiles[name].env[envName] = envValue; 84 | 85 | callback(); 86 | }); 87 | 88 | this.Given(/^a '(.*)' formatter$/, function(name, callback) { 89 | if (this.isDryRun()) { return callback(); } 90 | 91 | var world = this; 92 | 93 | if (!world.formatters) { 94 | world.formatters = {}; 95 | } 96 | 97 | world.formatters[name] = { 98 | type: name 99 | }; 100 | 101 | callback(); 102 | }); 103 | 104 | this.Given(/^'(.*)' workers?$/, function(count, callback) { 105 | if (this.isDryRun()) { return callback(); } 106 | 107 | var world = this; 108 | 109 | world.workerCount = count; 110 | 111 | callback(); 112 | }); 113 | 114 | this.Given(/^the '(.*)' custom version of Cucumber$/, function(path, callback) { 115 | if (this.isDryRun()) { return callback(); } 116 | 117 | var world = this; 118 | 119 | world.customerCucumberPath = path; 120 | 121 | callback(); 122 | }); 123 | 124 | this.Given(/^'(.*)' is required$/, function(path, callback) { 125 | if (this.isDryRun()) { return callback(); } 126 | 127 | var world = this; 128 | 129 | if (!world.supportCodePaths) { 130 | world.supportCodePaths = []; 131 | } 132 | 133 | world.supportCodePaths.push(path); 134 | 135 | callback(); 136 | }); 137 | 138 | this.Given(/^dry run mode$/, function(callback) { 139 | if (this.isDryRun()) { return callback(); } 140 | 141 | var world = this; 142 | 143 | world.dryRun = true; 144 | 145 | callback(); 146 | }); 147 | 148 | this.Given(/^the environment variable '(.*)' is set to '(.*)'$/, function(name, value, callback) { 149 | if (this.isDryRun()) { return callback(); } 150 | 151 | var world = this; 152 | 153 | if (!world.env) { 154 | world.env = {}; 155 | } 156 | 157 | world.env[name] = value; 158 | 159 | callback(); 160 | }); 161 | 162 | this.Given(/^'(.*)' max retries$/, function(maxRetries, callback) { 163 | if (this.isDryRun()) { return callback(); } 164 | 165 | var world = this; 166 | 167 | world.maxRetries = maxRetries; 168 | 169 | callback(); 170 | }); 171 | 172 | this.When(/^executing the parallel-cucumber-js bin$/, function(callback) { 173 | if (this.isDryRun()) { return callback(); } 174 | 175 | var world = this; 176 | 177 | var args = ['../bin/parallel-cucumber-js']; 178 | 179 | if (world.customerCucumberPath) { 180 | args.push('--cucumber'); 181 | args.push(world.customerCucumberPath); 182 | } 183 | 184 | if (world.tags) { 185 | args.push('-t'); 186 | args.push(world.tags); 187 | } 188 | 189 | if (world.profiles) { 190 | Object.keys(world.profiles).forEach(function(profileName) { 191 | var profile = world.profiles[profileName]; 192 | var profileDefined = false; 193 | 194 | if (profile.tags) { 195 | profile.tags.forEach(function(tags) { 196 | args.push('--profiles.' + profileName + '.tags'); 197 | args.push(tags); 198 | }); 199 | 200 | profileDefined = true; 201 | } 202 | 203 | if (profile.env) { 204 | Object.keys(profile.env).forEach(function(envName) { 205 | var envValue = profile.env[envName]; 206 | args.push('--profiles.' + profileName + '.env.' + envName); 207 | args.push(envValue); 208 | }); 209 | 210 | profileDefined = true; 211 | } 212 | 213 | if (!profileDefined) { 214 | args.push('--profiles.' + profileName); 215 | } 216 | }); 217 | } 218 | 219 | if (world.formatters) { 220 | Object.keys(world.formatters).forEach(function(formatterName) { 221 | var formatter = world.formatters[formatterName]; 222 | 223 | var formatterExpression = formatterName; 224 | 225 | if (formatter.out) { 226 | formatterExpression += ':' + formatter.out; 227 | } 228 | 229 | args.push('-f'); 230 | args.push(formatterExpression); 231 | }); 232 | } 233 | 234 | if (world.supportCodePaths) { 235 | world.supportCodePaths.forEach(function(supportCodePath) { 236 | args.push('-r'); 237 | args.push(supportCodePath); 238 | }); 239 | } 240 | 241 | if (world.workerCount) { 242 | args.push('-w'); 243 | args.push(world.workerCount); 244 | } 245 | 246 | if (world.dryRun) { 247 | args.push('-d'); 248 | } 249 | 250 | if (world.maxRetries) { 251 | args.push('--max-retries'); 252 | args.push(world.maxRetries); 253 | } 254 | 255 | if (world.features) { 256 | world.features.forEach(function(feature) { 257 | args.push(feature); 258 | }); 259 | 260 | args.push('-r'); 261 | args.push('features/'); 262 | } 263 | 264 | if (!world.env) { 265 | world.env = {}; 266 | } 267 | 268 | var env = {}; 269 | 270 | Object.keys(process.env).forEach(function(envName) { 271 | env[envName] = process.env[envName]; 272 | }); 273 | 274 | Object.keys(world.env).forEach(function(envName) { 275 | env[envName] = world.env[envName]; 276 | }); 277 | 278 | world.child = ChildProcess.spawn('node', args, { 279 | stdio: ['ignore', 'pipe', 'pipe'], 280 | cwd: 'test_assets', 281 | env: env 282 | }); 283 | 284 | var stdout = []; 285 | var stderr = []; 286 | 287 | world.child.stdout.on('data', function (data) { 288 | stdout.push(data); 289 | }); 290 | 291 | world.child.stderr.on('data', function (data) { 292 | stderr.push(data); 293 | process.stderr.write(data); 294 | }); 295 | 296 | world.child.on('exit', function(code) { 297 | world.stdout = stdout.join(''); 298 | world.stderr = stderr.join(''); 299 | world.exitCode = code; 300 | 301 | callback(); 302 | }); 303 | }); 304 | 305 | this.Then(/^the exit code should be '(.*)'$/, function(exitCode, callback) { 306 | if (this.isDryRun()) { return callback(); } 307 | 308 | var world = this; 309 | 310 | if (world.exitCode !== parseInt(exitCode, 10)) { 311 | callback(JSON.stringify({ message: 'Unexpected value', expected: exitCode, actual: world.exitCode, stdout: world.stdout })); 312 | } 313 | else { 314 | callback(); 315 | } 316 | }); 317 | 318 | this.Then(/^(.*) should contain JSON matching:$/, function(stream, expectedJson, callback) { 319 | if (this.isDryRun()) { return callback(); } 320 | 321 | var world = this; 322 | 323 | try { 324 | expectedJson = JSON.parse(expectedJson); 325 | } 326 | catch (e) { 327 | if (!(e instanceof SyntaxError)) { 328 | throw e; 329 | } 330 | 331 | callback({ message: 'Syntax error in expected JSON', error: e, expectedJson: expectedJson }); 332 | return; 333 | } 334 | 335 | var actualJson = world[stream]; 336 | 337 | try { 338 | actualJson = JSON.parse(actualJson); 339 | } 340 | catch (e) { 341 | if (!(e instanceof SyntaxError)) { 342 | throw e; 343 | } 344 | 345 | callback({ message: 'Syntax error in actual JSON', error: e, actualJson: actualJson }); 346 | return; 347 | } 348 | 349 | //console.log(JSON.stringify(actualJson, null, ' ')); 350 | 351 | actualJson.forEach(function(feature) { 352 | if (typeof feature.uri === 'string') { 353 | feature.uri = feature.uri.replace(/^.*[\\\/]features[\\\/]/i, '{uri}/features/').replace('\\', '/'); 354 | } 355 | 356 | if (feature.elements) { 357 | feature.elements.forEach(function(element) { 358 | element.steps.forEach(function(step) { 359 | if (typeof step.result.duration === 'number' && step.result.duration > 0) { 360 | step.result.duration = '{duration}'; 361 | } 362 | if (step.match && typeof step.match.location === 'string') { 363 | step.match.location = '{location}'; 364 | } 365 | }); 366 | }); 367 | } 368 | }); 369 | 370 | normalizeJsonFeatureOrder(expectedJson); 371 | normalizeJsonFeatureOrder(actualJson); 372 | 373 | var differences = DeepDiff.diff(actualJson, expectedJson); 374 | 375 | if (differences) { 376 | var transformedDifferences = []; 377 | 378 | differences.forEach(function(difference) { 379 | var path = ''; 380 | 381 | difference.path.forEach(function(pathPart) { 382 | path += '/' + pathPart; 383 | }); 384 | 385 | transformedDifferences.push({ 386 | kind: difference.kind, 387 | path: path, 388 | lhs: difference.lhs, 389 | rhs: difference.rhs 390 | }); 391 | }); 392 | 393 | callback({ message: 'Actual JSON did not match expected JSON', differences: transformedDifferences }); 394 | } 395 | else { 396 | callback(); 397 | } 398 | }); 399 | 400 | this.Then(/^(.*) should contain new line separated YAML matching:$/, function(stream, expectedYaml, callback) { 401 | if (this.isDryRun()) { return callback(); } 402 | 403 | var world = this; 404 | 405 | expectedYaml = normalizeMultiLineYaml(expectedYaml); 406 | var actualYaml = normalizeMultiLineYaml(world[stream]); 407 | 408 | var zeroOrGreaterNumberKeys = ['worker']; 409 | var durationKeys = ['duration', 'duration', 'elapsed', 'saved']; 410 | var durationRegExp = /^-?\d{2,}:\d{2}:\d{2}\.\d{3}$/; 411 | var percentageKeys = ['savings']; 412 | var percentageRegExp = /^-?\d{1,}\.\d{2}%$/; 413 | 414 | actualYaml.forEach(function(event) { 415 | Object.keys(event).forEach(function(itemKey) { 416 | var item = event[itemKey]; 417 | 418 | zeroOrGreaterNumberKeys.forEach(function(valueKey) { 419 | if (typeof item[valueKey] === 'number' && item[valueKey] >= 0) { 420 | item[valueKey] = '{zeroOrGreaterNumber}'; 421 | } 422 | }); 423 | 424 | durationKeys.forEach(function(valueKey) { 425 | if (typeof item[valueKey] === 'string' && durationRegExp.test(item[valueKey])) { 426 | item[valueKey] = '{duration}'; 427 | } 428 | }); 429 | 430 | percentageKeys.forEach(function(valueKey) { 431 | if (typeof item[valueKey] === 'string' && percentageRegExp.test(item[valueKey])) { 432 | item[valueKey] = '{percentage}'; 433 | } 434 | }); 435 | }); 436 | }); 437 | 438 | expectedYaml = dumpMultiLineYaml(expectedYaml); 439 | actualYaml = dumpMultiLineYaml(actualYaml); 440 | 441 | var differences = diffText(expectedYaml, actualYaml); 442 | 443 | if (differences) { 444 | callback({ message: 'Actual YAML did not match expected YAML', differences: differences }); 445 | } 446 | else { 447 | callback(); 448 | } 449 | }); 450 | 451 | this.Then(/^(.*) should contain text matching:$/, function(stream, expectedText, callback) { 452 | if (this.isDryRun()) { return callback(); } 453 | 454 | var world = this; 455 | 456 | expectedText = normalizeText(expectedText); 457 | var actualText = normalizeText(world[stream]); 458 | 459 | var differences = diffText(expectedText, actualText); 460 | 461 | if (differences) { 462 | callback({ messages: 'Actual text did not match expected text', differences: differences }); 463 | } 464 | else { 465 | callback(); 466 | } 467 | }); 468 | 469 | this.Then(/^(.*) should be empty$/, function(stream, callback) { 470 | if (this.isDryRun()) { return callback(); } 471 | 472 | var world = this; 473 | 474 | var actualText = world[stream]; 475 | 476 | if (actualText.length > 0) { 477 | callback({ message: 'Expected stderr to be empty but it was not', actualText: actualText }); 478 | } 479 | else { 480 | callback(); 481 | } 482 | }); 483 | }; 484 | 485 | function diffText(expectedText, actualText) { 486 | var differences = Diff.diffLines(expectedText, actualText); 487 | 488 | var different = false; 489 | 490 | differences.forEach(function(part) { 491 | if (part.added || part.removed) { 492 | different = true; 493 | } 494 | }); 495 | 496 | if (!different) { 497 | return undefined; 498 | } 499 | 500 | var unifiedDiff = Diff.createPatch('text', expectedText, actualText); 501 | 502 | return unifiedDiff; 503 | } 504 | 505 | function normalizeJsonFeatureOrder(report) { 506 | report.sort(function(a, b) { 507 | if (a.uri < b.uri) { return -1; } 508 | if (a.uri > b.uri) { return 1; } 509 | if (a.profile < b.profile) { return -1; } 510 | if (a.profile > b.profile) { return 1; } 511 | return 0; 512 | }); 513 | } 514 | 515 | function normalizeMultiLineYaml(value) { 516 | value = normalizeText(value); 517 | var lines = value.split('\n'); 518 | var events = []; 519 | 520 | lines.forEach(function(line) { 521 | events.push(JSYAML.load(line)); 522 | }); 523 | 524 | events.sort(function(a, b) { 525 | if (a.scenario && b.scenario) { 526 | if (a.uri < b.uri) { return -1; } 527 | if (a.uri > b.uri) { return 1; } 528 | if (a.profile < b.profile) { return -1; } 529 | if (a.profile > b.profile) { return 1; } 530 | return 0; 531 | } 532 | 533 | if (a.feature && b.feature) { 534 | if (a.uri < b.uri) { return -1; } 535 | if (a.uri > b.uri) { return 1; } 536 | if (a.profile < b.profile) { return -1; } 537 | if (a.profile > b.profile) { return 1; } 538 | return 0; 539 | } 540 | 541 | if (a.summary && b.summary) { 542 | if (a.status < b.status) { return -1; } 543 | if (a.status > b.status) { return 1; } 544 | return 0; 545 | } 546 | 547 | if (a.scenario) { return -1; } 548 | if (b.scenario) { return 1; } 549 | if (a.feature) { return -1; } 550 | if (b.feature) { return 1; } 551 | if (a.summary) { return -1; } 552 | if (b.summary) { return 1; } 553 | return 0; 554 | }); 555 | 556 | return events; 557 | } 558 | 559 | function dumpMultiLineYaml(multiLineYaml) { 560 | var lines = []; 561 | 562 | multiLineYaml.forEach(function(yaml) { 563 | lines.push(JSYAML.safeDump(yaml, { flowLevel: 0 })); 564 | }); 565 | 566 | return lines.join('\n'); 567 | } 568 | 569 | function normalizeText(value) { 570 | value = StripColorCodes(value); 571 | value = value.replace(/\r\n/gm, '\n'); 572 | value = value.replace(/^\n+/gm, ''); 573 | value = value.replace(/\n+$/gm, ''); 574 | value = value.replace(/^\s+/gm, ''); 575 | value = value.replace(/\s+$/gm, ''); 576 | value = value.replace(/\s+\n/gm, '\n'); 577 | value = value.replace(/\n\s+/gm, '\n'); 578 | return value; 579 | } 580 | -------------------------------------------------------------------------------- /features/support/world.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.World = function() { 3 | var world = this; 4 | 5 | world.isDryRun = function() { 6 | return process.argv.indexOf('--dry-run') !== -1 || process.env.PARALLEL_CUCUMBER_DRY_RUN === 'true'; 7 | }; 8 | 9 | }; 10 | } ; -------------------------------------------------------------------------------- /features/tags.feature: -------------------------------------------------------------------------------- 1 | Feature: Tags 2 | 3 | Scenario: Tags 4 | Given the 'tags' feature 5 | And the '@tag-1' tag 6 | And a 'json' formatter 7 | When executing the parallel-cucumber-js bin 8 | Then the exit code should be '0' 9 | And stdout should contain JSON matching: 10 | """ 11 | [ 12 | { 13 | "id": "tags", 14 | "name": "Tags", 15 | "tags": [], 16 | "line": 1, 17 | "keyword": "Feature", 18 | "uri": "{uri}/features/tags.feature", 19 | "elements": [ 20 | { 21 | "name": "Tagged", 22 | "id": "tags;tagged", 23 | "line": 4, 24 | "keyword": "Scenario", 25 | "type": "scenario", 26 | "tags": [ 27 | { 28 | "name": "@tag-1", 29 | "line": 3 30 | } 31 | ], 32 | "steps": [ 33 | { 34 | "arguments": [], 35 | "name": "a passing action is executed", 36 | "line": 5, 37 | "keyword": "When ", 38 | "result": { 39 | "duration": "{duration}", 40 | "status": "passed" 41 | }, 42 | "match": { 43 | "location": "{location}" 44 | } 45 | } 46 | ] 47 | } 48 | ], 49 | "profile": "default", 50 | "retry": 0 51 | } 52 | ] 53 | """ 54 | And stderr should be empty 55 | 56 | Scenario: Tags, combining tags with ANDs 57 | Given the 'tags' feature 58 | And the '@tag-1' tag 59 | And the '@tag-2' tag 60 | And a 'json' formatter 61 | When executing the parallel-cucumber-js bin 62 | Then the exit code should be '0' 63 | And stdout should contain JSON matching: 64 | """ 65 | [ 66 | { 67 | "id": "tags", 68 | "name": "Tags", 69 | "tags": [], 70 | "line": 1, 71 | "keyword": "Feature", 72 | "uri": "{uri}/features/tags.feature", 73 | "elements": [ 74 | { 75 | "name": "Tagged twice", 76 | "id": "tags;tagged-twice", 77 | "line": 8, 78 | "keyword": "Scenario", 79 | "type": "scenario", 80 | "tags": [ 81 | { 82 | "name": "@tag-2", 83 | "line": 7 84 | }, 85 | { 86 | "name": "@tag-3", 87 | "line": 7 88 | } 89 | ], 90 | "steps": [ 91 | { 92 | "arguments": [], 93 | "name": "a passing action is executed", 94 | "line": 9, 95 | "keyword": "When ", 96 | "result": { 97 | "duration": "{duration}", 98 | "status": "passed" 99 | }, 100 | "match": { 101 | "location": "{location}" 102 | } 103 | } 104 | ] 105 | } 106 | ], 107 | "profile": "default", 108 | "retry": 0 109 | } 110 | ] 111 | """ 112 | And stderr should be empty 113 | -------------------------------------------------------------------------------- /lib/cucumber/runtime/event_broadcaster.js: -------------------------------------------------------------------------------- 1 | // flag to prevent firing BeforeFeatures more than once 2 | var fireBeforeFeatures = true, 3 | // saves AfterFeatures event to fire once after all features 4 | afterFeaturesEvent = null; 5 | 6 | function EventBroadcaster(listeners, supportCodeLibrary) { 7 | var Cucumber = require('cucumber'), 8 | listenerDefaultTimeout = supportCodeLibrary.getDefaultTimeout(), 9 | supportCodeListeners = supportCodeLibrary.getListeners(); 10 | 11 | 12 | var self = { 13 | broadcastAroundEvent: function broadcastAroundEvent(event, userFunction, callback) { 14 | self.broadcastBeforeEvent(event, function() { 15 | userFunction(function() { 16 | var userFunctionCallbackArguments = arguments; 17 | self.broadcastAfterEvent(event, function() { 18 | callback.apply(null, userFunctionCallbackArguments); 19 | }); 20 | }); 21 | }); 22 | }, 23 | 24 | broadcastBeforeEvent: function broadcastBeforeEvent(event, callback) { 25 | var preEvent = event.replicateAsPreEvent(); 26 | self.broadcastEvent(preEvent, callback); 27 | }, 28 | 29 | broadcastAfterEvent: function broadcastAfterEvent(event, callback) { 30 | var postEvent = event.replicateAsPostEvent(); 31 | self.broadcastEvent(postEvent, callback); 32 | }, 33 | 34 | broadcastEvent: function broadcastEvent(event, callback) { 35 | Cucumber.Util.asyncForEach(getAllListeners(), function (listener, callback) { 36 | listener.hear(event, listenerDefaultTimeout, function(error) { 37 | if (error) { 38 | process.nextTick(function(){ throw error; }); // prevent swallow by unhandled rejection 39 | } 40 | callback(); 41 | }); 42 | }, callback); 43 | } 44 | }; 45 | 46 | var realBroadcastEvent = self.broadcastEvent; 47 | 48 | self.broadcastEventUncensored = function broadcastEventUncensored(event, callback) { 49 | realBroadcastEvent(event, callback); 50 | }; 51 | 52 | self.broadcastAfterFeaturesEvent = function broadcastAfterFeaturesEvent(callback) { 53 | self.broadcastEventUncensored(afterFeaturesEvent, callback); 54 | }; 55 | 56 | // customised broadcaster in order to fire BeforeFeatures and AfterFeatures once 57 | self.broadcastEvent = function (event, callback) { 58 | if (isCensoredEvent(event)) { 59 | disableSupportCodeListeners(); 60 | realBroadcastEvent(event, function () { 61 | enableSupportCodeListeners(); 62 | callback(); 63 | }); 64 | } 65 | else { 66 | realBroadcastEvent(event, callback); 67 | } 68 | }; 69 | 70 | 71 | function getAllListeners() { 72 | return listeners.concat(supportCodeListeners); 73 | } 74 | 75 | function disableSupportCodeListeners() { 76 | supportCodeListeners = []; 77 | } 78 | 79 | function enableSupportCodeListeners() { 80 | supportCodeListeners = supportCodeLibrary.getListeners(); 81 | } 82 | 83 | function isCensoredEvent(event) { 84 | var name = event.getName(); 85 | 86 | if (name === 'BeforeFeatures') { 87 | if (fireBeforeFeatures) { 88 | fireBeforeFeatures = false; 89 | return false; 90 | } 91 | return true; 92 | } 93 | 94 | else if (name === 'AfterFeatures') { 95 | afterFeaturesEvent = event; 96 | return true; 97 | } 98 | 99 | else { 100 | return false; 101 | } 102 | } 103 | 104 | return self; 105 | } 106 | 107 | module.exports = EventBroadcaster; 108 | -------------------------------------------------------------------------------- /lib/cucumber/runtime/features_runner.js: -------------------------------------------------------------------------------- 1 | function FeaturesRunner(features, supportCodeLibrary, listeners, options) { 2 | var Cucumber = require('cucumber'); 3 | 4 | // branch EventBroadcaster with access to supportCodeLibrary 5 | var eventBroadcaster = Cucumber.Runtime.EventBroadcaster(listeners, supportCodeLibrary); 6 | var featuresResult = Cucumber.Runtime.FeaturesResult(options.strict); 7 | 8 | var self = { 9 | run: function run(callback) { 10 | var event = Cucumber.Runtime.Event(Cucumber.Events.FEATURES_EVENT_NAME, features); 11 | eventBroadcaster.broadcastAroundEvent( 12 | event, 13 | function (callback) { 14 | Cucumber.Util.asyncForEach(features, self.runFeature, function() { 15 | self.broadcastFeaturesResult(callback); 16 | }); 17 | }, 18 | function() { 19 | callback(featuresResult.isSuccessful()); 20 | } 21 | ); 22 | }, 23 | 24 | broadcastFeaturesResult: function visitFeaturesResult(callback) { 25 | var event = Cucumber.Runtime.Event(Cucumber.Events.FEATURES_RESULT_EVENT_NAME, featuresResult); 26 | eventBroadcaster.broadcastEvent(event, callback); 27 | }, 28 | 29 | runFeature: function runFeature(feature, callback) { 30 | if (!featuresResult.isSuccessful() && options.failFast) { 31 | return callback(); 32 | } 33 | var event = Cucumber.Runtime.Event(Cucumber.Events.FEATURE_EVENT_NAME, feature); 34 | eventBroadcaster.broadcastAroundEvent( 35 | event, 36 | function (callback) { 37 | Cucumber.Util.asyncForEach(feature.getScenarios(), self.runScenario, callback); 38 | }, 39 | callback 40 | ); 41 | }, 42 | 43 | runScenario: function runScenario(scenario, callback) { 44 | if (!featuresResult.isSuccessful() && options.failFast) { 45 | return callback(); 46 | } 47 | 48 | var scenarioRunner = Cucumber.Runtime.ScenarioRunner(scenario, supportCodeLibrary, eventBroadcaster, options); 49 | scenarioRunner.run(function(scenarioResult) { 50 | featuresResult.witnessScenarioResult(scenarioResult); 51 | callback(); 52 | }); 53 | } 54 | }; 55 | return self; 56 | } 57 | 58 | module.exports = FeaturesRunner; 59 | -------------------------------------------------------------------------------- /lib/parallel_cucumber.js: -------------------------------------------------------------------------------- 1 | var ParallelCucumber = {}; 2 | 3 | ParallelCucumber.Cli = require('./parallel_cucumber/cli'); 4 | ParallelCucumber.Formatters = require('./parallel_cucumber/formatters'); 5 | ParallelCucumber.Runtime = require('./parallel_cucumber/runtime'); 6 | 7 | module.exports = ParallelCucumber; 8 | -------------------------------------------------------------------------------- /lib/parallel_cucumber/cli.js: -------------------------------------------------------------------------------- 1 | var Cli = {}; 2 | 3 | Cli.Main = require('./cli/main'); 4 | Cli.Configuration = require('./cli/configuration'); 5 | 6 | module.exports = Cli; 7 | -------------------------------------------------------------------------------- /lib/parallel_cucumber/cli/configuration.js: -------------------------------------------------------------------------------- 1 | var Configuration = function(argv) { 2 | var Yargs = require('yargs'); 3 | var Debug = require('debug')('parallel-cucumber-js'); 4 | var OS = require('os'); 5 | 6 | var self = {}; 7 | 8 | var defaultWorkerCount = OS.cpus().length; 9 | 10 | if (defaultWorkerCount < 2) { 11 | defaultWorkerCount = 2; 12 | } 13 | 14 | self.yargs = Yargs 15 | .usage('Usage: $0 options [FILE|DIR]*') 16 | .options('h', { 17 | alias: 'help', 18 | describe: 'Displays this help message' 19 | }) 20 | .options('f', { 21 | alias: 'format', 22 | describe: 'How to format the results of executing features. Syntax is FORMAT[:PATH] where FORMAT can be json or progress or the path to a custom formatter script. The default format is progress. If specified, PATH is the file that output will be written to' 23 | }) 24 | .options('w', { 25 | alias: 'workers', 26 | default: defaultWorkerCount, 27 | describe: 'Number of instances of cucumber to run in parallel. Defaults to the number of CPU cores' 28 | }) 29 | .options('r', { 30 | alias: 'require', 31 | describe: 'Require support code files before executing the features. Specifying this option disables automatic loading.' 32 | }) 33 | .options('t', { 34 | alias: 'tags', 35 | describe: 'Tags of the features or scenarios to execute. Sets the tags for the default profile. Use --profile.name.tags to set the tags for a profile' 36 | }) 37 | .options('profiles.profile_name.tags', { 38 | describe: 'Tags of the features or scenarios to execute for a profile. Replace profile_name in the arg with the name of the profile' 39 | }) 40 | .options('profiles.profile_name.env.env_name', { 41 | describe: 'An environment variable to set when executing Cucumber scenarios for a profile. Replace profile_name in the arg with the name of the profile. Replace env_name with the name of the environment variable' 42 | }) 43 | .options('cucumber', { 44 | describe: 'Use a custom version of cucumber-js. Specifies either a relative path or the name of a module', 45 | default: 'cucumber' 46 | }) 47 | .options('max-retries', { 48 | default: 0, 49 | describe: 'Maximum number of retries for a failing feature. Defaults to no retries' 50 | }) 51 | .options('d', { 52 | alias: 'dry-run', 53 | boolean: true, 54 | default: false, 55 | describe: 'Enables cucumber\'s dry run mode' 56 | }) 57 | .options('debug', { 58 | describe: 'Starts cucumber workers in debug mode. Pass a number to set the port the first worker should listen on. Subsequent workers will increment the port number. Default debug port is the node.js standard of 5858' 59 | }) 60 | .options('debug-brk', { 61 | describe: 'Starts cucumber workers in debug mode and breaks on the first line. Accepts an optional port number like --debug' 62 | }); 63 | var args = self.yargs.parse(argv.slice(2)); 64 | 65 | self.help = args['help']; 66 | self.formats = ensureValueIsAnArray(args['format']); 67 | self.workerCount = args['workers']; 68 | self.supportCodePaths = ensureValueIsAnArray(args['require']); 69 | self.tags = ensureValueIsAnArray(args['tags']); 70 | self.profiles = args['profiles']; 71 | self.cucumberPath = args['cucumber']; 72 | self.maxRetries = args['max-retries']; 73 | self.dryRun = args['dry-run']; 74 | self.debug = args['debug']; 75 | self.debugBrk = args['debug-brk']; 76 | // Clone the array using slice() 77 | self.featurePaths = args._.slice(); 78 | 79 | if (self.formats.length === 0) { 80 | self.formats.push('progress'); 81 | } 82 | 83 | if (!self.profiles) { 84 | self.profiles = { 'default': {} }; 85 | } 86 | 87 | Debug('Profiles:', self.profiles); 88 | 89 | if (self.tags.length > 0) { 90 | if (!self.profiles['default']) { 91 | self.profiles['default'] = {}; 92 | } 93 | 94 | self.profiles['default'].tags = self.tags; 95 | } 96 | 97 | delete self.tags; 98 | 99 | Debug('Profiles:', self.profiles); 100 | 101 | Object.keys(self.profiles).forEach(function(profileName) { 102 | var profile = self.profiles[profileName]; 103 | profile.tags = ensureValueIsAnArray(profile.tags); 104 | }); 105 | 106 | Debug('Profiles:', self.profiles); 107 | 108 | if (self.featurePaths.length === 0) { 109 | self.featurePaths.push('./features/'); 110 | } 111 | 112 | Debug('Configuration:', self); 113 | 114 | self.showHelp = function() { 115 | self.yargs.showHelp(); 116 | }; 117 | 118 | return self; 119 | }; 120 | 121 | function ensureValueIsAnArray(value) { 122 | if (value === null || value === undefined) { 123 | return []; 124 | } 125 | 126 | return Array.isArray(value) ? value : [value]; 127 | } 128 | 129 | module.exports = Configuration; -------------------------------------------------------------------------------- /lib/parallel_cucumber/cli/main.js: -------------------------------------------------------------------------------- 1 | var Main = function() { 2 | var ParallelCucumber = require('../../parallel_cucumber'); 3 | var Debug = require('debug')('parallel-cucumber-js'); 4 | 5 | var self = {}; 6 | 7 | self.start = function() { 8 | var configuration = ParallelCucumber.Cli.Configuration(process.argv); 9 | 10 | if (configuration.help) { 11 | configuration.showHelp(); 12 | self._exit(0); 13 | } 14 | else { 15 | ParallelCucumber.Runtime(configuration).run(function(err, info) { 16 | if (err) { 17 | Debug('Exiting'); 18 | console.error(err); 19 | self._exit(2); 20 | } 21 | else { 22 | var code = info.success ? 0 : 1; 23 | self._exit(code); 24 | } 25 | }); 26 | } 27 | }; 28 | 29 | self._exit = function(code) { 30 | Debug('Exiting with code ' + code); 31 | 32 | // See https://github.com/joyent/node/issues/3737 for more information 33 | // on why the process exit event is used 34 | process.on('exit', function() { 35 | process.exit(code); 36 | }); 37 | }; 38 | 39 | return self; 40 | }; 41 | 42 | module.exports = Main; -------------------------------------------------------------------------------- /lib/parallel_cucumber/formatters.js: -------------------------------------------------------------------------------- 1 | var Formatters = {}; 2 | 3 | Formatters.Formatter = require('./formatters/formatter'); 4 | Formatters.JsonFormatter = require('./formatters/json_formatter'); 5 | Formatters.ProgressFormatter = require('./formatters/progress_formatter'); 6 | 7 | module.exports = Formatters; 8 | -------------------------------------------------------------------------------- /lib/parallel_cucumber/formatters/formatter.js: -------------------------------------------------------------------------------- 1 | var Formatter = function(options) { 2 | var Events = require('events'); 3 | var FS = require('fs'); 4 | 5 | var self = new Events.EventEmitter(); 6 | 7 | if (options.outFilePath) { 8 | self._out = FS.createWriteStream(options.outFilePath, { flags: 'w' }); 9 | self._outNeedsClosing = true; 10 | } 11 | else { 12 | self._out = process.stdout; 13 | } 14 | 15 | self._out.on('error', function(err) { 16 | self.emit('error', err); 17 | }); 18 | 19 | self.formatFeatures = function(options) { 20 | options.features.forEach(function(feature) { 21 | self.formatFeature({ workerIndex: options.workerIndex, profileName: options.profileName, feature: feature }); 22 | }); 23 | }; 24 | 25 | self._write = function(data) { 26 | self._out.write(data, 'utf8'); 27 | }; 28 | 29 | self.end = function(callback) { 30 | if (self._outNeedsClosing) { 31 | self._outNeedsClosing = false; 32 | self._out.end(callback); 33 | } 34 | else { 35 | callback(); 36 | } 37 | }; 38 | 39 | return self; 40 | }; 41 | 42 | module.exports = Formatter; -------------------------------------------------------------------------------- /lib/parallel_cucumber/formatters/json_formatter.js: -------------------------------------------------------------------------------- 1 | var JsonFormatter = function(options) { 2 | var ParallelCucumber = require('../../parallel_cucumber'); 3 | var OS = require('os'); 4 | var Debug = require('debug')('parallel-cucumber-js'); 5 | 6 | var self = ParallelCucumber.Formatters.Formatter(options); 7 | 8 | self.type = 'json'; 9 | self._firstFeature = true; 10 | self._superEnd = self.end; 11 | self._features = []; 12 | 13 | self.formatFeature = function(options) { 14 | var newFeature = true; 15 | var currentFeature = options.feature; 16 | self._features.forEach(function(feature) { 17 | if (feature.uri === currentFeature.uri && feature.profile === currentFeature.profile && feature.retry === currentFeature.retry) { 18 | Debug('Existing feature, merging elements (scenarios)'); 19 | newFeature = false; 20 | feature.elements = feature.elements.concat(currentFeature.elements); 21 | feature.elements.sort(function(a, b) { 22 | return a.line - b.line; 23 | }); 24 | } 25 | }); 26 | if (newFeature) { 27 | Debug('New feature'); 28 | self._features.push(currentFeature); 29 | } 30 | }; 31 | 32 | self.end = function(callback) { 33 | var json = JSON.stringify(self._features); 34 | self._write(json); 35 | self._write(OS.EOL); 36 | 37 | self._superEnd(callback); 38 | }; 39 | 40 | return self; 41 | }; 42 | 43 | module.exports = JsonFormatter; -------------------------------------------------------------------------------- /lib/parallel_cucumber/formatters/progress_formatter.js: -------------------------------------------------------------------------------- 1 | var ProgressFormatter = function(options) { 2 | var ParallelCucumber = require('../../parallel_cucumber'); 3 | var Path = require('path'); 4 | var JSYAML = require('js-yaml'); 5 | require('colors'); 6 | var Debug = require('debug')('parallel-cucumber-js'); 7 | 8 | var self = ParallelCucumber.Formatters.Formatter(options); 9 | self.type = 'progress'; 10 | self.cumulativeDuration = 0; 11 | self._superEnd = self.end; 12 | self._startTime = process.hrtime(); 13 | 14 | self.formatFeature = function(options) { 15 | var feature = options.feature; 16 | var featureUri = cleanUri(Path.relative(process.cwd(), feature.uri)); 17 | var featureDuration = 0; 18 | 19 | if (feature.elements) { 20 | feature.elements.forEach(function(element) { 21 | var elementStatus = 'stepless'; 22 | var errorMessage; 23 | var elementDuration = 0; 24 | 25 | if (element.steps) { 26 | var foundElementStatus = false; 27 | 28 | element.steps.forEach(function(step) { 29 | if (!foundElementStatus) { 30 | if (step.result) { 31 | elementStatus = step.result.status; 32 | foundElementStatus = elementStatus !== 'passed'; 33 | errorMessage = step.result['error_message']; 34 | 35 | if (step.result.duration) { 36 | elementDuration += step.result.duration; 37 | } 38 | } 39 | else { 40 | elementStatus = 'unknown'; 41 | foundElementStatus = true; 42 | } 43 | } 44 | }); 45 | } 46 | 47 | featureDuration += elementDuration; 48 | 49 | var event = { 50 | worker: options.workerIndex, 51 | status: elementStatus, 52 | profile: options.profileName, 53 | uri: featureUri + '/' + cleanUri(element.name), 54 | duration: formatDuration(elementDuration) 55 | }; 56 | 57 | if (errorMessage) { 58 | event.errorMessage = errorMessage; 59 | } 60 | 61 | self._logEvent(element.type, event); 62 | }); 63 | } 64 | 65 | self.cumulativeDuration += featureDuration; 66 | 67 | self._logEvent('feature', { 68 | worker: options.workerIndex, 69 | status: 'finished', 70 | profile: options.profileName, 71 | uri: featureUri, 72 | duration: formatDuration(featureDuration) 73 | }); 74 | }; 75 | 76 | self.end = function(callback) { 77 | var elapsedDuration = process.hrtime(self._startTime); 78 | elapsedDuration = elapsedDuration[0] * 1e9 + elapsedDuration[1]; 79 | var savedTime = self.cumulativeDuration - elapsedDuration; 80 | var savingsPercentage = savedTime / self.cumulativeDuration * 100; 81 | 82 | if (savingsPercentage < -100) { 83 | savingsPercentage = -100; 84 | } 85 | 86 | self._logEvent('summary', { 87 | status: 'finished', 88 | duration: formatDuration(self.cumulativeDuration), 89 | elapsed: formatDuration(elapsedDuration), 90 | saved: formatDuration(savedTime), 91 | savings: formatPercentage(savingsPercentage) 92 | }); 93 | 94 | self._superEnd(callback); 95 | }; 96 | 97 | self._logEvent = function(key, item) { 98 | Debug('Event', key, item); 99 | var event = {}; 100 | event[key] = self._expandNestedJson(item); 101 | var objectDepth = self._findObjectDepth(event); 102 | var flowLevel = objectDepth > 2 ? -1 : 1; 103 | 104 | var eventYaml = JSYAML.safeDump( 105 | event, 106 | { 107 | flowLevel: flowLevel 108 | } 109 | ); 110 | 111 | if (item.status === 'passed') { 112 | eventYaml = eventYaml.green; 113 | } 114 | else if (item.status === 'failed') { 115 | eventYaml = eventYaml.red; 116 | } 117 | else if (item.status === 'stepless') { 118 | eventYaml = eventYaml.blue; 119 | } 120 | else { 121 | eventYaml = eventYaml.grey; 122 | } 123 | 124 | self._write(eventYaml); 125 | }; 126 | 127 | self._expandNestedJson = function(value) { 128 | if (Array.isArray(value)) { 129 | for (var i = 0, length = value.length; i < length; i++) { 130 | value[i] = self._expandNestedJson(value[i]); 131 | } 132 | } 133 | else if (value === Object(value)) { 134 | Object.keys(value).forEach(function(key) { 135 | value[key] = self._expandNestedJson(value[key]); 136 | }); 137 | } 138 | else if (typeof value === 'string') { 139 | var json; 140 | var isJson = false; 141 | 142 | try { 143 | json = JSON.parse(value); 144 | isJson = true; 145 | } 146 | catch (e) { 147 | if (!(e instanceof SyntaxError)) { 148 | throw e; 149 | } 150 | } 151 | 152 | if (isJson) { 153 | value = self._expandNestedJson(json); 154 | } 155 | } 156 | 157 | return value; 158 | }; 159 | 160 | self._findObjectDepth = function(value) { 161 | var depth = 0; 162 | var items; 163 | 164 | if (Array.isArray(value)) { 165 | items = value; 166 | } 167 | else if (value === Object(value)) { 168 | items = []; 169 | Object.keys(value).forEach(function(key) { 170 | items.push(value[key]); 171 | }); 172 | } 173 | 174 | if (items) { 175 | items.forEach(function(item) { 176 | depth = Math.max(1 + self._findObjectDepth(item), depth); 177 | }); 178 | } 179 | 180 | return depth; 181 | }; 182 | 183 | return self; 184 | }; 185 | 186 | function cleanUri(value) { 187 | return value.replace(/[^a-zA-Z0-9\\/]/g, '_').replace(/\\/g, '/'); 188 | } 189 | 190 | function formatDuration(totalNanoseconds) { 191 | var sign; 192 | 193 | if (totalNanoseconds >= 0) { 194 | sign = ''; 195 | } 196 | else { 197 | sign = '-'; 198 | totalNanoseconds = -totalNanoseconds; 199 | } 200 | 201 | var totalMilliseconds = Math.floor(totalNanoseconds / 1000000); 202 | var milliseconds = totalMilliseconds % 1000; 203 | var totalSeconds = Math.floor(totalMilliseconds / 1000); 204 | var seconds = totalSeconds % 60; 205 | var totalMinutes = Math.floor(totalSeconds / 60); 206 | var minutes = totalMinutes % 60; 207 | var totalHours = Math.floor(totalMinutes / 60); 208 | 209 | var padding = '0'; 210 | var text = sign + padStringLeft(totalHours.toString(), padding, 2) + ':' + 211 | padStringLeft(minutes.toString(), padding, 2) + ':' + 212 | padStringLeft(seconds.toString(), padding, 2) + '.' + 213 | padStringLeft(milliseconds.toString(), padding, 3); 214 | return text; 215 | } 216 | 217 | function padStringLeft(value, padding, count) { 218 | while (value.length < count) { 219 | value = padding + value; 220 | } 221 | 222 | return value; 223 | } 224 | 225 | function formatPercentage(value) { 226 | return value.toFixed(2) + '%'; 227 | } 228 | 229 | module.exports = ProgressFormatter; -------------------------------------------------------------------------------- /lib/parallel_cucumber/runtime.js: -------------------------------------------------------------------------------- 1 | var Runtime = function (configuration) { 2 | var ParallelCucumber = require('../parallel_cucumber'); 3 | var Async = require('async'); 4 | var FS = require('fs'); 5 | var Path = require('path'); 6 | var Debug = require('debug')('parallel-cucumber-js'); 7 | var Gherkin = require('gherkin'); 8 | 9 | var self = {}; 10 | 11 | self._featureFinder = ParallelCucumber.Runtime.FeatureFinder(); 12 | self._supportCodeFinder = ParallelCucumber.Runtime.SupportCodeFinder(); 13 | 14 | self._configuration = configuration; 15 | 16 | self.run = function(callback) { 17 | self._executeFeatures( 18 | self._configuration, 19 | function(err, info) { 20 | callback(err, info); 21 | } 22 | ); 23 | }; 24 | 25 | self._executeFeatures = function(options, callback) { 26 | function done(err, info) { 27 | callback(err, info); 28 | } 29 | 30 | Async.parallel( 31 | [ 32 | Async.apply(self._featureFinder.find, { featurePaths: options.featurePaths, dryRun: options.dryRun, profiles: options.profiles }), 33 | Async.apply(self._supportCodeFinder.find, { featurePaths: options.featurePaths, supportCodePaths: options.supportCodePaths }) 34 | ], 35 | function(err, results) { 36 | if (err) { 37 | done(err); 38 | } 39 | else { 40 | options.featureFilePaths = results[0]; 41 | options.supportCodePaths = results[1]; 42 | 43 | Debug('Feature file paths:', options.featureFilePaths); 44 | Debug('Support code paths:', options.supportCodePaths); 45 | 46 | self._executeFeaturesOnWorkerPool( 47 | options, 48 | function(err, info) { 49 | done(err, info); 50 | } 51 | ); 52 | } 53 | } 54 | ); 55 | }; 56 | 57 | self._executeFeaturesOnWorkerPool = function(options, callback) { 58 | Debug('Spawning cucumber instances'); 59 | 60 | var finished = false; 61 | var success = true; 62 | 63 | function done(err) { 64 | if (finished) { return; } 65 | finished = true; 66 | 67 | callback(err, { success: success }); 68 | } 69 | 70 | Async.parallel( 71 | [ 72 | Async.apply(self._getTasks, options), 73 | Async.apply(self._createFormatters, options) 74 | ], 75 | function(err, results) { 76 | if (err) { 77 | done(err); 78 | return; 79 | } 80 | 81 | var tasks = results[0]; 82 | var noTasks = tasks.length === 0; 83 | 84 | Debug('Found ' + tasks.length + ' tasks to execute'); 85 | Debug('Execution will be limited to ' + options.workerCount + ' workers'); 86 | 87 | var workerPool = ParallelCucumber.Runtime.WorkerPool({ workerCount: options.workerCount, cucumberPath: options.cucumberPath, dryRun: options.dryRun, debug: options.debug, debugBrk: options.debugBrk }); 88 | 89 | workerPool.on( 90 | 'next', 91 | function(callback) { 92 | if (tasks.length > 0) { 93 | var task = tasks.shift(); 94 | callback(task); 95 | } 96 | else { 97 | callback(); 98 | } 99 | } 100 | ); 101 | 102 | workerPool.on( 103 | 'report', 104 | function(info) { 105 | var task = info.task; 106 | 107 | if (task.retryCount > 0) { 108 | var nameSuffix = ' - retry ' + task.retryCount; 109 | 110 | info.report.forEach(function(feature) { 111 | feature.name += nameSuffix; 112 | }); 113 | } 114 | 115 | info.report.forEach(function(feature) { 116 | feature.profile = task.profileName; 117 | feature.retry = task.retryCount; 118 | }); 119 | 120 | self.formatters.forEach(function(formatter) { 121 | formatter.formatFeatures({ workerIndex: info.workerIndex, profileName: task.profileName, features: info.report }); 122 | }); 123 | 124 | if (!info.success) { 125 | if (task.retryCount < self._configuration.maxRetries) { 126 | task.retryCount++; 127 | tasks.push(task); 128 | } 129 | else { 130 | success = false; 131 | } 132 | } 133 | } 134 | ); 135 | 136 | workerPool.on( 137 | 'done', 138 | function() { 139 | Debug('Done'); 140 | Async.each( 141 | self.formatters, 142 | function(formatter, callback) { 143 | formatter.end(callback); 144 | }, 145 | function(err) { 146 | done(err); 147 | } 148 | ); 149 | } 150 | ); 151 | 152 | workerPool.on( 153 | 'error', 154 | function(err) { 155 | success = false; 156 | done(err); 157 | } 158 | ); 159 | 160 | workerPool.on( 161 | 'empty', 162 | function() { 163 | Debug('Empty'); 164 | Async.each( 165 | self.formatters, 166 | function(formatter, callback) { 167 | if (formatter.type !== 'progress') { 168 | formatter.end(callback); 169 | } 170 | }, 171 | function(err) { 172 | done(err); 173 | } 174 | ); 175 | } 176 | ); 177 | 178 | workerPool.start(); 179 | 180 | if (noTasks) { 181 | workerPool.emit('empty'); 182 | return; 183 | } 184 | } 185 | ); 186 | }; 187 | 188 | self._createFormatters = function(options, callback) { 189 | self.formatters = []; 190 | 191 | self._configuration.formats.forEach(function(format) { 192 | var parts = format.split(':'); 193 | var formatterName = parts[0]; 194 | var outFilePath = parts.slice(1).join(':'); 195 | var formatterType; 196 | 197 | if (formatterName.toLowerCase() === 'json') { 198 | formatterType = ParallelCucumber.Formatters.JsonFormatter; 199 | } 200 | else if (formatterName.toLowerCase() === 'progress') { 201 | formatterType = ParallelCucumber.Formatters.ProgressFormatter; 202 | } 203 | else { 204 | try { 205 | formatterType = require(Path.resolve(formatterName)); 206 | } 207 | catch (err) { 208 | callback({ message: 'Failed to load custom formatter', formatter: formatterName, innerError: err }); 209 | return; 210 | } 211 | } 212 | 213 | var formatter = formatterType({ outFilePath: outFilePath }); 214 | self.formatters.push(formatter); 215 | }); 216 | 217 | callback(); 218 | }; 219 | 220 | self._getTasks = function(options, callback) { 221 | var tasks = []; 222 | var taskCount = 0; 223 | 224 | options.featureFilePaths.forEach(function (featureFilePath) { 225 | self._getFeatureFileObjects(featureFilePath).forEach(function (featureFile) { 226 | Object.keys(options.profiles).forEach(function (profileName) { 227 | var profile = options.profiles[profileName]; 228 | // Clone the array using slice() 229 | var tags = profile.tags ? profile.tags.slice() : []; 230 | var env = profile.env || {}; 231 | 232 | env.PARALLEL_CUCUMBER_PROFILE = profileName; 233 | 234 | var matchTags = true; 235 | tags.forEach(function (tag) { 236 | if (matchTags) { 237 | if (tag.indexOf('~') === 0) { 238 | matchTags = featureFile.tags.indexOf(tag.substring(1)) === -1; 239 | } 240 | else { 241 | matchTags = featureFile.tags.indexOf(tag) > -1; 242 | } 243 | } 244 | }); 245 | if (matchTags) { 246 | var task = { 247 | taskIndex: taskCount++, 248 | profileName: profileName, 249 | featureFilePath: featureFile.path, 250 | supportCodePaths: options.supportCodePaths, 251 | tags: tags, 252 | env: env, 253 | retryCount: 0 254 | }; 255 | tasks.push(task); 256 | } 257 | }); 258 | }); 259 | }); 260 | 261 | callback(null, tasks); 262 | }; 263 | 264 | self._getFeatureFileObjects = function (featureFilePath) { 265 | var parser = new Gherkin.Parser(); 266 | var gherkinDocument = parser.parse(FS.readFileSync(featureFilePath, 'utf8')); 267 | var featureFileObjects = []; 268 | 269 | var featureTags = self._collectTags(gherkinDocument.feature.tags); 270 | if (gherkinDocument.feature.children.length > 0) { 271 | gherkinDocument.feature.children.forEach(function (child) { 272 | var scenarioTags = []; 273 | if (child.type !== 'Background') { 274 | scenarioTags = self._collectTags(child.tags); 275 | } 276 | if (child.examples === undefined || child.examples.length === 0) { 277 | featureFileObjects.push({ 278 | path: featureFilePath + ':' + child.location.line, 279 | tags: featureTags.concat(scenarioTags) 280 | }); 281 | } 282 | else { 283 | child.examples.forEach(function (example) { 284 | var exampleTags = self._collectTags(example.tags); 285 | example.tableBody.forEach(function (tableRow) { 286 | featureFileObjects.push({ 287 | path: featureFilePath + ':' + tableRow.location.line, 288 | tags: featureTags.concat(scenarioTags).concat(exampleTags) 289 | }); 290 | }); 291 | }); 292 | } 293 | }); 294 | } 295 | 296 | return featureFileObjects; 297 | }; 298 | 299 | self._collectTags = function (tagsArray) { 300 | var tags = []; 301 | tagsArray.forEach(function (tag) { 302 | tags.push(tag.name); 303 | }); 304 | return tags; 305 | }; 306 | 307 | return self; 308 | }; 309 | 310 | Runtime.FeatureFinder = require('./runtime/feature_finder'); 311 | Runtime.SupportCodeFinder = require('./runtime/support_code_finder'); 312 | Runtime.WorkerPool = require('./runtime/worker_pool'); 313 | 314 | module.exports = Runtime; 315 | -------------------------------------------------------------------------------- /lib/parallel_cucumber/runtime/feature_finder.js: -------------------------------------------------------------------------------- 1 | var FeatureFinder = function() { 2 | var FS = require('fs'); 3 | var Glob = require('glob'); 4 | var Path = require('path'); 5 | var Async = require('async'); 6 | var Debug = require('debug')('parallel-cucumber-js'); 7 | 8 | var self = {}; 9 | 10 | self.find = function(options, callback) { 11 | Async.waterfall( 12 | [ 13 | Async.apply(self._getGlobPatterns, { featurePaths: options.featurePaths, dryRun: options.dryRun }), 14 | self._ensureForwardSlashes, 15 | self._combineGlobPatterns, 16 | self._findFeatureFilePaths, 17 | self._getFeatureFileSizes, 18 | self._sortFeatureFilePathsInDescendingSizeOrder 19 | ], 20 | function (err, featureFilePaths) { 21 | if (err) { 22 | callback({ message: 'Failed to find the features', innerError: err }); 23 | } 24 | else { 25 | callback(null, featureFilePaths); 26 | } 27 | } 28 | ); 29 | }; 30 | 31 | self._getGlobPatterns = function(options, callback) { 32 | Async.map( 33 | options.featurePaths, 34 | function(featurePath, callback) { 35 | FS.stat(featurePath, function(err, stats) { 36 | if (err) { 37 | callback(err); 38 | } 39 | else { 40 | if (stats.isDirectory()) { 41 | if (options.dryRun) { 42 | callback(null, featurePath); 43 | } 44 | else { 45 | callback(null, Path.join(featurePath, '**/*.feature')); 46 | } 47 | } 48 | else { 49 | callback(null, featurePath); 50 | } 51 | } 52 | }); 53 | }, 54 | function(err, globPatterns) { 55 | if (err) { 56 | callback(err); 57 | } 58 | else { 59 | callback(null, { globPatterns: globPatterns }); 60 | } 61 | } 62 | ); 63 | }; 64 | 65 | self._ensureForwardSlashes = function(options, callback) { 66 | var globPatterns = options.globPatterns.map(function(globPattern) { 67 | return globPattern.replace(/\\/g, '/'); 68 | }); 69 | 70 | callback(null, { globPatterns: globPatterns }); 71 | }; 72 | 73 | self._combineGlobPatterns = function(options, callback) { 74 | if (options.globPatterns.length > 1) { 75 | callback(null, { globPattern: '{' + options.globPatterns.join(',') + '}' }); 76 | } 77 | else { 78 | callback(null, { globPattern: options.globPatterns[0] }); 79 | } 80 | }; 81 | 82 | self._findFeatureFilePaths = function(options, callback) { 83 | Debug('Glob:', options.globPattern); 84 | Glob(options.globPattern, { strict: true }, function(err, featureFilePaths) { 85 | if (err) { 86 | callback(err); 87 | } 88 | else { 89 | Debug('Found feature paths:', featureFilePaths); 90 | callback(null, { featureFilePaths: featureFilePaths }); 91 | } 92 | }); 93 | }; 94 | 95 | self._getFeatureFileSizes = function(options, callback) { 96 | Async.map( 97 | options.featureFilePaths, 98 | function(featureFilePath, callback) { 99 | FS.stat(featureFilePath, function(err, stats) { 100 | if (err) { 101 | callback(err); 102 | } 103 | else { 104 | if (stats.isDirectory()) { 105 | callback(null, { path: featureFilePath, size: Number.MAX_VALUE }); 106 | } 107 | else { 108 | callback(null, { path: featureFilePath, size: stats.size }); 109 | } 110 | } 111 | }); 112 | }, 113 | function(err, featureFilePaths) { 114 | if (err) { 115 | callback(err); 116 | } 117 | else { 118 | callback(null, { featureFilePaths: featureFilePaths }); 119 | } 120 | } 121 | ); 122 | }; 123 | 124 | self._sortFeatureFilePathsInDescendingSizeOrder = function(options, callback) { 125 | options.featureFilePaths.sort(function(a, b) { 126 | return b.size - a.size; 127 | }); 128 | 129 | Async.map( 130 | options.featureFilePaths, 131 | function(featureFilePath, callback) { 132 | callback(null, featureFilePath.path); 133 | }, 134 | callback 135 | ); 136 | }; 137 | 138 | return self; 139 | }; 140 | 141 | module.exports = FeatureFinder; 142 | -------------------------------------------------------------------------------- /lib/parallel_cucumber/runtime/support_code_finder.js: -------------------------------------------------------------------------------- 1 | var SupportCodeFinder = function() { 2 | var self = {}; 3 | 4 | self.find = function(options, callback) { 5 | var supportCodePaths = options.supportCodePaths; 6 | 7 | if (supportCodePaths.length > 0) { 8 | // Clone the array using slice() 9 | supportCodePaths = supportCodePaths.slice(); 10 | 11 | callback(null, supportCodePaths); 12 | return; 13 | } 14 | 15 | supportCodePaths = []; 16 | 17 | options.featurePaths.forEach(function (featurePath) { 18 | supportCodePaths.push(featurePath.replace(/[\/\\][^\/\\]+\.feature$/i, '')); 19 | }); 20 | 21 | callback(null, supportCodePaths); 22 | }; 23 | 24 | return self; 25 | }; 26 | 27 | module.exports = SupportCodeFinder; 28 | -------------------------------------------------------------------------------- /lib/parallel_cucumber/runtime/worker_pool.js: -------------------------------------------------------------------------------- 1 | var WorkerPool = function(options) { 2 | var Path = require('path'); 3 | var Events = require('events'); 4 | var ChildProcess = require('child_process'); 5 | var Async = require('async'); 6 | var Debug = require('debug')('parallel-cucumber-js'); 7 | 8 | var self = new Events.EventEmitter(); 9 | 10 | self.options = options; 11 | 12 | self.activeWorkerCount = 0; 13 | self.workers = []; 14 | 15 | if (self.options.debugBrk) { 16 | Debug('Enabling node debug mode, breaking on first line'); 17 | self.debugArgName = '--debug-brk'; 18 | self.firstDebugPort = self.options.debugBrk; 19 | } 20 | else if (self.options.debug) { 21 | Debug('Enabling node debug mode'); 22 | self.debugArgName = '--debug'; 23 | self.firstDebugPort = self.options.debug; 24 | } 25 | 26 | if (self.debugArgName) { 27 | self.debug = true; 28 | 29 | if (typeof self.firstDebugPort !== 'number') { 30 | self.firstDebugPort = 5858; 31 | } 32 | 33 | Debug('Debug ports starting from ' + self.firstDebugPort); 34 | } 35 | else { 36 | self.debug = false; 37 | } 38 | 39 | self.start = function(callback) { 40 | Debug('Started worker pool'); 41 | 42 | function nextTask(workerIndex) { 43 | var done = false; 44 | 45 | self.emit('next', function(task) { 46 | if (done) { return; } 47 | done = true; 48 | 49 | var worker = self.workers[workerIndex]; 50 | 51 | if (task) { 52 | if (!worker) { 53 | Debug('Creating worker', workerIndex); 54 | 55 | // Clone the array using slice() 56 | var execArgv = process.execArgv.slice(); 57 | 58 | if (self.debug) { 59 | var debugPort = self.firstDebugPort + workerIndex; 60 | Debug('Worker debug port: ' + debugPort); 61 | 62 | execArgv.push(self.debugArgName + '=' + debugPort); 63 | } 64 | 65 | worker = ChildProcess.fork(Path.join(__dirname, '../../../bin/parallel-cucumber-js-worker'), [], { execArgv: execArgv }); 66 | 67 | self.activeWorkerCount++; 68 | self.workers[workerIndex] = worker; 69 | 70 | worker.on('message', function(message) { 71 | Debug('Received message: ', message); 72 | if (message.cmd === 'start') { 73 | worker.send({ cmd: 'init', workerIndex: workerIndex, cucumberPath: self.options.cucumberPath, dryRun: self.options.dryRun }); 74 | worker.send({ cmd: 'task', task: task }); 75 | } 76 | else if (message.cmd === 'report') { 77 | Debug('Received "report" message'); 78 | self.emit('report', { workerIndex: message.workerIndex, task: message.task, report: message.report, success: message.success }); 79 | } 80 | else if (message.cmd === 'next') { 81 | Debug('Received "next" message'); 82 | nextTask(message.workerIndex); 83 | } 84 | else if (message.cmd === 'error') { 85 | Debug('Child error:', message); 86 | self.emit('error', message); 87 | } 88 | }); 89 | 90 | worker.on('error', function (err) { 91 | Debug('Child error:', err); 92 | self.emit('error', err); 93 | }); 94 | 95 | worker.on('exit', function (code, signal) { 96 | Debug('Child exited:', code, signal); 97 | }); 98 | } 99 | else { 100 | worker.send({ cmd: 'task', task: task }); 101 | } 102 | } 103 | else { 104 | if (worker) { 105 | Debug('Worker exiting', workerIndex); 106 | self.activeWorkerCount--; 107 | self.workers[workerIndex] = null; 108 | worker.send({ cmd: 'exit' }); 109 | 110 | Debug('Worker count:', self.activeWorkerCount); 111 | 112 | if (self.activeWorkerCount === 0) { 113 | Debug('Last worker exited'); 114 | self.emit('done'); 115 | } 116 | } 117 | } 118 | }); 119 | } 120 | 121 | Async.times( 122 | self.options.workerCount, 123 | function(workerIndex, next) { 124 | nextTask(workerIndex); 125 | 126 | next(); 127 | }, 128 | function(err) { 129 | if (callback) { 130 | callback(err); 131 | } 132 | } 133 | ); 134 | }; 135 | 136 | return self; 137 | }; 138 | 139 | module.exports = WorkerPool; -------------------------------------------------------------------------------- /lib/parallel_cucumber/workers.js: -------------------------------------------------------------------------------- 1 | var Workers = {}; 2 | 3 | Workers.Main = require('./workers/main'); 4 | 5 | module.exports = Workers; 6 | -------------------------------------------------------------------------------- /lib/parallel_cucumber_worker.js: -------------------------------------------------------------------------------- 1 | var ParallelCucumberWorker = {}; 2 | 3 | ParallelCucumberWorker.Cli = require('./parallel_cucumber_worker/cli'); 4 | 5 | module.exports = ParallelCucumberWorker; 6 | -------------------------------------------------------------------------------- /lib/parallel_cucumber_worker/cli.js: -------------------------------------------------------------------------------- 1 | var Cli = {}; 2 | 3 | Cli.Main = require('./cli/main'); 4 | 5 | module.exports = Cli; 6 | -------------------------------------------------------------------------------- /lib/parallel_cucumber_worker/cli/main.js: -------------------------------------------------------------------------------- 1 | var Main = function() { 2 | var Path = require('path'), 3 | Debug = require('debug')('parallel-cucumber-js'), 4 | FeaturesRunner = require('../../cucumber/runtime/features_runner.js'), 5 | EventBroadcaster = require('../../cucumber/runtime/event_broadcaster.js'), 6 | self = {}; 7 | 8 | Debug('Process current working directory:', process.cwd()); 9 | 10 | process.on('message', function(message) { 11 | if (message.cmd === 'init') { 12 | self._init(message); 13 | Debug('Worker ' + self.workerIndex + ' received "init" message'); 14 | } 15 | else if (message.cmd === 'task') { 16 | Debug('Worker ' + self.workerIndex + ' received "task" message'); 17 | self._task(message); 18 | } 19 | else if (message.cmd === 'exit') { 20 | Debug('Worker ' + self.workerIndex + ' received "exit" message'); 21 | self._exit(); 22 | } 23 | }); 24 | 25 | self.start = function() { 26 | process.send({ cmd: 'start' }); 27 | }; 28 | 29 | self._init = function(options) { 30 | self.workerIndex = options.workerIndex; 31 | self.dryRun = options.dryRun; 32 | self.beforeFeatures = true; 33 | 34 | var cucumberPath = options.cucumberPath; 35 | 36 | if (!self._pathIsAModule(cucumberPath)) { 37 | cucumberPath = Path.resolve(cucumberPath); 38 | } 39 | 40 | self.Cucumber = require(cucumberPath); 41 | 42 | 43 | self.Cucumber.Runtime.EventBroadcaster = function(listeners, supportCodeLibrary) { 44 | var eventBroadCasterInstance = EventBroadcaster(listeners, supportCodeLibrary); 45 | self.eventBroadcaster = eventBroadCasterInstance; 46 | return eventBroadCasterInstance; 47 | }; 48 | 49 | self.Cucumber.Runtime.FeaturesRunner = FeaturesRunner; 50 | 51 | }; 52 | 53 | self._task = function(options) { 54 | self.task = options.task; 55 | self.output = []; 56 | Debug('Task:', self.task); 57 | 58 | // Set cucumber defaults 59 | var opts = { 60 | require: [], 61 | tags: [], 62 | formats: ['pretty'], 63 | compiler: [], 64 | profile: [], 65 | name: [], 66 | }; 67 | var featurePaths = []; 68 | 69 | self.task.supportCodePaths.forEach(function(supportCodePath) { 70 | opts.require.push(supportCodePath); 71 | }); 72 | 73 | self.task.tags.forEach(function(tagExpression) { 74 | opts.tags.push(tagExpression); 75 | }); 76 | 77 | if (self.task.formats) { 78 | self.task.formats.forEach(function(formatExpression) { 79 | opts.formats.push(formatExpression); 80 | }); 81 | } 82 | 83 | featurePaths.push(self.task.featureFilePath); 84 | Debug('Cucumber options:', opts); 85 | Debug('Cucumber feature paths:', featurePaths); 86 | 87 | Debug('Setting the PARALLEL_CUCUMBER_PROFILE environment variable to \'' + self.task.profileName + '\''); 88 | process.env.PARALLEL_CUCUMBER_PROFILE = self.task.profileName; 89 | process.env.PARALLEL_CUCUMBER_DRY_RUN = self.dryRun; 90 | process.env.PARALLEL_CUCUMBER_RETRY = self.task.retryCount.toString(); 91 | var originalEnv = {}; 92 | 93 | Object.keys(self.task.env).forEach(function(envName) { 94 | originalEnv[envName] = process.env[envName]; 95 | process.env[envName] = self.task.env[envName]; 96 | }); 97 | 98 | var configuration = self.Cucumber.Cli.Configuration(opts, featurePaths); 99 | var runtime = self.Cucumber.Runtime(configuration); 100 | var formatter = self.Cucumber.Listener.JsonFormatter( 101 | { 102 | logToConsole: false, 103 | logToFunction: self._writeReport 104 | } 105 | ); 106 | runtime.attachListener(formatter); 107 | runtime.start(function(success) { 108 | Object.keys(originalEnv).forEach(function(envName) { 109 | if (typeof originalEnv[envName] === 'undefined') { 110 | delete process.env[envName]; 111 | } 112 | else { 113 | process.env[envName] = originalEnv[envName]; 114 | } 115 | }); 116 | 117 | 118 | self._taskDone(success); 119 | }); 120 | }; 121 | 122 | self._writeReport = function (value) { 123 | Debug(self); 124 | self.output.push(value); 125 | }; 126 | 127 | self._taskDone = function(success) { 128 | Debug('Success:', success); 129 | 130 | var report = self.output.join(''); 131 | 132 | try { 133 | report = JSON.parse(report); 134 | } 135 | catch (e) { 136 | if (!(e instanceof SyntaxError)) { 137 | throw e; 138 | } 139 | 140 | process.send({ cmd: 'error', workerIndex: self.workerIndex, profileName: self.task.profileName, message: 'Syntax error in the JSON report: ' + e }); 141 | Debug('Sent "error" message'); 142 | return; 143 | } 144 | 145 | process.send({ cmd: 'report', workerIndex: self.workerIndex, task: self.task, report: report, success: success }); 146 | Debug('Sent "report" message'); 147 | process.send({ cmd: 'next', workerIndex: self.workerIndex }); 148 | Debug('Sent "next" message'); 149 | }; 150 | 151 | self._exit = function() { 152 | function exit() { 153 | process.disconnect(); 154 | process.exit(0); 155 | } 156 | 157 | if (self.eventBroadcaster) { 158 | self.eventBroadcaster.broadcastAfterFeaturesEvent(function() { 159 | exit(); 160 | }); 161 | } 162 | else { 163 | exit(); 164 | } 165 | }; 166 | 167 | self._pathIsAModule = function(path) { 168 | return path === Path.basename(path); 169 | }; 170 | 171 | return self; 172 | }; 173 | 174 | module.exports = Main; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parallel-cucumber", 3 | "description": "Executes Cucumber scenarios in parrallel, reducing the amount of time tests take to execute", 4 | "keywords": [ 5 | "cucumber", 6 | "cucumber-js", 7 | "grid", 8 | "parallel", 9 | "scale" 10 | ], 11 | "version": "1.1.0", 12 | "homepage": "http://github.com/simondean/parallel-cucumber-js", 13 | "author": { 14 | "name" : "Simon Dean", 15 | "email" : "simon@simondean.org", 16 | "url" : "http://www.simondean.org" 17 | }, 18 | "contributors": [], 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/simondean/parallel-cucumber-js.git" 22 | }, 23 | "bugs": { 24 | "url": "http://github.com/simondean/parallel-cucumber-js/issues" 25 | }, 26 | "directories": { 27 | "lib": "./lib" 28 | }, 29 | "main": "./lib/parallel-cucumber", 30 | "engines": { 31 | "node": ">=0.8.19" 32 | }, 33 | "dependencies": { 34 | "yargs": "~1.2.6", 35 | "async": "~0.9.0", 36 | "glob": "~4.0.2", 37 | "debug": "~1.0.2", 38 | "js-yaml": "~3.0.2", 39 | "colors": "~0.6.2", 40 | "gherkin": "~4.0.0" 41 | }, 42 | "peerDependencies": { 43 | "cucumber": "~1.3.3" 44 | }, 45 | "devDependencies": { 46 | "grunt-cli": "~0.1.13", 47 | "grunt": "~0.4.4", 48 | "grunt-contrib-jshint": "~0.6.0", 49 | "grunt-contrib-clean": "~0.4.1", 50 | "grunt-shell": "~0.7.0", 51 | "deep-diff": "~0.3.4", 52 | "diff": "~1.0.8", 53 | "stripcolorcodes": "~0.1.0" 54 | }, 55 | "bin": { 56 | "parallel-cucumber-js": "./bin/parallel-cucumber-js" 57 | }, 58 | "scripts": { 59 | "test": "grunt test" 60 | }, 61 | "licenses": [ 62 | { 63 | "type": "MIT", 64 | "url": "http://github.com/simondean/parallel-cucumber/LICENSE" 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /test_assets/features/background.feature: -------------------------------------------------------------------------------- 1 | Feature: Background 2 | 3 | Background: 4 | Given a passing pre-condition 5 | 6 | Scenario: Passing 7 | When a passing action is executed 8 | Then a post-condition passes 9 | -------------------------------------------------------------------------------- /test_assets/features/empty.feature: -------------------------------------------------------------------------------- 1 | Feature: Empty -------------------------------------------------------------------------------- /test_assets/features/environment_variables.feature: -------------------------------------------------------------------------------- 1 | Feature: Environment variables 2 | 3 | @no-environment-variable 4 | Scenario: Environment variable has not been set 5 | Then the environment variable 'EXAMPLE_NAME' is not set 6 | 7 | @environment-variable 8 | Scenario: Environment variable has been set 9 | Then the environment variable 'EXAMPLE_NAME' equals 'example_value' 10 | 11 | @old-environment-variable 12 | Scenario: Environment variable has old value 13 | Then the environment variable 'EXAMPLE_NAME' equals 'old_example_value' 14 | 15 | -------------------------------------------------------------------------------- /test_assets/features/failing.feature: -------------------------------------------------------------------------------- 1 | Feature: Failing 2 | 3 | Scenario: Failing 4 | Given a passing pre-condition 5 | When a failing action is executed 6 | Then a post-condition passes -------------------------------------------------------------------------------- /test_assets/features/parallel/blue.feature: -------------------------------------------------------------------------------- 1 | Feature: Blue 2 | 3 | @blue 4 | Scenario: Blue 5 | When a passing action is executed 6 | -------------------------------------------------------------------------------- /test_assets/features/parallel/purple.feature: -------------------------------------------------------------------------------- 1 | Feature: Purple 2 | 3 | @blue @red 4 | Scenario: Purple 5 | When a passing action is executed 6 | -------------------------------------------------------------------------------- /test_assets/features/parallel/red.feature: -------------------------------------------------------------------------------- 1 | Feature: Red 2 | 3 | @red 4 | Scenario: Red 5 | When a passing action is executed 6 | -------------------------------------------------------------------------------- /test_assets/features/passing.feature: -------------------------------------------------------------------------------- 1 | Feature: Passing 2 | 3 | Scenario: Passing 4 | Given a passing pre-condition 5 | When a passing action is executed 6 | Then a post-condition passes -------------------------------------------------------------------------------- /test_assets/features/pending.feature: -------------------------------------------------------------------------------- 1 | Feature: Pending 2 | 3 | Scenario: Pending 4 | When a pending action is executed 5 | -------------------------------------------------------------------------------- /test_assets/features/profile_environment_variable.feature: -------------------------------------------------------------------------------- 1 | Feature: Profile environment variable 2 | 3 | Scenario: Profile environment variable 4 | Then the environment variable 'PARALLEL_CUCUMBER_PROFILE' equals 'test_profile' 5 | -------------------------------------------------------------------------------- /test_assets/features/retries.feature: -------------------------------------------------------------------------------- 1 | Feature: Retries 2 | 3 | Scenario: Retries 4 | When an action is executed that passes on retry '2' -------------------------------------------------------------------------------- /test_assets/features/scenario_outline.feature: -------------------------------------------------------------------------------- 1 | Feature: Scenario outline 2 | 3 | Scenario Outline: Scenario outline 4 | When an action is executed that passes on retry '' 5 | 6 | Examples: 7 | | retry | 8 | | 0 | 9 | | 1 | 10 | -------------------------------------------------------------------------------- /test_assets/features/step_definitions/environment_variables.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.Then(/^the environment variable '(.*)' equals '(.*)'$/, function(name, expectedValue, callback) { 3 | if (this.isDryRun()) { return callback(); } 4 | 5 | var actualValue = process.env[name]; 6 | 7 | if (typeof actualValue === 'undefined') { 8 | callback('Expected environment variable ' + name + ' to exist but it did not exist'); 9 | } 10 | if (actualValue !== expectedValue) { 11 | callback('Actual value \'' + actualValue + '\' did not match expected value \'' + expectedValue + '\''); 12 | } 13 | else { 14 | callback(); 15 | } 16 | }); 17 | 18 | this.Then(/^the environment variable '(.*)' is not set$/, function(name, callback) { 19 | if (this.isDryRun()) { return callback(); } 20 | 21 | var actualValue = process.env[name]; 22 | 23 | if (typeof actualValue !== 'undefined') { 24 | callback('Expected environment variable to not be set but was set to \'' + actualValue + '\''); 25 | } 26 | else { 27 | callback(); 28 | } 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /test_assets/features/step_definitions/failing.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.When(/^a failing action is executed$/, function(callback) { 3 | if (this.isDryRun()) { return callback(); } 4 | 5 | callback('Failed'); 6 | }); 7 | }; 8 | 9 | 10 | -------------------------------------------------------------------------------- /test_assets/features/step_definitions/passing.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.Given(/^a passing pre-condition$/, function(callback) { 3 | if (this.isDryRun()) { return callback(); } 4 | 5 | callback(); 6 | }); 7 | 8 | this.When(/^a passing action is executed$/, function(callback) { 9 | if (this.isDryRun()) { return callback(); } 10 | 11 | callback(); 12 | }); 13 | 14 | this.Then(/^a post-condition passes$/, function(callback) { 15 | if (this.isDryRun()) { return callback(); } 16 | 17 | callback(); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /test_assets/features/step_definitions/pending.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.When(/^a pending action is executed$/, function(callback) { 3 | if (this.isDryRun()) { return callback(); } 4 | 5 | callback(null, 'pending'); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /test_assets/features/step_definitions/retries.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.When(/^an action is executed that passes on retry '(.*)'$/, function(retryCount, callback) { 3 | if (this.isDryRun()) { return callback(); } 4 | 5 | var currentRetryCount = parseInt(process.env.PARALLEL_CUCUMBER_RETRY); 6 | 7 | if (currentRetryCount < retryCount) { 8 | callback('Failed on retry ' + currentRetryCount); 9 | } 10 | else { 11 | callback(); 12 | } 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /test_assets/features/support/hooks.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.BeforeFeatures(function(event, callback) { 3 | if (shouldLog()) { console.log('Before features'); } 4 | callback(); 5 | }); 6 | 7 | this.BeforeFeature(function(event, callback) { 8 | if (shouldLog()) { console.log('Before feature'); } 9 | callback(); 10 | }); 11 | 12 | this.BeforeScenario(function(event, callback) { 13 | if (shouldLog()) { console.log('Before scenario'); } 14 | callback(); 15 | }); 16 | 17 | this.BeforeStep(function(event, callback) { 18 | if (shouldLog()) { console.log('Before step'); } 19 | callback(); 20 | }); 21 | 22 | this.StepResult(function(event, callback) { 23 | if (shouldLog()) { console.log('Step result'); } 24 | callback(); 25 | }); 26 | 27 | this.AfterStep(function(event, callback) { 28 | if (shouldLog()) { console.log('After step'); } 29 | callback(); 30 | }); 31 | 32 | this.AfterScenario(function(event, callback) { 33 | if (shouldLog()) { console.log('After scenario'); } 34 | callback(); 35 | }); 36 | 37 | this.AfterFeature(function(event, callback) { 38 | if (shouldLog()) { console.log('After feature'); } 39 | callback(); 40 | }); 41 | 42 | this.AfterFeatures(function(event, callback) { 43 | if (shouldLog()) { console.log('After features'); } 44 | callback(); 45 | }); 46 | 47 | function shouldLog() { 48 | return process.env.LOG_CUCUMBER_EVENTS === 'true'; 49 | } 50 | }; -------------------------------------------------------------------------------- /test_assets/features/support/world.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.World = function() { 3 | var world = this; 4 | 5 | world.isDryRun = function() { 6 | return process.argv.indexOf('--dry-run') !== -1 || process.env.PARALLEL_CUCUMBER_DRY_RUN === 'true'; 7 | }; 8 | 9 | }; 10 | }; -------------------------------------------------------------------------------- /test_assets/features/tags.feature: -------------------------------------------------------------------------------- 1 | Feature: Tags 2 | 3 | @tag-1 4 | Scenario: Tagged 5 | When a passing action is executed 6 | 7 | @tag-2 @tag-3 8 | Scenario: Tagged twice 9 | When a passing action is executed 10 | -------------------------------------------------------------------------------- /test_assets/features/undefined.feature: -------------------------------------------------------------------------------- 1 | Feature: Undefined 2 | 3 | Scenario: Undefined 4 | When an undefined action is executed 5 | -------------------------------------------------------------------------------- /test_assets/lib/custom_cucumber.js: -------------------------------------------------------------------------------- 1 | var Cucumber = require('cucumber'); 2 | 3 | var OriginalJsonFormatter = Cucumber.Listener.JsonFormatter; 4 | 5 | Cucumber.Listener.JsonFormatter = function(options) { 6 | var self = OriginalJsonFormatter(options); 7 | 8 | var originalLog = self.log; 9 | var logOutput = ''; 10 | 11 | self.log = function(string) { 12 | logOutput += string; 13 | var report; 14 | 15 | try { 16 | report = JSON.parse(logOutput); 17 | } 18 | catch (e) { 19 | if (!(e instanceof SyntaxError)) { 20 | throw e; 21 | } 22 | 23 | return; 24 | } 25 | 26 | logOutput = ''; 27 | 28 | report.forEach(function(item) { 29 | item['custom_cucumber'] = true; 30 | item['elements'] = []; 31 | }); 32 | 33 | originalLog(JSON.stringify(report)); 34 | }; 35 | 36 | return self; 37 | }; 38 | 39 | module.exports = Cucumber; -------------------------------------------------------------------------------- /test_assets/lib/formatters/custom_formatter.js: -------------------------------------------------------------------------------- 1 | var CustomFormatter = function(options) { 2 | var ParallelCucumber = require('../../../lib/parallel_cucumber'); 3 | var OS = require('os'); 4 | 5 | var self = ParallelCucumber.Formatters.Formatter(options); 6 | 7 | self._firstFeature = true; 8 | var superEnd = self.end; 9 | 10 | self._write('Start' + OS.EOL); 11 | 12 | self.formatFeature = function(options) { 13 | self._write('Feature ' + options.feature.id + OS.EOL); 14 | }; 15 | 16 | self.end = function(callback) { 17 | self._write('End' + OS.EOL); 18 | 19 | superEnd(callback); 20 | }; 21 | 22 | return self; 23 | }; 24 | 25 | module.exports = CustomFormatter; -------------------------------------------------------------------------------- /test_assets/lib/formatters/null_formatter.js: -------------------------------------------------------------------------------- 1 | var NullFormatter = function(options) { 2 | var ParallelCucumber = require('../../../lib/parallel_cucumber'); 3 | 4 | var self = ParallelCucumber.Formatters.Formatter(options); 5 | 6 | self.formatFeature = function() { 7 | // no-op 8 | }; 9 | 10 | return self; 11 | }; 12 | 13 | module.exports = NullFormatter; --------------------------------------------------------------------------------