├── .editorconfig ├── .eslintignore ├── .gitignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── package-lock.json ├── package.json ├── src ├── .eslintrc.js ├── bin │ ├── cli.ts │ └── options │ │ ├── async-only.ts │ │ ├── bail.ts │ │ ├── check-leaks.ts │ │ ├── compilers.ts │ │ ├── delay.ts │ │ ├── exit.ts │ │ ├── file.ts │ │ ├── forbid-only.ts │ │ ├── forbid-pending.ts │ │ ├── full-trace.ts │ │ ├── grep.ts │ │ ├── max-parallel.ts │ │ ├── no-timeouts.ts │ │ ├── reporter-options.ts │ │ ├── reporter.ts │ │ ├── require.ts │ │ ├── rest.ts │ │ ├── retries.ts │ │ ├── slow.ts │ │ └── timeout.ts ├── config.ts ├── main │ ├── mocha.ts │ ├── runner.ts │ ├── task-manager.ts │ ├── thread.ts │ ├── thread │ │ ├── process.ts │ │ └── worker.ts │ └── util.ts ├── message-channel.ts ├── mocha.ts ├── subprocess │ ├── cli.ts │ ├── message-channel.ts │ ├── options │ │ ├── exit.ts │ │ └── full-trace.ts │ ├── reporter.ts │ ├── runner.ts │ ├── thread │ │ ├── worker.development.js │ │ └── worker.ts │ ├── util.ts │ └── worker.ts ├── thread.ts └── util.ts ├── test ├── .eslintrc.js ├── bail-and-retries │ ├── README │ ├── index.js │ └── index.spec.js ├── cli-target │ ├── README │ └── index.sh ├── console-log-inject │ ├── README │ ├── index.js │ └── tests │ │ ├── foo.js │ │ ├── parallel.js │ │ └── parallel2.js ├── delay │ ├── README │ ├── index.js │ ├── index.spec.js │ └── reporter.js ├── describe-inside-describe │ ├── README │ ├── index.js │ └── tests │ │ ├── parallel1.js │ │ └── parallel2.js ├── exit │ ├── README │ ├── index.js │ └── index.spec.js ├── file │ ├── README │ ├── config.js │ ├── index.js │ └── index.spec.js ├── full-trace │ ├── README │ ├── index.js │ └── index.spec.js ├── global-hooks-directory │ ├── README │ ├── index.sh │ └── tests │ │ ├── global-hooks.js │ │ └── test.js ├── global-hooks-require │ ├── README │ ├── global-hooks.js │ ├── index.sh │ └── test.js ├── global-hooks │ ├── README │ ├── index.sh │ └── test.js ├── grep │ ├── README │ ├── index-alias.js │ ├── index.js │ ├── indexProgrammatic.js │ └── tests │ │ ├── test.js │ │ └── test1.js ├── index.sh ├── js-compilers-1 │ ├── .eslintrc │ ├── README │ ├── babel-register.js │ ├── index.sh │ └── test.js ├── js-compilers-2 │ ├── .eslintrc │ ├── README │ ├── babel-register.js │ ├── index.sh │ ├── setup.js │ └── test.js ├── js-compilers │ ├── README │ ├── index.sh │ └── test.js ├── max-parallel-1 │ ├── README │ ├── index.sh │ └── tests │ │ ├── parallel1.js │ │ ├── parallel2.js │ │ └── parallel3.js ├── max-parallel-empty │ ├── README │ ├── index.sh │ └── tests │ │ ├── parallel-empty1.js │ │ ├── parallel-empty2.js │ │ ├── parallel-empty3.js │ │ └── parallel.js ├── max-parallel │ ├── README │ ├── index.sh │ └── tests │ │ ├── parallel1.js │ │ ├── parallel2.js │ │ ├── parallel3.js │ │ ├── parallel4.js │ │ └── parallel5.js ├── missing-test │ ├── README │ └── index.js ├── mocha-opts │ ├── README │ ├── index.sh │ ├── mocha.opts │ └── tests │ │ ├── parallel.js │ │ ├── parallel2.js │ │ └── parallel3.js ├── nesting │ ├── nesting.js │ └── nesting.sh ├── no-subprocess-end │ ├── README │ ├── index.js │ └── spec │ │ ├── one-crash-early-one-ok │ │ ├── crash.spec.js │ │ └── ok.spec.js │ │ ├── one-crash-early │ │ └── crash.spec.js │ │ ├── one-crash-one-ok │ │ ├── crash.spec.js │ │ └── ok.spec.js │ │ └── one-crash │ │ └── crash.spec.js ├── no-timeouts │ ├── README │ ├── index.sh │ └── tests │ │ ├── 1.js │ │ └── 2.js ├── node-native-addon │ ├── README │ ├── index.sh │ └── spec.js ├── only-tests-run │ ├── README │ ├── index.js │ └── tests │ │ ├── common.js │ │ ├── parallel1.js │ │ └── parallel2.js ├── parallel-order │ ├── README │ ├── index.js │ └── tests │ │ ├── parallel1.js │ │ ├── parallel2.js │ │ └── parallel3.js ├── parallel │ ├── parallel.sh │ └── tests │ │ ├── parallel.js │ │ ├── parallel2.js │ │ └── parallel3.js ├── q-promises │ ├── README │ ├── index.js │ └── index.spec.js ├── race-condition-timeout │ ├── README │ ├── index.js │ └── index.spec.js ├── recursive-no-target │ ├── README │ ├── index.js │ └── test │ │ ├── 1 │ │ └── suite.js │ │ └── 2 │ │ └── suite.js ├── reporter-custom-allure │ ├── .gitignore │ ├── README │ ├── index.sh │ └── suite.js ├── reporter-custom-jenkins │ ├── README │ ├── index.sh │ └── suite.js ├── reporter-custom-mochawesome │ ├── README │ ├── index.sh │ └── suite.js ├── reporter-custom-teamcity │ ├── README │ ├── index.sh │ └── suite.js ├── reporter-end-no-exit │ ├── README │ ├── index.js │ └── tests │ │ ├── 1.js │ │ ├── 2.js │ │ ├── 3.js │ │ └── reporter │ │ └── spec-file.js ├── reporter-log │ ├── README │ ├── index.js │ └── tests │ │ ├── log1.js │ │ └── log2.js ├── reporter-native-json │ ├── README │ ├── index.js │ ├── index.sh │ └── suite.js ├── reporter-native-tap │ ├── README │ ├── index.sh │ └── suite.js ├── reporter-options │ ├── README │ ├── cli-once │ │ └── index.js │ ├── foundation │ │ └── index.sh │ └── spec.js ├── reporter-pwd │ ├── README │ ├── index.sh │ ├── reporter-pwd.js │ └── suite.js ├── reporter-same-describes │ ├── index.js │ └── tests │ │ ├── first.js │ │ └── second.js ├── require-option │ ├── README │ ├── index.sh │ ├── require-module.js │ └── test.js ├── retries-all-fail-2 │ ├── README │ ├── index.js │ └── index.spec.js ├── retries-all-fail-3 │ ├── README │ ├── index.js │ └── index.spec.js ├── retries-all-fail │ ├── README │ ├── index.js │ └── index.spec.js ├── retries │ ├── README │ ├── index.js │ └── index.spec.js ├── run-programmatically │ ├── _spec │ │ ├── dummy.js │ │ ├── parallel1.js │ │ └── parallel2.js │ ├── callback │ │ ├── README │ │ └── index.js │ ├── reporter-done │ │ ├── README │ │ └── index.js │ └── tty-worker │ │ ├── README │ │ └── index.js ├── skip-suite │ ├── README │ ├── index.sh │ └── test.js ├── skip-test │ ├── README │ ├── index.sh │ └── test.js ├── syntax-errors │ ├── README │ ├── index.js │ └── tests │ │ ├── index.js │ │ └── server.js ├── timeouts-exit-code │ ├── README │ ├── index.sh │ └── tests │ │ ├── 1.js │ │ └── 2.js ├── total-time │ ├── README │ ├── index.js │ └── tests │ │ ├── parallel1.js │ │ ├── parallel2.js │ │ └── parallel3.js ├── ui-option │ ├── README │ ├── index.sh │ └── test.js ├── uncaught-exception │ ├── README │ ├── index.js │ └── index.spec.js └── util │ ├── events-reporter.js │ └── silent-reporter.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | 9 | [{**/*.js,test/index.sh}] 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/reporter-custom-mochawesome/mochawesome-report 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | .*DS_Store* 4 | 5 | lib-cov 6 | *.seed 7 | *.log 8 | *.csv 9 | *.dat 10 | *.out 11 | *.pid 12 | *.gz 13 | 14 | pids 15 | logs 16 | results 17 | 18 | dist/ 19 | npm-debug.log 20 | node_modules 21 | .idea 22 | 23 | # app specific 24 | test/node-addon 25 | test/reporter-custom-mochawesome/mochawesome-report/ 26 | mochawesome-report/ 27 | test/reporter-custom-jenkins/result.xml 28 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 13 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "12" 6 | os: 7 | - linux 8 | - osx 9 | before_install: npm install -g greenkeeper-lockfile 10 | install: npm install 11 | before_script: greenkeeper-lockfile-update 12 | after_script: greenkeeper-lockfile-upload 13 | script: npm run test:ci 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Upcoming release 2 | 3 | * fix: add `--ui` option support 4 | 5 | ## 2.3.0 6 | 7 | * new: `mocha=7` is supported 8 | * fix: `--file` option is not supported ([#222](https://github.com/mocha-parallel/mocha-parallel-tests/issues/222)) 9 | * fix: security warnings 10 | * trivial: local development Node.JS version is updated to 13 11 | 12 | ## 2.2.2 13 | 14 | * fix: `mocha-parallel-tests` no longer reequire implicit `tslib` dependency ([#262](https://github.com/mocha-parallel/mocha-parallel-tests/issues/262)) 15 | 16 | ## 2.2.1 17 | 18 | * fix: TTY-related issue where users of Node.JS>=12 could see an error "TypeError: tty.getWindowSize is not a function" ([#247](https://github.com/mocha-parallel/mocha-parallel-tests/issues/247)) 19 | * chore: last tslint comments removed from the code, `mocha-parallel-tests` Typescript code and Javascript tests are now linted completely by `eslint` ([#242](https://github.com/mocha-parallel/mocha-parallel-tests/issues/242)) 20 | * chore: `mocha-parallel-tests` are now also executed in OSX via Travis CI 21 | 22 | ## 2.2.0 23 | 24 | * new: `mocha-parallel-tests` now forks light threads instead of fully fledged Node.JS processes in environments where [worker_threads](https://nodejs.org/api/worker_threads.html) API is supported. Usually it's Node.JS >= 12. This results in a faster tests processing and less memory consumption. 25 | 26 | ## 2.1.2 27 | 28 | * fix: programmatic API doesn't emit particular suites when multiple suites share the name ([#237](https://github.com/mocha-parallel/mocha-parallel-tests/issues/237)) 29 | 30 | ## 2.1.1 31 | 32 | * fix: `--reporter-options` doesn't work with mocha@6 ([#202](https://github.com/mocha-parallel/mocha-parallel-tests/issues/202)) 33 | * fix: npm install audio warnings because of the outdated dependencies 34 | 35 | ## 2.1.0 36 | 37 | * new: `mocha=6` is supported ([#217](https://github.com/mocha-parallel/mocha-parallel-tests/issues/217)) 38 | * fix: `--retries` throws an unhandled exception when test cases are defined in a loop ([#194](https://github.com/mocha-parallel/mocha-parallel-tests/issues/194)) 39 | * fix: `-g` alias for `--grep` is not supported ([#203](https://github.com/mocha-parallel/mocha-parallel-tests/issues/203)) 40 | * fix: `--full-trace` option is not supported ([#202](https://github.com/mocha-parallel/mocha-parallel-tests/issues/202)) 41 | * fix: `--exit` option leaves no trace for some asynchronously running tests ([#202](https://github.com/mocha-parallel/mocha-parallel-tests/issues/202)) 42 | * remove: `crypto` library usage 43 | * linting is all done by `eslint` now. 44 | 45 | ## 2.0.5 46 | 47 | * fix: `--retries` with hooks throws an unhandled exception ([#194](https://github.com/mocha-parallel/mocha-parallel-tests/issues/194)) 48 | 49 | ## 2.0.4 50 | 51 | * new: `--grep` option is now supported for both CLI and API ([#187](https://github.com/mocha-parallel/mocha-parallel-tests/pull/187), [#188](https://github.com/mocha-parallel/mocha-parallel-tests/pull/188)) 52 | 53 | ## 2.0.3 54 | 55 | * fix: subprocess processing should not stop when unhandled rejection occurs ([#173](https://github.com/mocha-parallel/mocha-parallel-tests/issues/173)) 56 | * fix: subprocess processing should not stop when uncaught exception occurs ([#172](https://github.com/mocha-parallel/mocha-parallel-tests/issues/172)) 57 | * added a [list of limitations](https://github.com/mocha-parallel/mocha-parallel-tests/wiki/Limitations) that you can hit when you launch your tests with `mocha-parallel-tests` 58 | 59 | ## 2.0.2 60 | 61 | * fix: subprocess stderr is not shown if the subprocess crashes before sending the "start" event ([#158](https://github.com/mocha-parallel/mocha-parallel-tests/issues/158)) 62 | * fix: `--retries` and `--bail` option conflict with each other causing `mohcha-parallel-tests` own assertion error ([#163](https://github.com/mocha-parallel/mocha-parallel-tests/issues/163)) 63 | 64 | ## 2.0.1 65 | 66 | * fix: reporter doesn't show anything until all tests are finished ([#145](https://github.com/mocha-parallel/mocha-parallel-tests/issues/145)) 67 | * fix: wrong assertion error when subprocess crashes before sending test results ([#147](https://github.com/mocha-parallel/mocha-parallel-tests/issues/147)) 68 | * fix: `--exit` option was not working as expected ([#146](https://github.com/mocha-parallel/mocha-parallel-tests/issues/146)) 69 | * fix: wrong assertion error when `--retries` option is used and all retries fail ([#143](https://github.com/mocha-parallel/mocha-parallel-tests/issues/143)) 70 | * fix: events order is not following `mocha` style. This fixes `mochawesome` reporter behaviour ([#113](https://github.com/mocha-parallel/mocha-parallel-tests/issues/113)) 71 | * `mocha-allure-reporter` is supported ([#80](https://github.com/mocha-parallel/mocha-parallel-tests/issues/80)) 72 | 73 | ## 2.0.0 74 | 75 | v2 is a completely new version of `mocha-parallel-tests` rewritten in TypeScript from scratch. Its main focus is to simplify parallel run of mocha tests: while previously they were executed in one single process **now each file is executed in a separate process**. 76 | 77 | Some of the main changes are: 78 | 79 | * **breaking change**: each file is now executed in a separate process 80 | * **breaking change**: minimum supported node version is 8 because it's current LTS and because of [performance reasons](https://blog.risingstack.com/important-features-fixes-node-js-version-8/) 81 | * **breaking change**: main exported file should now be imported as `require("mocha-parallel-tests").default` if you're using CommonJS modules 82 | * **breaking change**: `--retry` is not supported anymore: `mocha-parallel-tests` main target is to be 100%-compliant with `mocha` in terms of API and to not introduce its own options and APIs other than `--max-parallel` 83 | * **breaking change**: reporter output/stats now contains one more level for each file 84 | * change: `--max-parallel` option is `os.cpus().length` by default. You can also specify it manually or set it to 0 which means "immediately launch as many processes as the number of files" 85 | * new: `--delay` option is now supported for each subprocess 86 | * new: `--retries` option is now supported for each subprocess 87 | * new: supported peerDependencies versions of `mocha` are not 3, 4 and 5 88 | * new: all tests are now executed against all supported mocha versions 89 | * new: `mocha-parallel-tests` install should work fine on windows 90 | * new: TypeScript definitions are now provided in package.json 91 | 92 | Read more about new release here: https://github.com/mocha-parallel/mocha-parallel-tests/wiki/v2-release-notes 93 | 94 | ## 1.2.10 95 | 96 | * fix: support `--no-timeouts` option ([#120](https://github.com/mocha-parallel/mocha-parallel-tests/issues/120)) 97 | 98 | ## 1.2.9 99 | 100 | * fix: support only latest (3.x) `mocha` versions ([#98](https://github.com/mocha-parallel/mocha-parallel-tests/issues/98)) 101 | * fix: support `reporter.done()` callback for all external reporters (`mochawesome` for example) in both CLI and node.js API ([#113](https://github.com/mocha-parallel/mocha-parallel-tests/issues/113)) 102 | 103 | ## 1.2.8 104 | 105 | * fix: do not fail if no target is set and `--recursive` option is used ([#94](https://github.com/mocha-parallel/mocha-parallel-tests/issues/94)) 106 | 107 | ## 1.2.7 108 | 109 | * fix: add support for `xdescribe`, `xcontext`, `xspecify`, `xit` and `it.skip` ([#102](https://github.com/mocha-parallel/mocha-parallel-tests/issues/102)) 110 | * most of packages dependencies upgrade (minor changes) 111 | 112 | ## 1.2.6 113 | 114 | * fix: tests with binary dependencies can now be used with `mocha-parallel-tests` ([#100](https://github.com/mocha-parallel/mocha-parallel-tests/issues/100)) 115 | 116 | ## 1.2.5 117 | 118 | * fix: if file contains multiple `describe()` sections in it, one of it fails and `mocha-parallel-tests` is executed with `--retry` option, only this failed section is re-executed ([#77](https://github.com/mocha-parallel/mocha-parallel-tests/issues/77)) 119 | 120 | ## 1.2.4 121 | 122 | * fix: `--no-exit` CLI option is supported ([#85](https://github.com/mocha-parallel/mocha-parallel-tests/issues/85)) 123 | 124 | ## 1.2.3 125 | 126 | * fix: pwd-based reporters (located somewhere inside your repo) are supported ([#75](https://github.com/mocha-parallel/mocha-parallel-tests/issues/75)) 127 | * fix: `tap` and other reporters which use mocha's `runner` object are supported ([#78](https://github.com/mocha-parallel/mocha-parallel-tests/issues/78)) 128 | * fix: `mocha-jenkins-reporter`is supported ([#81](https://github.com/mocha-parallel/mocha-parallel-tests/issues/81)) 129 | 130 | ## 1.2.2 131 | 132 | * fix: `selenium-webdriver` issue with number of tests is fixed ([#69](https://github.com/mocha-parallel/mocha-parallel-tests/issues/69)). Currently all `selenium-webdriver` tests should work as expected. 133 | 134 | ## 1.2.1 135 | 136 | * fix: `--require`'d files are working as expected together with `--compilers` option ([#63](https://github.com/mocha-parallel/mocha-parallel-tests/issues/63)) 137 | 138 | ## 1.2.0 139 | 140 | * **new**: using mocha-parallel-tests programmatically is now supported. You can also pass own mocha-parallel-tests options by calling `setOwnOptions({maxParallel, retry})` method of mocha-parallel-tests instance. You should call it before `run()`. 141 | 142 | ## 1.1.x 143 | 144 | * **breaking change**: `mocha` is now in peerDependencies part of `mocha-parallel-tests` which means that you need to add both `mocha` and `mocha-parallel-tests` in your project's package.json. This also means that you can run both of your tests: written for mocha@2.x and mocha@3.x ([#55](https://github.com/mocha-parallel/mocha-parallel-tests/issues/55)) 145 | * fix: support for only() and skip() helpers ([#59](https://github.com/mocha-parallel/mocha-parallel-tests/issues/59)) 146 | * fix: main package file now refers to CommonJS file, not ES6 module. 147 | 148 | Version number is 1.1.0 because first `mocha-parallel-tests` releases had 1.0.x versioning scheme. 149 | 150 | ## 0.5.x 151 | 152 | * **new**: mutiple test suites (describe) inside one file run in parallel 153 | * fix: support mocha `--compilers` option ([#53](https://github.com/mocha-parallel/mocha-parallel-tests/issues/53)). `0.4.x` lacked support for JavaScript files. 154 | * fix: support mocha `--require` option 155 | * fix: support [mocha.opts](https://mochajs.org/#mochaopts) config file ([#44](https://github.com/mocha-parallel/mocha-parallel-tests/issues/44)) 156 | * fix: global hooks stored inside separate files are now executed if target is directory ([#41](https://github.com/mocha-parallel/mocha-parallel-tests/issues/41)) 157 | * fix: global hooks stored inside separate files are now executed ([#39](https://github.com/mocha-parallel/mocha-parallel-tests/issues/39)) 158 | 159 | ## 0.4.x 160 | 161 | * **new**: support for mocha `--compilers` option introduced 162 | * fix: tests using `--max-parallel=1` run without errors ([#34](https://github.com/mocha-parallel/mocha-parallel-tests/issues/34)) 163 | * fix: reporters with own options like `xunit` don't produce errors now ([#31](https://github.com/mocha-parallel/mocha-parallel-tests/issues/31)) 164 | * fix: `--retry` option is now working for mocha test hooks (before/after/etc) 165 | * `mocha-parallel-tests` code is now ES2015-compatible 166 | 167 | ## 0.3.x 168 | 169 | * fix: runner displays right tests execution time now ([#24](https://github.com/mocha-parallel/mocha-parallel-tests/issues/24)) 170 | * fix: exit codes are now the same as in original mocha runner ([#27](https://github.com/mocha-parallel/mocha-parallel-tests/issues/27)) 171 | * bump `mocha` version to 2.4.5. Check its [changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md) for more info. Previous version was 2.3.4. 172 | 173 | ## 0.2.x 174 | 175 | * **new**: `--retry ` to set number of retries for failing tests (0 by default) 176 | 177 | ## 0.1.x 178 | 179 | * fix: `console.log|error`s from tests do not cross with each other. Instead they are waiting until proper test starts executing :smiley: 180 | * fix: support stack trace of errors 181 | * fix: support for incoming array of files 182 | * fix: `--max-parallel` option introduced bug when some of the files were not tests: not all tests could run 183 | * **new**: `--max-parallel ` to set max number of running parallel tests 184 | 185 | ## 0.0.x 186 | 187 | * fix emits of end events 188 | * tests for package 189 | * changed readme and refactoring 190 | * reporter control added 191 | * first release :smiley: 192 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dmitry Sorin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mocha-parallel-tests 2 | 3 | PROJECT IS NOT SUPPORTED ANYMORE. [MAINTAINERS ARE WELCOME](https://github.com/mocha-parallel/mocha-parallel-tests/issues/308). 4 | 5 | `mocha-parallel-tests` is a test runner for tests written with `mocha` testing framework which allows you to run them in parallel. `mocha-parallel-tests` executes **each of your test files in a separate process** while maintaining the output structure of `mocha`. Compared to the other tools which try to parallelize `mocha` tests execution, `mocha-parallel-tests` doesn't require you to write the code in a different way or use some specific APIs - just run your tests with `mocha-parallel-tests` instead of `mocha` and you will see the difference. Or if you prefer to use `mocha` programmatic API replace it with `mocha-parallel-tests` default export and you're done! 6 | 7 | If you're using Node.JS >= 12 your tests execution will be even faster because `mocha-parallel-tests` supports running tests with Node.JS worker threads API: instead of creating fully fledged Node.JS processes `mocha-parallel-tests` runs your tests in lighter threads within the same process. This results in a faster tests processing and less memory consumption. 8 | 9 | ## Installation 10 | 11 | `npm install --save-dev mocha mocha-parallel-tests` 12 | 13 | **ATTENTION**: `mocha` is a peer dependency of `mocha-parallel-tests` so you also need to install `mocha`. Currently `mocha` versions 3, 4, 5, 6, and 7 are supported. 14 | 15 | ## Usage 16 | 17 | ### CLI 18 | 19 | ```bash 20 | # mocha example 21 | $ mocha -R xunit --timeout 10000 --slow 1000 test/*.spec.js 22 | 23 | # mocha-parallel-tests example 24 | $ mocha-parallel-tests -R xunit --timeout 10000 --slow 1000 test/*.spec.js 25 | ``` 26 | 27 | Most of `mocha` CLI options are supported. If you're missing some of the options support you're welcome to submit a PR: all options are applied in a same simple way. 28 | 29 | ### Programmatic API 30 | 31 | ```javascript 32 | // mocha example 33 | import Mocha from 'mocha'; 34 | const mocha = new Mocha(); 35 | mocha.addFile(`${__dirname}/index.spec.js`); 36 | mocha.run(); 37 | 38 | // mocha-parallel-tests example 39 | // if you're using TypeScript you don't need to install @types/mocha-parallel-tests 40 | // because package comes with typings in it 41 | import Mocha from 'mocha-parallel-tests'; // or `const Mocha = require('mocha-parallel-tests').default` if you're using CommonJS 42 | const mocha = new Mocha(); 43 | mocha.addFile(`${__dirname}/index.spec.js`); 44 | mocha.run(); 45 | ``` 46 | 47 | ## Parallel limit 48 | 49 | `mocha-parallel-tests` CLI executable has its own `--max-parallel` option which is the amount of tests executed at the same time. By default it's equal to the number of logical CPI cores (`os.cpus().length`) on your computer but you can also specify your own number or set it to 0, which means that all test files will be started executing at the same time. However this is not recommended especially on machines with low number of CPUs and big number of tests executed. 50 | 51 | ## Differences with mocha 52 | 53 | Main difference with `mocha` comes from the fact that all files are executed in separate processes/threads and don't share the scope. This means that even global variables values that you could've used to share the data between test suites will not be reliable. There's also some specific behaviour for some of the `mocha` CLI options like `--bail`: it's just applied to each test in its process. You can see the full list of differences [here](https://github.com/mocha-parallel/mocha-parallel-tests/wiki/Differences-with-mocha). 54 | 55 | There's also a [list of limitations](https://github.com/mocha-parallel/mocha-parallel-tests/wiki/Limitations) that you can hit when you launch your tests with `mocha-parallel-tests`. 56 | 57 | From the reporter perspective the main difference between tests executed with `mocha` and `mocha-parallel-tests` is another level of nesting which again comes from the fact that main process adds one more "suite" level and all tests results are merged into that: 58 | 59 | **mocha** 60 | 61 | 62 | mocha spec reporter output 63 | 64 | 65 | **mocha-parallel-tests** 66 | 67 | 68 | mocha-parallel-tests spec reporter output 69 | 70 | 71 | ## Comparison with `mocha.parallel` 72 | 73 | [mocha.parallel](https://github.com/danielstjules/mocha.parallel) is a tool which allows you to run mocha tests in parallel. While it seems pretty similar to `mocha-parallel-tests` there's a massive difference between them. Check [this page](https://github.com/mocha-parallel/mocha-parallel-tests/wiki/Comparison-with-mocha.parallel) for a full comparison. 74 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against the latest version of this Node.js version 2 | environment: 3 | matrix: 4 | - nodejs_version: "8" 5 | - nodejs_version: "9" 6 | - nodejs_version: "10" 7 | 8 | # Install scripts. (runs after repo cloning) 9 | install: 10 | - ps: Install-Product node $env:nodejs_version 11 | - npm install 12 | 13 | # Post-install test scripts. 14 | test_script: 15 | # Output useful info for debugging. 16 | - node --version 17 | - npm --version 18 | # run tests 19 | - npm run test:ci 20 | 21 | # Don't actually build. 22 | build: off 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocha-parallel-tests", 3 | "version": "2.3.0", 4 | "homepage": "https://github.com/mocha-parallel/mocha-parallel-tests", 5 | "description": "Run mocha tests in parallel", 6 | "main": "./dist/main/mocha.js", 7 | "module": "./dist/main/mocha.js", 8 | "bin": { 9 | "mocha-parallel-tests": "dist/bin/cli.js" 10 | }, 11 | "contributors": [ 12 | "Dmitrii Sorin ", 13 | "Gabriel Fürstenheim", 14 | "Jorge Florian ", 15 | "Kirill Molchanov ", 16 | "Maria Motkina ", 17 | "Nikolay Basov", 18 | "Vadim Kolontsov ", 19 | "Hennadii Bulakh", 20 | "Jeroen Claassens " 21 | ], 22 | "dependencies": { 23 | "circular-json": "^0.5.9", 24 | "debug": "^4.1.1", 25 | "uuid": "^3.4.0", 26 | "yargs": "^13.3.0" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git://github.com/mocha-parallel/mocha-parallel-tests.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/mocha-parallel/mocha-parallel-tests/issues" 34 | }, 35 | "files": [ 36 | "bin", 37 | "dist", 38 | "lib" 39 | ], 40 | "types": "dist/main/mocha.d.ts", 41 | "keywords": [ 42 | "mocha", 43 | "mocha-parallel-tests", 44 | "parallel tests", 45 | "unit tests", 46 | "tests", 47 | "threads", 48 | "worker threads" 49 | ], 50 | "scripts": { 51 | "build": "tsc && chmod +x dist/bin/cli.js", 52 | "clean": "rm -fr dist", 53 | "lint": "npm run lint:sources && npm run lint:tests", 54 | "lint:sources": "eslint -c src/.eslintrc.js --ext .ts src/", 55 | "lint:tests": "eslint -c test/.eslintrc.js --ext .js test/", 56 | "prepublishOnly": "npm run clean && npm run build", 57 | "pretest": "npm run prepublishOnly", 58 | "test": "bash test/index.sh", 59 | "test:ci": "npm run lint && npm run test:mocha-3 && npm run test:mocha-4 && npm run test:mocha-5 && npm run test:mocha-6 && npm run test:mocha-7", 60 | "test:mocha-3": "npm install mocha@3 --no-save && npm test", 61 | "test:mocha-4": "npm install mocha@4 --no-save && npm test", 62 | "test:mocha-5": "npm install mocha@5 --no-save && npm test", 63 | "test:mocha-6": "npm install mocha@6 --no-save && npm test", 64 | "test:mocha-7": "npm install mocha@7 --no-save && npm test" 65 | }, 66 | "license": "MIT", 67 | "engines": { 68 | "node": ">=8" 69 | }, 70 | "peerDependencies": { 71 | "mocha": "3.0.0 - 7.x.x" 72 | }, 73 | "devDependencies": { 74 | "@babel/core": "^7.8.4", 75 | "@babel/plugin-transform-react-jsx": "^7.8.3", 76 | "@babel/register": "^7.8.3", 77 | "@types/circular-json": "^0.4.0", 78 | "@types/debug": "^4.1.5", 79 | "@types/mocha": "^7.0.1", 80 | "@types/node": "^13.7.0", 81 | "@types/uuid": "^3.4.7", 82 | "@types/yargs": "^13.0.8", 83 | "@typescript-eslint/eslint-plugin": "^2.19.0", 84 | "@typescript-eslint/parser": "^2.19.0", 85 | "chai": "^4.2.0", 86 | "cheerio": "^0.22.0", 87 | "eslint": "^6.8.0", 88 | "husky": "^3.1.0", 89 | "microtime": "^3.0.0", 90 | "mocha": "^7.0.1", 91 | "mocha-allure-reporter": "^1.4.0", 92 | "mocha-jenkins-reporter": "^0.4.2", 93 | "mocha-teamcity-reporter": "^3.0.0", 94 | "mochawesome": "^4.1.0", 95 | "q": "^1.5.1", 96 | "sinon": "^7.5.0", 97 | "ts-node": "^8.6.2", 98 | "typescript": "^3.7.5" 99 | }, 100 | "funding": { 101 | "url": "https://github.com/sponsors/1999" 102 | }, 103 | "husky": { 104 | "hooks": { 105 | "pre-commit": "npm run lint" 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true 5 | }, 6 | parser: '@typescript-eslint/parser', 7 | parserOptions: { 8 | tsconfigRootDir: '../', 9 | }, 10 | plugins: ['@typescript-eslint'], 11 | extends: [ 12 | 'plugin:@typescript-eslint/recommended', 13 | 'eslint:recommended', 14 | ], 15 | rules: { 16 | '@typescript-eslint/indent': ['error', 2], 17 | '@typescript-eslint/explicit-function-return-type': 'off', 18 | '@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import yargs from 'yargs'; 4 | 5 | import MochaWrapper from '../main/mocha'; 6 | import { setProcessExitListeners } from '../util'; 7 | 8 | import applyAsyncOnly from './options/async-only'; 9 | import applyBail from './options/bail'; 10 | import applyCheckLeaks from './options/check-leaks'; 11 | import applyCompilers from './options/compilers'; 12 | import applyDelay from './options/delay'; 13 | import applyExit from './options/exit'; 14 | import applyForbidOnly from './options/forbid-only'; 15 | import applyForbidPending from './options/forbid-pending'; 16 | import applyFullTrace from './options/full-trace'; 17 | import applyGrepPattern from './options/grep'; 18 | import applyMaxParallel from './options/max-parallel'; 19 | import applyNoTimeouts from './options/no-timeouts'; 20 | import applyReporter from './options/reporter'; 21 | import applyReporterOptions from './options/reporter-options'; 22 | import applyRequires from './options/require'; 23 | import applyFiles from './options/file'; 24 | import getFilesList from './options/rest'; 25 | import applyRetries from './options/retries'; 26 | import applySlow from './options/slow'; 27 | import applyTimeout from './options/timeout'; 28 | 29 | setProcessExitListeners(); 30 | 31 | // mocha@6 introduced a new way to parse CLI arguments through yargs context 32 | // this mechanism should be used if `loadOptions` is defined 33 | const yargsOptionalArgs: object[] = []; 34 | 35 | try { 36 | // eslint-disable-next-line @typescript-eslint/no-var-requires 37 | const { loadOptions } = require('mocha/lib/cli/options'); 38 | yargsOptionalArgs.push(loadOptions(process.argv)); 39 | } catch (ex) { 40 | // eslint-disable-next-line @typescript-eslint/no-var-requires 41 | const getOptions = require('mocha/bin/options'); 42 | 43 | // NB: legacy (mocha before version 6) 44 | // --opts changes process.argv so it should be executed first 45 | getOptions(); 46 | } 47 | 48 | // starting from mocha@6 `--no-timeout` and `--no-timeouts` is the same thing 49 | const newTimeoutsBehaviour = yargsOptionalArgs.length === 1; 50 | // starting from mocha@6 `--reporter-options` option type is array 51 | const newReporterOptionsType = yargsOptionalArgs.length === 1; 52 | 53 | const mocha = new MochaWrapper(); 54 | const argv = yargs 55 | .option('async-only', { 56 | alias: 'A', 57 | boolean: true, 58 | }) 59 | .option('bail', { 60 | alias: 'b', 61 | boolean: true, 62 | }) 63 | .option('check-leaks', { 64 | boolean: true, 65 | }) 66 | .option('compilers', { 67 | default: [], 68 | }) 69 | .option('delay', { 70 | boolean: true, 71 | }) 72 | .option('exit', { 73 | boolean: true, 74 | }) 75 | .option('forbidOnly', { 76 | boolean: true, 77 | }) 78 | .option('forbidPending', { 79 | boolean: true, 80 | }) 81 | .option('full-trace', { 82 | boolean: true, 83 | }) 84 | .option('max-parallel', { 85 | number: true, 86 | }) 87 | .option('grep', { 88 | alias: 'g', 89 | string: true, 90 | }) 91 | .option('recursive', { 92 | boolean: true, 93 | }) 94 | .option('reporter', { 95 | alias: 'R', 96 | default: 'spec', 97 | string: true, 98 | }) 99 | .option('reporter-options', { 100 | alias: 'O', 101 | string: !newReporterOptionsType, 102 | array: newReporterOptionsType, 103 | }) 104 | .option('retries', { 105 | number: true, 106 | }) 107 | .option('file', { 108 | default: [], 109 | }) 110 | .option('require', { 111 | alias: 'r', 112 | default: [], 113 | }) 114 | .option('slow', { 115 | alias: 's', 116 | number: true, 117 | }) 118 | .option('timeout', { 119 | alias: 't', 120 | number: true, 121 | }) 122 | .option('timeouts', { 123 | boolean: !newTimeoutsBehaviour, 124 | number: newTimeoutsBehaviour, 125 | }) 126 | .option('ui', { 127 | alias: 'u', 128 | string: true, 129 | }) 130 | .parse(process.argv, ...yargsOptionalArgs); 131 | 132 | // --async-only 133 | applyAsyncOnly(mocha, argv['async-only']); 134 | 135 | // --bail 136 | applyBail(mocha, argv.bail); 137 | 138 | // --check-leaks 139 | applyCheckLeaks(mocha, argv['check-leaks']); 140 | 141 | // --compilers 142 | const { compilers, extensions } = applyCompilers(argv.compilers); 143 | mocha.addCompilersForSubprocess(compilers); 144 | 145 | // --delay 146 | applyDelay(mocha, argv.delay); 147 | 148 | // --exit 149 | applyExit(mocha, argv.exit); 150 | 151 | // --forbid-only 152 | applyForbidOnly(mocha, argv.forbidOnly); 153 | 154 | // --forbid-pending 155 | applyForbidPending(mocha, argv.forbidPending); 156 | 157 | // --full-trace 158 | applyFullTrace(mocha, argv['full-trace']); 159 | 160 | // --grep option 161 | applyGrepPattern(mocha, argv.grep); 162 | 163 | // --max-parallel 164 | applyMaxParallel(mocha, argv['max-parallel']); 165 | 166 | // --no-timeouts 167 | if (newTimeoutsBehaviour) { 168 | const enableTimeouts = Boolean(argv.timeout || argv.timeouts); 169 | applyNoTimeouts(mocha, enableTimeouts); 170 | } else { 171 | applyNoTimeouts(mocha, argv.timeouts as boolean); 172 | } 173 | 174 | // --r, --require 175 | const requires = applyRequires(argv.require); 176 | mocha.addRequiresForSubprocess(requires); 177 | 178 | // --file 179 | applyFiles(mocha, argv.file); 180 | 181 | // --reporter-options 182 | const argvReporterOptions = newReporterOptionsType 183 | ? argv['reporter-options'] as string[] 184 | : [argv['reporter-options'] as string]; 185 | 186 | const reporterOptions = argv['reporter-options'] !== undefined 187 | ? applyReporterOptions(argvReporterOptions) 188 | : {}; 189 | 190 | // --reporter 191 | applyReporter(mocha, argv.reporter, reporterOptions); 192 | 193 | // --retries 194 | applyRetries(mocha, argv.retries); 195 | 196 | // --slow 197 | applySlow(mocha, argv.slow); 198 | 199 | // --timeout 200 | applyTimeout(mocha, argv.timeout); 201 | 202 | if(argv.ui) { 203 | mocha.setUi(argv.ui); 204 | } 205 | 206 | // find files 207 | const files = getFilesList(argv._.slice(2), extensions, argv.recursive || false); 208 | 209 | if (!files.length) { 210 | // eslint-disable-next-line no-console 211 | console.error('No test files found'); 212 | process.exit(1); 213 | } 214 | 215 | for (const file of files) { 216 | mocha.addFile(file); 217 | } 218 | 219 | const isTypescriptRun = argv.$0.endsWith('.ts'); 220 | if (isTypescriptRun) { 221 | mocha.setTypescriptRunMode(); 222 | } 223 | 224 | mocha.run((code) => { 225 | process.on('exit', function onExit() { 226 | process.exit(Math.min(code, 255)); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /src/bin/options/async-only.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | export default function applyAsyncOnly(mocha: Mocha, asyncOnly?: boolean) { 4 | if (asyncOnly) { 5 | mocha.asyncOnly(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/options/bail.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | export default function applyBail(mocha: Mocha, bail?: boolean) { 4 | mocha.suite.bail(bail || false); 5 | } 6 | -------------------------------------------------------------------------------- /src/bin/options/check-leaks.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | export default function applyCheckLeaks(mocha: Mocha, checkLeaks?: boolean) { 4 | if (checkLeaks) { 5 | mocha.checkLeaks(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/options/compilers.ts: -------------------------------------------------------------------------------- 1 | export { applyCompilers as default } from '../../util'; 2 | -------------------------------------------------------------------------------- /src/bin/options/delay.ts: -------------------------------------------------------------------------------- 1 | export { applyDelay as default } from '../../util'; 2 | -------------------------------------------------------------------------------- /src/bin/options/exit.ts: -------------------------------------------------------------------------------- 1 | import Mocha from '../../main/mocha'; 2 | 3 | export default function applyExit(mocha: Mocha, exitMode?: boolean) { 4 | if (exitMode) { 5 | mocha.enableExitMode(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/options/file.ts: -------------------------------------------------------------------------------- 1 | export { applyFiles as default } from '../../util'; 2 | -------------------------------------------------------------------------------- /src/bin/options/forbid-only.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | export default function applyForbidOnly(mocha: Mocha, forbidOnly?: boolean) { 4 | if (forbidOnly) { 5 | mocha.forbidOnly(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/options/forbid-pending.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | export default function applyForbidPending(mocha: Mocha, forbidPending?: boolean) { 4 | if (forbidPending) { 5 | mocha.forbidPending(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/options/full-trace.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | export default function applyFullTrace(mocha: Mocha, fullTrace?: boolean) { 4 | if (fullTrace) { 5 | mocha.fullTrace(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/options/grep.ts: -------------------------------------------------------------------------------- 1 | export { applyGrepPattern as default } from '../../util'; 2 | -------------------------------------------------------------------------------- /src/bin/options/max-parallel.ts: -------------------------------------------------------------------------------- 1 | import Mocha from '../../main/mocha'; 2 | 3 | export default function applyMaxParallel(mocha: Mocha, maxParallel?: number) { 4 | if (maxParallel !== undefined) { 5 | mocha.setMaxParallel(maxParallel); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/options/no-timeouts.ts: -------------------------------------------------------------------------------- 1 | export { applyNoTimeouts as default } from '../../util'; 2 | -------------------------------------------------------------------------------- /src/bin/options/reporter-options.ts: -------------------------------------------------------------------------------- 1 | export interface CLIReporterOptions { 2 | [key: string]: string | boolean; 3 | } 4 | 5 | export default function applyReporterOptions(reporterOptions: string[]) { 6 | const output: CLIReporterOptions = {}; 7 | 8 | for (const reporterOption of reporterOptions) { 9 | reporterOption.split(',').forEach((opt) => { 10 | const L = opt.split('='); 11 | 12 | if (L.length > 2 || L.length === 0) { 13 | throw new Error(`Invalid reporter option "${opt}"`); 14 | } 15 | 16 | if (L.length === 2) { 17 | output[L[0]] = L[1]; 18 | } else { 19 | output[L[0]] = true; 20 | } 21 | }); 22 | } 23 | 24 | return output; 25 | } 26 | -------------------------------------------------------------------------------- /src/bin/options/reporter.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Mocha, { reporters } from 'mocha'; 3 | import { join } from 'path'; 4 | import { CLIReporterOptions } from './reporter-options'; 5 | 6 | export default function applyReporter(mocha: Mocha, reporter: string, reporterOptions: CLIReporterOptions) { 7 | assert.strictEqual(typeof reporter, 'string', '--reporter option can be specified only once'); 8 | mocha.reporter(reporter, reporterOptions); 9 | 10 | // load reporter 11 | let Reporter: reporters.Base; 12 | 13 | // required reporter can be in the process CWD 14 | const cwd = process.cwd(); 15 | module.paths.push(cwd, join(cwd, 'node_modules')); 16 | 17 | try { 18 | Reporter = require(`mocha/lib/reporters/${reporter}`); 19 | } catch (ex) { 20 | try { 21 | Reporter = require(reporter); 22 | } catch (ex) { 23 | throw new Error(`Reporter "${reporter}" does not exist`); 24 | } 25 | } 26 | 27 | return Reporter; 28 | } 29 | -------------------------------------------------------------------------------- /src/bin/options/require.ts: -------------------------------------------------------------------------------- 1 | export { applyRequires as default } from '../../util'; 2 | -------------------------------------------------------------------------------- /src/bin/options/rest.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const { lookupFiles: mochaLookupFiles } = require('mocha/lib/utils'); 3 | 4 | export default function getFilesList(rest: string[], extensions: string[], recursive: boolean): string[] { 5 | const filesList = rest.length ? rest : ['test']; 6 | const output: string[] = []; 7 | 8 | for (const file of filesList) { 9 | try { 10 | const newFiles = mochaLookupFiles(file, extensions, recursive) as string[] | string; 11 | const newFilesList = Array.isArray(newFiles) ? newFiles : [newFiles]; 12 | 13 | output.push(...newFilesList); 14 | } catch (err) { 15 | if (err.message.startsWith('cannot resolve path')) { 16 | // eslint-disable-next-line no-console 17 | console.error(`Warning: Could not find any test files matching pattern: ${file}`); 18 | continue; 19 | } 20 | 21 | throw err; 22 | } 23 | } 24 | 25 | return output; 26 | } 27 | -------------------------------------------------------------------------------- /src/bin/options/retries.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | export default function applyRetries(mocha: Mocha, retries?: number) { 4 | if (retries) { 5 | mocha.suite.retries(retries); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/options/slow.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | export default function applySlow(mocha: Mocha, slow?: number) { 4 | if (slow) { 5 | mocha.suite.slow(slow); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/options/timeout.ts: -------------------------------------------------------------------------------- 1 | export { applyTimeouts as default } from '../../util'; 2 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const RUNNABLE_MESSAGE_CHANNEL_PROP = '__mpt_randomId__'; 2 | export const SUBPROCESS_RETRIED_SUITE_ID = '__mpt_suiteId_'; 3 | 4 | export const SUITE_OWN_OPTIONS = [ 5 | 'timeout', 6 | 'enableTimeouts', 7 | 'slow', 8 | 'bail', 9 | 'retries', 10 | ]; 11 | 12 | export const DEBUG_SUBPROCESS = { 13 | argument: '--debug-mpt-subprocess', 14 | yargs: 'debugMptSubprocess', 15 | }; 16 | -------------------------------------------------------------------------------- /src/main/mocha.ts: -------------------------------------------------------------------------------- 1 | import CircularJSON from 'circular-json'; 2 | import debug from 'debug'; 3 | import Mocha from 'mocha'; 4 | 5 | import RunnerMain from './runner'; 6 | import TaskManager from './task-manager'; 7 | import { subprocessParseReviver } from './util'; 8 | 9 | import { SubprocessResult } from '../message-channel'; 10 | import { RetriedTest, Suite } from '../mocha'; 11 | import { getThread } from './thread'; 12 | import { ThreadOptions } from '../thread'; 13 | import { SUITE_OWN_OPTIONS } from '../config'; 14 | 15 | const debugLog = debug('mocha-parallel-tests'); 16 | 17 | export default class MochaWrapper extends Mocha { 18 | private isTypescriptRunMode = false; 19 | private maxParallel: number | undefined; 20 | private requires: string[] = []; 21 | private compilers: string[] = []; 22 | private exitImmediately = false; 23 | private uiOption: string; 24 | 25 | setUi(ui: string) { 26 | this.uiOption = ui; 27 | } 28 | 29 | setTypescriptRunMode() { 30 | this.isTypescriptRunMode = true; 31 | } 32 | 33 | /** 34 | * All `--require` options should be applied for subprocesses 35 | */ 36 | addRequiresForSubprocess(requires: string[]) { 37 | this.requires = requires; 38 | } 39 | 40 | /** 41 | * All `--compiler` options should be applied for subprocesses 42 | */ 43 | addCompilersForSubprocess(compilers: string[]) { 44 | this.compilers = compilers; 45 | } 46 | 47 | setMaxParallel(maxParallel: number) { 48 | this.maxParallel = maxParallel; 49 | } 50 | 51 | enableExitMode() { 52 | this.exitImmediately = true; 53 | return this; 54 | } 55 | 56 | run(onComplete?: (failures: number) => void): RunnerMain { 57 | const { 58 | asyncOnly, 59 | ignoreLeaks, 60 | forbidOnly, 61 | forbidPending, 62 | fullStackTrace, 63 | } = this.options; 64 | 65 | const rootSuite = this.suite as Suite; 66 | 67 | const runner = new RunnerMain(rootSuite); 68 | runner.ignoreLeaks = ignoreLeaks !== false; 69 | runner.forbidOnly = forbidOnly; 70 | runner.forbidPending = forbidPending; 71 | runner.fullStackTrace = fullStackTrace; 72 | runner.asyncOnly = asyncOnly; 73 | 74 | const taskManager = new TaskManager(this.maxParallel); 75 | for (const file of this.files) { 76 | const task = () => this.runThread(file); 77 | taskManager.add(task); 78 | } 79 | 80 | this.options.files = this.files; 81 | 82 | // Refer to mocha lib/mocha.js run() method for more info here 83 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 84 | const reporter = new (this as any)._reporter( 85 | runner, 86 | this.options, 87 | ); 88 | 89 | // emit `start` and `suite` events 90 | // so that reporters can record the start time 91 | runner.emitStartEvents(); 92 | taskManager.execute(); 93 | 94 | taskManager 95 | .on('taskFinished', (testResults: SubprocessResult) => { 96 | const { 97 | code, 98 | execTime, 99 | events, 100 | file, 101 | syncedSubprocessData, 102 | } = testResults; 103 | 104 | debugLog(`File execution finished: ${file}`); 105 | debugLog(`Has synced data: ${Boolean(syncedSubprocessData)}, number of events: ${events.length}, execution time: ${execTime}`); 106 | 107 | const retriedTests: RetriedTest[] = []; 108 | 109 | if (syncedSubprocessData) { 110 | this.addSubprocessSuites(testResults); 111 | retriedTests.push(...this.extractSubprocessRetriedTests(testResults)); 112 | } 113 | 114 | runner.reEmitSubprocessEvents(testResults, retriedTests); 115 | 116 | const hasEndEvent = events.find((event) => event.type === 'runner' && event.event === 'end'); 117 | if (!hasEndEvent && code !== 0) { 118 | process.exit(code); 119 | } 120 | }) 121 | .on('end', () => { 122 | debugLog('All tests finished processing'); 123 | 124 | const done = (failures: number) => { 125 | if (reporter.done) { 126 | reporter.done(failures, onComplete); 127 | } else if (onComplete) { 128 | onComplete(failures); 129 | } 130 | }; 131 | 132 | runner.emitFinishEvents(done); 133 | }); 134 | 135 | return runner; 136 | } 137 | 138 | private addSubprocessSuites(testArtifacts: SubprocessResult): void { 139 | const rootSuite = this.suite; 140 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 141 | const serialized = testArtifacts.syncedSubprocessData!; 142 | const { rootSuite: testRootSuite } = CircularJSON.parse(serialized.results, subprocessParseReviver); 143 | 144 | Object.assign(testRootSuite, { 145 | parent: rootSuite, 146 | root: false, 147 | }); 148 | 149 | rootSuite.suites.push(testRootSuite); 150 | } 151 | 152 | private extractSubprocessRetriedTests(testArtifacts: SubprocessResult): RetriedTest[] { 153 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 154 | const serialized = testArtifacts.syncedSubprocessData!; 155 | const { retriesTests } = CircularJSON.parse(serialized.retries, subprocessParseReviver); 156 | 157 | return retriesTests as RetriedTest[]; 158 | } 159 | 160 | private async runThread(file: string): Promise { 161 | const options = this.getThreadOptions(); 162 | const thread = getThread(file, options); 163 | 164 | return await thread.run(); 165 | } 166 | 167 | private getThreadOptions(): ThreadOptions { 168 | const options: ThreadOptions = { 169 | compilers: [], 170 | delay: false, 171 | exitImmediately: false, 172 | fullTrace: false, 173 | isTypescriptRunMode: this.isTypescriptRunMode, 174 | requires: [], 175 | file: [], 176 | ui: this.uiOption 177 | }; 178 | 179 | for (const requirePath of this.requires) { 180 | options.requires.push(requirePath); 181 | } 182 | 183 | for (const compilerPath of this.compilers) { 184 | options.compilers.push(compilerPath); 185 | } 186 | 187 | if (this.options.delay) { 188 | options.delay = true; 189 | } 190 | 191 | if (this.options.grep) { 192 | options.grep = this.options.grep.toString(); 193 | } 194 | 195 | if (this.exitImmediately) { 196 | options.exitImmediately = true; 197 | } 198 | 199 | if (this.options.fullStackTrace) { 200 | options.fullTrace = true; 201 | } 202 | 203 | for (const option of SUITE_OWN_OPTIONS) { 204 | options[option] = this.suite[option](); 205 | } 206 | 207 | return options; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/runner.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Runner } from 'mocha'; 3 | 4 | import { RUNNABLE_MESSAGE_CHANNEL_PROP, SUBPROCESS_RETRIED_SUITE_ID } from '../config'; 5 | import { SubprocessResult, SubprocessMessage, SubprocessRunnerMessage, isErrorEvent, isEventWithId } from '../message-channel'; 6 | import { 7 | Hook, 8 | RetriedTest, 9 | Suite, 10 | Test, 11 | } from '../mocha'; 12 | 13 | export default class RunnerMain extends Runner { 14 | private rootSuite: Suite; 15 | private retriedTests: RetriedTest[] = []; 16 | private subprocessTestResults: SubprocessResult; 17 | 18 | constructor(rootSuite: Suite) { 19 | super(rootSuite, false); 20 | this.rootSuite = rootSuite; 21 | 22 | // in mocha@6 assigning "stats" field to the runner is extracted into a separate function 23 | try { 24 | // eslint-disable-next-line @typescript-eslint/no-var-requires 25 | const createStatsCollector = require('mocha/lib/stats-collector'); 26 | createStatsCollector(this); 27 | } catch (ex) { 28 | // older mocha version 29 | } 30 | 31 | this.once('end', this.onExecutionComplete); 32 | this.on('fail', this.onFail); 33 | } 34 | 35 | emitStartEvents() { 36 | this.emit('start'); 37 | this.emit('suite', this.rootSuite); 38 | } 39 | 40 | emitFinishEvents(onComplete?: (failures: number) => void) { 41 | this.emit('suite end', this.rootSuite); 42 | this.emit('end'); 43 | 44 | if (onComplete) { 45 | onComplete(this.failures); 46 | } 47 | } 48 | 49 | reEmitSubprocessEvents( 50 | testResults: SubprocessResult, 51 | retriedTests: RetriedTest[], 52 | ) { 53 | this.subprocessTestResults = testResults; 54 | this.setRetriesTests(retriedTests); 55 | 56 | this.emitSubprocessEvents(); 57 | } 58 | 59 | private onFail = () => { 60 | this.failures++; 61 | } 62 | 63 | private onExecutionComplete = () => { 64 | if (this.forbidOnly) { 65 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 66 | this.failures += this.stats!.tests; 67 | } 68 | 69 | if (this.forbidPending) { 70 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 71 | this.failures += this.stats!.pending; 72 | } 73 | } 74 | 75 | private setRetriesTests(tests: RetriedTest[]) { 76 | for (const test of tests) { 77 | const suite = this.findSuiteById(test[SUBPROCESS_RETRIED_SUITE_ID]); 78 | assert(suite, 'Couldn\'t find retried test suite'); 79 | 80 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 81 | test.parent = suite!; 82 | this.retriedTests.push(test); 83 | } 84 | } 85 | 86 | private findSuiteById(id: string, rootSuite: Suite = this.rootSuite): Suite | null { 87 | if (rootSuite[RUNNABLE_MESSAGE_CHANNEL_PROP] === id) { 88 | return rootSuite; 89 | } 90 | 91 | for (const suite of rootSuite.suites) { 92 | const inner = this.findSuiteById(id, suite); 93 | if (inner) { 94 | return inner; 95 | } 96 | } 97 | 98 | return null; 99 | } 100 | 101 | private findRetriedTestById(id: string): Test | undefined { 102 | return this.retriedTests.find((test) => test[RUNNABLE_MESSAGE_CHANNEL_PROP] === id); 103 | } 104 | 105 | private findTestById(id: string, rootSuite: Suite = this.rootSuite): Test | null { 106 | for (const test of rootSuite.tests) { 107 | if (test[RUNNABLE_MESSAGE_CHANNEL_PROP] === id) { 108 | return test; 109 | } 110 | } 111 | 112 | for (const suite of rootSuite.suites) { 113 | const inner = this.findTestById(id, suite); 114 | if (inner) { 115 | return inner; 116 | } 117 | } 118 | 119 | return null; 120 | } 121 | 122 | private findHookById(id: string, rootSuite: Suite = this.rootSuite): Hook | null { 123 | for (const hookType of ['_beforeEach', '_beforeAll', '_afterEach', '_afterAll']) { 124 | for (const hook of rootSuite[hookType]) { 125 | if (hook[RUNNABLE_MESSAGE_CHANNEL_PROP] === id) { 126 | return hook; 127 | } 128 | } 129 | } 130 | 131 | for (const suite of rootSuite.suites) { 132 | const inner = this.findHookById(id, suite); 133 | if (inner) { 134 | return inner; 135 | } 136 | } 137 | 138 | return null; 139 | } 140 | 141 | /** 142 | * Sometimes mocha "forgets" to replace the test in suite.tests 143 | * Example of this can be a sync test which fails twice and passes on third run 144 | * If the test is executed with `--retries 2` we will get this result 145 | */ 146 | private findForgottenTestById(id: string, rootSuite: Suite = this.rootSuite): Test | null { 147 | if (rootSuite.ctx.test && rootSuite.ctx.test[RUNNABLE_MESSAGE_CHANNEL_PROP] === id) { 148 | return rootSuite.ctx.test as Test; 149 | } 150 | 151 | for (const suite of rootSuite.suites) { 152 | const inner = this.findForgottenTestById(id, suite); 153 | if (inner) { 154 | return inner; 155 | } 156 | } 157 | 158 | return null; 159 | } 160 | 161 | private emitSubprocessEvents() { 162 | for (const subprocessEvent of this.subprocessTestResults.events) { 163 | if (this.isRunnerMessage(subprocessEvent)) { 164 | const { event, data } = subprocessEvent; 165 | 166 | if (event === 'waiting') { 167 | this.emit('waiting', this.rootSuite); 168 | continue; 169 | } 170 | 171 | if (!isEventWithId(data)) { 172 | continue; 173 | } 174 | 175 | switch (event) { 176 | case 'start': 177 | case 'end': 178 | // ignore these events from subprocess 179 | break; 180 | 181 | case 'suite': 182 | case 'suite end': { 183 | const suite = this.findSuiteById(data.id); 184 | assert(suite, `Couldn't find suite by id: ${data.id}`); 185 | 186 | this.emit(event, suite); 187 | break; 188 | } 189 | 190 | case 'test': 191 | case 'test end': 192 | case 'pending': 193 | case 'pass': { 194 | const test = this.findTestById(data.id) 195 | || this.findRetriedTestById(data.id) 196 | || this.findForgottenTestById(data.id); 197 | assert(test, `Couldn't find test by id: ${data.id}`); 198 | 199 | this.emit(event, test); 200 | break; 201 | } 202 | 203 | case 'fail': { 204 | const test = this.findTestById(data.id) 205 | || this.findHookById(data.id) 206 | || this.findForgottenTestById(data.id); 207 | assert(test, `Couldn't find test by id: ${data.id}`); 208 | 209 | if (!isErrorEvent(data)) { 210 | throw new Error('Unexpected fail event without err field'); 211 | } 212 | 213 | this.emit(event, test, data.err); 214 | break; 215 | } 216 | 217 | case 'hook': 218 | case 'hook end': { 219 | const hook = this.findHookById(data.id); 220 | assert(hook, `Couldn't find hook by id: ${data.id}`); 221 | 222 | this.emit(event, hook); 223 | break; 224 | } 225 | 226 | default: 227 | throw new Error(`Unknown event: ${event}`); 228 | } 229 | } else { 230 | const { data, type } = subprocessEvent; 231 | process[type].write(data); 232 | } 233 | } 234 | } 235 | 236 | private isRunnerMessage(message: SubprocessMessage): message is SubprocessRunnerMessage { 237 | return message.type === 'runner'; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/main/task-manager.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { EventEmitter } from 'events'; 3 | import { cpus } from 'os'; 4 | 5 | export type Task = () => Promise; 6 | export interface TaskOutput { 7 | task: Task; 8 | output?: T; 9 | } 10 | 11 | 12 | export default class TaskManager extends EventEmitter { 13 | private maxParallel: number; 14 | private tasks: TaskOutput[] = []; 15 | private remainingTasks = new Set>(); 16 | private processingTasks = new Set>(); 17 | 18 | constructor(maxParallel: number = cpus().length) { 19 | super(); 20 | 21 | this.maxParallel = maxParallel || Number.POSITIVE_INFINITY; 22 | this.on('processingFinished', this.onTaskProcessingFinished); 23 | } 24 | 25 | add(task: Task): void { 26 | this.tasks.push({ task }); 27 | this.remainingTasks.add(task); 28 | } 29 | 30 | execute() { 31 | for (const task of this.remainingTasks) { 32 | this.startTaskProcessing(task); 33 | 34 | if (this.processingTasks.size >= this.maxParallel) { 35 | break; 36 | } 37 | } 38 | } 39 | 40 | private onTaskProcessingFinished = (finishedTask: Task, output: TaskResult) => { 41 | assert(!this.remainingTasks.has(finishedTask), 'Unknown task, cannot finalize it'); 42 | assert(this.processingTasks.has(finishedTask), 'Task has not been started processing'); 43 | 44 | const taskIndex = this.tasks.findIndex(({ task }) => task === finishedTask); 45 | assert(taskIndex !== -1, 'Unknown task, cannot write its output'); 46 | 47 | this.tasks[taskIndex].output = output; 48 | this.processingTasks.delete(finishedTask); 49 | 50 | this.emit('taskFinished', output); 51 | 52 | this.execute(); 53 | this.emitEndIfAllFinished(); 54 | } 55 | 56 | private async startTaskProcessing(task: Task) { 57 | assert(this.remainingTasks.has(task), 'Unknown task, cannot process it'); 58 | assert(!this.processingTasks.has(task), 'Task has already started processing'); 59 | 60 | this.remainingTasks.delete(task); 61 | this.processingTasks.add(task); 62 | 63 | const res = await task(); 64 | this.emit('processingFinished', task, res); 65 | } 66 | 67 | private emitEndIfAllFinished() { 68 | const shouldEmit = this.tasks.every(({ output }) => output !== undefined); 69 | 70 | if (shouldEmit) { 71 | this.emit('end'); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/thread.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import { Debugger } from 'debug'; 3 | 4 | import { ProcessThread } from './thread/process'; 5 | import { Thread, ThreadOptions, supportsWorkerThreads } from '../thread'; 6 | 7 | export type ThreadFactory = (file: string, options: ThreadOptions) => Thread; 8 | 9 | const getWorkerThread = (file: string, debugLog: Debugger, options: ThreadOptions) => { 10 | // we can't ES6-import this because older versions of node will throw exceptions 11 | // eslint-disable-next-line @typescript-eslint/no-var-requires 12 | const { WorkerThread } = require('./thread/worker'); 13 | return new WorkerThread(file, debugLog, options) as Thread; 14 | } 15 | 16 | export const getThread: ThreadFactory = (file, options) => { 17 | return supportsWorkerThreads() 18 | ? getWorkerThread(file, debug('mocha-parallel-tests:worker-thread'), options) 19 | : new ProcessThread(file, debug('mocha-parallel-tests:worker-thread'), options); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/thread/process.ts: -------------------------------------------------------------------------------- 1 | import { fork } from 'child_process'; 2 | import { Debugger } from 'debug'; 3 | import { resolve } from 'path'; 4 | 5 | import { SubprocessMessage, Thread, ThreadOptions } from '../../thread'; 6 | import { SubprocessResult, SubprocessSyncedData, SubprocessRunnerMessage, SubprocessOutputMessage, InterProcessMessage, isSyncSnapshot, isOverwrittenStandardStreamMessage } from '../../message-channel'; 7 | import { removeDebugArgs } from '../util'; 8 | import { SUITE_OWN_OPTIONS } from '../../config'; 9 | 10 | export class ProcessThread implements Thread { 11 | private file: string; 12 | private log: Debugger; 13 | private options: ThreadOptions; 14 | private events: SubprocessMessage[] = []; 15 | private startedAt: number | undefined; 16 | private syncedSubprocessData: SubprocessSyncedData | undefined; 17 | 18 | constructor(file: string, log: Debugger, options: ThreadOptions) { 19 | this.file = file; 20 | this.log = log; 21 | this.options = options; 22 | } 23 | 24 | run() { 25 | const extension = this.options.isTypescriptRunMode ? 'ts' : 'js'; 26 | const runnerPath = resolve(__dirname, `../../subprocess/cli.${extension}`); 27 | 28 | this.startedAt = Date.now(); 29 | 30 | return new Promise((resolve, reject) => { 31 | const test = fork(runnerPath, this.buildForkArgs(), { 32 | // otherwise `--inspect-brk` and other params will be passed to subprocess 33 | execArgv: process.execArgv.filter(removeDebugArgs), 34 | stdio: ['ipc'], 35 | }); 36 | 37 | if (!test.stdout || !test.stderr) { 38 | reject(new Error('Could not find standard streams for forked process')); 39 | return; 40 | } 41 | 42 | test.on('message', this.onMessage); 43 | test.stdout.on('data', this.onStdout); 44 | test.stderr.on('data', this.onStderr); 45 | test.on('close', this.onClose(resolve)); 46 | }); 47 | } 48 | 49 | private buildForkArgs(): string[] { 50 | const forkArgs: string[] = ['--test', resolve(this.file)]; 51 | 52 | for (const option of SUITE_OWN_OPTIONS) { 53 | // bail is undefined by default, we need to somehow pass its value to the subprocess 54 | const propValue = this.options[option]; 55 | forkArgs.push(`--${option}`, propValue === undefined ? false : propValue); 56 | } 57 | 58 | for (const requirePath of this.options.requires) { 59 | forkArgs.push('--require', requirePath); 60 | } 61 | 62 | for (const compilerPath of this.options.compilers) { 63 | forkArgs.push('--compilers', compilerPath); 64 | } 65 | 66 | if (this.options.delay) { 67 | forkArgs.push('--delay'); 68 | } 69 | 70 | if (this.options.grep) { 71 | forkArgs.push('--grep', this.options.grep); 72 | } 73 | 74 | if (this.options.exitImmediately) { 75 | forkArgs.push('--exit'); 76 | } 77 | 78 | if (this.options.fullTrace) { 79 | forkArgs.push('--full-trace'); 80 | } 81 | 82 | return forkArgs; 83 | } 84 | 85 | private onMessage = (message: InterProcessMessage) => { 86 | if (isSyncSnapshot(message)) { 87 | this.syncedSubprocessData = message.data; 88 | } else if (!isOverwrittenStandardStreamMessage(message)) { 89 | const runnerEvent: SubprocessRunnerMessage = { 90 | data: message.data, 91 | event: message.event, 92 | type: 'runner', 93 | }; 94 | 95 | this.events.push(runnerEvent); 96 | } 97 | } 98 | 99 | private onStdout = (data: Buffer) => { 100 | const outputEvent: SubprocessOutputMessage = { 101 | data, 102 | type: 'stdout', 103 | }; 104 | 105 | this.events.push(outputEvent); 106 | } 107 | 108 | private onStderr = (data: Buffer) => { 109 | const outputEvent: SubprocessOutputMessage = { 110 | data, 111 | type: 'stderr', 112 | }; 113 | 114 | this.events.push(outputEvent); 115 | } 116 | 117 | private onClose = (resolve: (data: SubprocessResult) => void) => (code: number) => { 118 | this.log(`Process for ${this.file} exited with code ${code}`); 119 | 120 | if (!this.startedAt) { 121 | throw new Error('Attempt to close a thread which hasn\'t been started yet'); 122 | } 123 | 124 | resolve({ 125 | code, 126 | events: this.events, 127 | execTime: Date.now() - this.startedAt, 128 | file: this.file, 129 | syncedSubprocessData: this.syncedSubprocessData, 130 | }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/thread/worker.ts: -------------------------------------------------------------------------------- 1 | import { Debugger } from 'debug'; 2 | import { Worker } from 'worker_threads'; 3 | import { resolve } from 'path'; 4 | 5 | import { SubprocessMessage, Thread, ThreadOptions } from '../../thread'; 6 | import { SubprocessResult, SubprocessSyncedData, SubprocessRunnerMessage, SubprocessOutputMessage, InterProcessMessage, isSyncSnapshot, isOverwrittenStandardStreamMessage } from '../../message-channel'; 7 | import { removeDebugArgs } from '../util'; 8 | 9 | export interface WorkerData { 10 | file: string; 11 | options: ThreadOptions; 12 | } 13 | 14 | export class WorkerThread implements Thread { 15 | private file: string; 16 | private log: Debugger; 17 | private options: ThreadOptions; 18 | private events: SubprocessMessage[] = []; 19 | private startedAt: number | undefined; 20 | private syncedSubprocessData: SubprocessSyncedData | undefined; 21 | 22 | constructor(file: string, log: Debugger, options: ThreadOptions) { 23 | this.file = file; 24 | this.log = log; 25 | this.options = options; 26 | } 27 | 28 | run() { 29 | const workerFilename = this.options.isTypescriptRunMode ? 'worker.development.js' : 'worker.js'; 30 | const workerPath = resolve(__dirname, `../../subprocess/thread/${workerFilename}`); 31 | 32 | this.startedAt = Date.now(); 33 | 34 | return new Promise((resolve, reject) => { 35 | const worker = new Worker(workerPath, { 36 | execArgv: process.execArgv.filter(removeDebugArgs), 37 | stderr: true, 38 | stdout: true, 39 | workerData: this.buildWorkerData(), 40 | }); 41 | 42 | // it's unsafe to listen to stderr/stdout messages from the worker thread 43 | // because they are asynchronous (process.stdout.isTTY = False) 44 | // worker.stderr.on('data', this.onStderr); 45 | // worker.stdout.on('data', this.onStdout); 46 | 47 | worker.on('message', this.onMessage); 48 | worker.on('error', this.onError(reject)); 49 | worker.on('exit', this.onExit(resolve)); 50 | }); 51 | } 52 | 53 | private buildWorkerData(): WorkerData { 54 | return { 55 | file: resolve(this.file), 56 | options: this.options, 57 | }; 58 | } 59 | 60 | private onMessage = (message: InterProcessMessage) => { 61 | if (isOverwrittenStandardStreamMessage(message)) { 62 | const { data, stream } = message; 63 | 64 | if (stream === 'stdout') { 65 | this.onStdout(Buffer.from(data)); 66 | } else if (stream === 'stderr') { 67 | this.onStderr(Buffer.from(data)); 68 | } 69 | 70 | return; 71 | } 72 | 73 | if (isSyncSnapshot(message)) { 74 | this.syncedSubprocessData = message.data; 75 | } else { 76 | const runnerEvent: SubprocessRunnerMessage = { 77 | data: message.data, 78 | event: message.event, 79 | type: 'runner', 80 | }; 81 | 82 | this.events.push(runnerEvent); 83 | } 84 | } 85 | 86 | private onStdout = (data: Buffer) => { 87 | const outputEvent: SubprocessOutputMessage = { 88 | data, 89 | type: 'stdout', 90 | }; 91 | 92 | this.events.push(outputEvent); 93 | } 94 | 95 | private onStderr = (data: Buffer) => { 96 | const outputEvent: SubprocessOutputMessage = { 97 | data, 98 | type: 'stderr', 99 | }; 100 | 101 | this.events.push(outputEvent); 102 | } 103 | 104 | private onError = (reject: (err: Error) => void) => (err: Error) => { 105 | this.log(`Error occured in subprocess: ${err.stack}`); 106 | reject(err); 107 | } 108 | 109 | private onExit = (resolve: (data: SubprocessResult) => void) => (code: number) => { 110 | this.log(`Process for ${this.file} exited with code ${code}`); 111 | 112 | if (!this.startedAt) { 113 | throw new Error('Attempt to close a thread which hasn\'t been started yet'); 114 | } 115 | 116 | resolve({ 117 | code, 118 | events: this.events, 119 | execTime: Date.now() - this.startedAt, 120 | file: this.file, 121 | syncedSubprocessData: this.syncedSubprocessData, 122 | }); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | Hook, 4 | Suite, 5 | Test, 6 | } from 'mocha'; 7 | 8 | const DEBUG_CLI_ARGS = ['--inspect', '--debug', '--debug-brk', '--inspect-brk']; 9 | const noop = () => null; 10 | 11 | export function removeDebugArgs(arg: string): boolean { 12 | return !DEBUG_CLI_ARGS.includes(arg); 13 | } 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | export function subprocessParseReviver(_: string, value: any): any { 17 | if (typeof value !== 'object' || value === null) { 18 | return value; 19 | } 20 | 21 | if (value.type === 'test') { 22 | const test = new Test(value.title, noop); 23 | 24 | // mimic test.fn as much as we can 25 | Object.assign(test, value); 26 | if (test.fn) { 27 | test.fn.toString = () => value.body; 28 | } 29 | 30 | return test; 31 | } 32 | 33 | if (value.type === 'hook') { 34 | const hook = new Hook(value.title, noop); 35 | return Object.assign(hook, value); 36 | } 37 | 38 | if (Array.isArray(value.suites)) { 39 | const ctx = new Context(); 40 | const suite = new Suite(value.title, ctx); 41 | 42 | return Object.assign(suite, value); 43 | } 44 | 45 | return value; 46 | } 47 | -------------------------------------------------------------------------------- /src/message-channel.ts: -------------------------------------------------------------------------------- 1 | export interface Snapshot { 2 | data: { 3 | results: string; 4 | retries: string; 5 | }; 6 | event: 'sync'; 7 | } 8 | 9 | export interface ReporterSimpleEvent { 10 | id: string; 11 | } 12 | 13 | export interface ReporterErrorEvent { 14 | id: string; 15 | err: { 16 | message: string; 17 | name: string; 18 | stack?: string; 19 | }; 20 | } 21 | 22 | export type ReporterEvent = ReporterSimpleEvent | ReporterErrorEvent | {}; 23 | 24 | export interface ReporterNotification { 25 | event: string; 26 | data: ReporterEvent; 27 | } 28 | 29 | export interface OverwrittenStandardStreamMessage { 30 | stream: 'stderr' | 'stdout'; 31 | data: string; 32 | } 33 | 34 | export type InterProcessMessage = Snapshot | ReporterNotification | OverwrittenStandardStreamMessage; 35 | 36 | export interface SubprocessRunnerMessage { 37 | data: ReporterEvent; 38 | event: string; 39 | type: 'runner'; 40 | } 41 | 42 | export interface SubprocessOutputMessage { 43 | data: Buffer; 44 | type: 'stdout' | 'stderr'; 45 | } 46 | 47 | export interface SubprocessSyncedData { 48 | results: string; 49 | retries: string; 50 | } 51 | 52 | export type SubprocessMessage = SubprocessRunnerMessage | SubprocessOutputMessage; 53 | 54 | export interface SubprocessResult { 55 | code: number; 56 | file: string; 57 | events: SubprocessMessage[]; 58 | execTime: number; 59 | syncedSubprocessData?: SubprocessSyncedData; 60 | } 61 | 62 | export function isSyncSnapshot(message: InterProcessMessage): message is Snapshot { 63 | return 'event' in message && message.event === 'sync'; 64 | } 65 | 66 | export function isOverwrittenStandardStreamMessage(message: InterProcessMessage): message is OverwrittenStandardStreamMessage { 67 | return 'stream' in message; 68 | } 69 | 70 | export function isEventWithId(event: ReporterEvent): event is (ReporterErrorEvent | ReporterSimpleEvent) { 71 | return 'id' in event; 72 | } 73 | 74 | export function isErrorEvent(event: ReporterEvent): event is ReporterErrorEvent { 75 | return 'err' in event; 76 | } 77 | -------------------------------------------------------------------------------- /src/mocha.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Hook as MochaHook, 3 | Suite as MochaSuite, 4 | Test as MochaTest, 5 | } from 'mocha'; 6 | 7 | import { RUNNABLE_MESSAGE_CHANNEL_PROP, SUBPROCESS_RETRIED_SUITE_ID } from './config'; 8 | 9 | export interface MochaParallelTestsRunnerObject { 10 | [RUNNABLE_MESSAGE_CHANNEL_PROP]: string; 11 | } 12 | 13 | export interface RetriedTest extends Test { 14 | [SUBPROCESS_RETRIED_SUITE_ID]: string; 15 | } 16 | 17 | export interface Hook extends MochaHook, MochaParallelTestsRunnerObject {} 18 | 19 | export interface Suite extends MochaSuite, MochaParallelTestsRunnerObject { 20 | suites: Suite[]; 21 | tests: Test[]; 22 | } 23 | 24 | export interface Test extends MochaTest, MochaParallelTestsRunnerObject {} 25 | -------------------------------------------------------------------------------- /src/subprocess/cli.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | 3 | import { DEBUG_SUBPROCESS } from '../config'; 4 | import { runMocha } from './runner'; 5 | import { ThreadOptions } from '../thread'; 6 | 7 | interface Args { 8 | bail?: boolean; 9 | compilers: string[]; 10 | delay?: boolean; 11 | grep?: string; 12 | enableTimeouts?: boolean; 13 | exit?: boolean; 14 | ['full-trace']?: boolean; 15 | require: string[]; 16 | retries?: number; 17 | slow?: boolean; 18 | timeout?: number; 19 | test: string; 20 | file: string[]; 21 | ui?: string; 22 | } 23 | 24 | function isDebugSubprocesss(argv: Args) { 25 | return argv[DEBUG_SUBPROCESS.yargs] as boolean; 26 | } 27 | 28 | function threadOptionsFromArgv(argv: Args): ThreadOptions { 29 | return { 30 | bail: argv.bail, 31 | compilers: argv.compilers, 32 | delay: argv.delay || false, 33 | enableTimeouts: argv.enableTimeouts, 34 | exitImmediately: argv.exit || false, 35 | fullTrace: argv['full-trace'] || false, 36 | grep: argv.grep, 37 | isTypescriptRunMode: isDebugSubprocesss(argv), 38 | requires: argv.require, 39 | retries: argv.retries, 40 | slow: argv.slow, 41 | timeout: argv.timeout, 42 | file: argv.file, 43 | ui: argv.ui 44 | }; 45 | } 46 | 47 | const argv: Args = yargs 48 | .boolean('bail') 49 | .option('compilers', { 50 | array: true, 51 | default: [], 52 | }) 53 | .boolean('delay') 54 | .string('grep') 55 | .boolean('enableTimeouts') 56 | .option('exit', { 57 | boolean: true, 58 | }) 59 | .option('full-trace', { 60 | boolean: true, 61 | }) 62 | .boolean('slow') 63 | .option('test', { 64 | demandOption: true, 65 | string: true, 66 | }) 67 | .option('require', { 68 | array: true, 69 | default: [], 70 | }) 71 | .option('file', { 72 | array: true, 73 | default: [] 74 | }) 75 | .number('retries') 76 | .number('timeout') 77 | .option('ui', { 78 | string: true, 79 | }) 80 | .parse(process.argv); 81 | 82 | const debugSubprocess = isDebugSubprocesss(argv); 83 | const options = threadOptionsFromArgv(argv); 84 | 85 | runMocha(argv.test, options, debugSubprocess); 86 | -------------------------------------------------------------------------------- /src/subprocess/message-channel.ts: -------------------------------------------------------------------------------- 1 | import { supportsWorkerThreads } from '../thread'; 2 | import { MessagePort } from 'worker_threads'; 3 | import { InterProcessMessage, OverwrittenStandardStreamMessage } from '../message-channel'; 4 | 5 | type WriteableStreamType = 'stderr' | 'stdout'; 6 | 7 | export default class MessageChannel { 8 | private handlesRunning = 0; 9 | private callbackRunOnExhausted: () => void; 10 | 11 | constructor() { 12 | if (supportsWorkerThreads()) { 13 | // stdout/stderr messages and worker thread messages are not synchronised 14 | // this means that we can't rely on worker.stdout stream 15 | process.stdout.write = this.overrideStdStream('stdout'); 16 | process.stderr.write = this.overrideStdStream('stderr'); 17 | } 18 | } 19 | 20 | sendEnsureDelivered(message: InterProcessMessage): void { 21 | this.handlesRunning += 1; 22 | this.sendToParent(message); 23 | } 24 | 25 | runOnExhausted(cb: () => void): void { 26 | if (this.handlesRunning) { 27 | this.callbackRunOnExhausted = cb; 28 | } else { 29 | cb(); 30 | } 31 | } 32 | 33 | private getParentPort(): MessagePort { 34 | // eslint-disable-next-line @typescript-eslint/no-var-requires 35 | const { parentPort } = require('worker_threads'); 36 | if (!parentPort) { 37 | throw new Error('Parent port is not available'); 38 | } 39 | 40 | return parentPort; 41 | } 42 | 43 | private onHandleFinished = (): void => { 44 | this.handlesRunning -= 1; 45 | 46 | if (this.handlesRunning === 0 && this.callbackRunOnExhausted) { 47 | this.callbackRunOnExhausted(); 48 | } 49 | } 50 | 51 | private sendToParent(message: InterProcessMessage) { 52 | if (supportsWorkerThreads()) { 53 | const parentPort = this.getParentPort(); 54 | parentPort.postMessage(message); 55 | 56 | this.onHandleFinished(); 57 | } else { 58 | if (!process.send) { 59 | throw new Error('IPC is not available'); 60 | } 61 | 62 | process.send(message, this.onHandleFinished); 63 | } 64 | } 65 | 66 | private overrideStdStream(stream: WriteableStreamType) { 67 | const originalWrite = process[stream].write.bind(process.stdout); 68 | 69 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 70 | const overrideCallback = (data: string, ...args: any[]) => { 71 | const parentPort = this.getParentPort(); 72 | const message: OverwrittenStandardStreamMessage = { stream, data }; 73 | parentPort.postMessage(message); 74 | 75 | return originalWrite(data, ...args); 76 | }; 77 | 78 | return overrideCallback; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/subprocess/options/exit.ts: -------------------------------------------------------------------------------- 1 | import MessageChannel from '../message-channel'; 2 | 3 | function exitLater(code) { 4 | process.on('exit', function onExit() { 5 | process.exit(Math.min(code, 255)); 6 | }); 7 | } 8 | 9 | const exit = (channel: MessageChannel) => (code: number) => { 10 | const clampedCode = Math.min(code, 255); 11 | 12 | // that's what mocha does 13 | console.log(''); // eslint-disable-line no-console 14 | console.error(''); // eslint-disable-line no-console 15 | 16 | // wait until all RUNNABLE_MESSAGE_CHANNEL_PROP messages are sent to the main process 17 | channel.runOnExhausted(() => { 18 | process.exit(clampedCode); 19 | }); 20 | }; 21 | 22 | export default function applyExit(channel: MessageChannel, shouldExitImmediately: boolean) { 23 | return shouldExitImmediately ? exit(channel) : exitLater; 24 | } 25 | -------------------------------------------------------------------------------- /src/subprocess/options/full-trace.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | export default function applyFullTrace(mocha: Mocha, fullTrace?: boolean) { 4 | if (fullTrace) { 5 | mocha.options.fullStackTrace = true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/subprocess/reporter.ts: -------------------------------------------------------------------------------- 1 | import { Runner } from 'mocha'; 2 | import CircularJSON from 'circular-json'; 3 | 4 | import { Test, Suite, Hook } from '../mocha'; 5 | import { getMessageId } from './util'; 6 | import { RUNNABLE_MESSAGE_CHANNEL_PROP, SUBPROCESS_RETRIED_SUITE_ID } from '../config'; 7 | import MessageChannel from './message-channel'; 8 | import { Snapshot, ReporterNotification } from '../message-channel'; 9 | 10 | export interface ReporterConstructor { 11 | new(runner: Runner); 12 | } 13 | 14 | export type ReporterFactory = (channel: MessageChannel, debugSubprocess: boolean) => ReporterConstructor; 15 | 16 | export const getReporterFactory: ReporterFactory = (channel, debugSubprocess) => { 17 | return class Reporter { 18 | /** 19 | * If `--retries N` option is specified runner can emit `test` events 20 | * multiple times for retried test cases. These test cases do not exist 21 | * if the final root suite structure, so we need to store them and return 22 | * to the main process after the end 23 | */ 24 | private runningTests = new Set(); 25 | private rootSuite: Suite; 26 | private currentTestIndex: number | null = null; 27 | private eventsCounter = 0; 28 | 29 | constructor(runner: Runner) { 30 | this.rootSuite = runner.suite as Suite; 31 | 32 | runner.on('waiting', this.onRunnerWaiting); 33 | runner.on('start', this.onRunnerStart); 34 | runner.on('end', this.onRunnerEnd); 35 | 36 | runner.on('suite', this.onRunnerSuTestart); 37 | runner.on('suite end', this.onRunnerSuiteEnd); 38 | 39 | runner.on('test', this.onTestStart); 40 | runner.on('test end', this.onTestEnd); 41 | 42 | runner.on('pass', this.onRunnerPass); 43 | runner.on('fail', this.onRunnerFail); 44 | runner.on('pending', this.onRunnerPending); 45 | 46 | runner.on('hook', this.onRunnerHookStart); 47 | runner.on('hook end', this.onRunnerHookEnd); 48 | } 49 | 50 | private onRunnerStart = () => { 51 | this.notifyParent('start'); 52 | } 53 | 54 | private onRunnerEnd = () => { 55 | this.notifyParent('end'); 56 | } 57 | 58 | private onRunnerSuTestart = (suite: Suite) => { 59 | const title = suite.root ? 'root' : suite.fullTitle(); 60 | const id = getMessageId('suite', title, this.eventsCounter); 61 | suite[RUNNABLE_MESSAGE_CHANNEL_PROP] = id; 62 | 63 | this.notifyParent('suite', { id }); 64 | this.eventsCounter += 1; 65 | } 66 | 67 | private onRunnerSuiteEnd = (suite: Suite) => { 68 | this.notifyParent('suite end', { 69 | id: suite[RUNNABLE_MESSAGE_CHANNEL_PROP], 70 | }); 71 | } 72 | 73 | private onRunnerWaiting = (/* rootSuite: Suite */) => { 74 | this.notifyParent('waiting'); 75 | } 76 | 77 | private onTestStart = (test: Test) => { 78 | const id = getMessageId('test', test.fullTitle(), this.eventsCounter); 79 | test[RUNNABLE_MESSAGE_CHANNEL_PROP] = id; 80 | 81 | if (!test.parent) { 82 | throw new Error('Could not find a parent for the current test'); 83 | } 84 | 85 | // this test is running for the first time, i.e. no retries for it have been executed yet 86 | if (this.currentTestIndex === null) { 87 | const currentTestIndex = test.parent.tests.indexOf(test); 88 | if (currentTestIndex === -1) { 89 | throw new Error('Could not find the test in the suite\'s tests'); 90 | } 91 | 92 | this.currentTestIndex = currentTestIndex; 93 | } else if (!test.parent.tests.includes(test)) { 94 | /** 95 | * When mocha runs tests with `--retries` option there's a specific behaviour for events order: 96 | * If the test fails and `--retries` = 1, mocha emits `test`, `test`, `fail` and `test end`. 97 | * This means that mocha doesn't emit the "test end" event and instead just re-emits the test. 98 | * The issue is that the last test in the currently running suite refers to the previously run test. 99 | * The fix for us here is to "fix" the mocha old pointer by replacing the failed test with a new one. 100 | * NB: This may be a mocha issue 101 | */ 102 | test.parent.tests[this.currentTestIndex] = test; 103 | } 104 | 105 | this.runningTests.add(test); 106 | 107 | this.notifyParent('test', { id }); 108 | this.eventsCounter += 1; 109 | } 110 | 111 | private onTestEnd = (test: Test) => { 112 | this.runningTests.delete(test); 113 | this.currentTestIndex = null; 114 | 115 | this.notifyParent('test end', { 116 | id: test[RUNNABLE_MESSAGE_CHANNEL_PROP], 117 | }); 118 | } 119 | 120 | private onRunnerPass = (test: Test) => { 121 | this.notifyParent('pass', { 122 | id: test[RUNNABLE_MESSAGE_CHANNEL_PROP], 123 | }); 124 | } 125 | 126 | private onRunnerFail = (test: Test, err: Error) => { 127 | this.notifyParent('fail', { 128 | err: { 129 | message: err.message, 130 | name: err.name, 131 | stack: err.stack, 132 | }, 133 | id: test[RUNNABLE_MESSAGE_CHANNEL_PROP], 134 | }); 135 | } 136 | 137 | private onRunnerPending = (test: Test) => { 138 | this.notifyParent('pending', { 139 | id: test[RUNNABLE_MESSAGE_CHANNEL_PROP], 140 | }); 141 | } 142 | 143 | private onRunnerHookStart = (hook: Hook) => { 144 | const id = hook[RUNNABLE_MESSAGE_CHANNEL_PROP] || getMessageId('hook', hook.title, this.eventsCounter); 145 | hook[RUNNABLE_MESSAGE_CHANNEL_PROP] = id; 146 | 147 | this.notifyParent('hook', { id }); 148 | this.eventsCounter += 1; 149 | } 150 | 151 | private onRunnerHookEnd = (hook: Hook) => { 152 | this.notifyParent('hook end', { 153 | id: hook[RUNNABLE_MESSAGE_CHANNEL_PROP], 154 | }); 155 | } 156 | 157 | private notifyParent(event: string, data = {}) { 158 | if (debugSubprocess) { 159 | // eslint-disable-next-line no-console 160 | console.log({ event, data }); 161 | } else { 162 | this.notifyParentThroughIPC(event, data); 163 | } 164 | } 165 | 166 | private notifyParentThroughIPC(event: string, data = {}) { 167 | // main process needs retried tests only when it starts 168 | // re-emitting subprocess test results, so it's safe to 169 | // omit them until the "end" event 170 | const retriesTests = event === 'end' 171 | ? [...this.runningTests].map((test) => { 172 | if (!test.parent) { 173 | throw new Error('Could not find a parent for the current test'); 174 | } 175 | 176 | return Object.assign({}, test, { 177 | [SUBPROCESS_RETRIED_SUITE_ID]: test.parent[RUNNABLE_MESSAGE_CHANNEL_PROP], 178 | parent: null, 179 | }); 180 | }) 181 | : []; 182 | 183 | // send the data snapshot with every event 184 | const snapshot: Snapshot = { 185 | data: { 186 | // can't use the root suite because it will not get revived in the master process 187 | // @see https://github.com/WebReflection/circular-json/issues/44 188 | results: CircularJSON.stringify({ rootSuite: this.rootSuite }), 189 | retries: CircularJSON.stringify({ retriesTests }), 190 | }, 191 | event: 'sync', 192 | }; 193 | 194 | channel.sendEnsureDelivered(snapshot); 195 | 196 | // and then send the event 197 | const reporterNotification: ReporterNotification = { event, data } 198 | channel.sendEnsureDelivered(reporterNotification); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/subprocess/runner.ts: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | 3 | import MessageChannel from './message-channel'; 4 | import { getReporterFactory } from './reporter'; 5 | import { applyCompilers, applyDelay, applyGrepPattern, applyNoTimeouts, applyRequires, applyTimeouts, applyFiles, applyUi } from '../util'; 6 | import applyExit from './options/exit'; 7 | import applyFullTrace from './options/full-trace'; 8 | import { SUITE_OWN_OPTIONS } from '../config'; 9 | import { ThreadOptions } from '../thread'; 10 | 11 | export function runMocha(file: string, options: ThreadOptions, debugSubprocess: boolean) { 12 | const channel = new MessageChannel(); 13 | const Reporter = getReporterFactory(channel, debugSubprocess); 14 | 15 | const mocha = new Mocha(); 16 | mocha.addFile(file); 17 | 18 | // --compilers 19 | applyCompilers(options.compilers); 20 | 21 | // --delay 22 | applyDelay(mocha, options.delay); 23 | 24 | // --grep 25 | applyGrepPattern(mocha, options.grep); 26 | 27 | // --enableTimeouts 28 | applyNoTimeouts(mocha, options.enableTimeouts); 29 | 30 | // --exit 31 | const onComplete = applyExit(channel, options.exitImmediately); 32 | 33 | // --file 34 | applyFiles(mocha, options.file); 35 | 36 | // --require 37 | applyRequires(options.requires); 38 | 39 | // --timeout 40 | applyTimeouts(mocha, options.timeout); 41 | 42 | // --full-trace 43 | applyFullTrace(mocha, options.fullTrace); 44 | 45 | // --ui 46 | applyUi(mocha, options.ui); 47 | 48 | // apply main process root suite properties 49 | for (const option of SUITE_OWN_OPTIONS) { 50 | const suiteProp = `_${option}`; 51 | mocha.suite[suiteProp] = options[option]; 52 | } 53 | 54 | mocha.reporter(Reporter).run(onComplete); 55 | } 56 | -------------------------------------------------------------------------------- /src/subprocess/thread/worker.development.js: -------------------------------------------------------------------------------- 1 | // import { register as registerTypescriptHandler } from 'ts-node'; 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const { register: registerTypescriptHandler } = require('ts-node'); 4 | 5 | registerTypescriptHandler(); 6 | require(`${__dirname}/worker.ts`); 7 | -------------------------------------------------------------------------------- /src/subprocess/thread/worker.ts: -------------------------------------------------------------------------------- 1 | // monkeypatch Node.JS native TTY function 2 | // otherwise mocha native base reporter throws exception 3 | // inside a worker environment 4 | require('tty').getWindowSize = () => 75; 5 | 6 | import { workerData } from 'worker_threads'; 7 | import { runMocha } from '../runner'; 8 | import { WorkerData } from '../../main/thread/worker'; 9 | 10 | const { file, options } = workerData as WorkerData; 11 | runMocha(file, options, false); 12 | -------------------------------------------------------------------------------- /src/subprocess/util.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | 3 | export type EntityType = 'suite' | 'test' | 'hook'; 4 | 5 | function cleanFullTitle(title: string): string { 6 | return title 7 | .replace(/[^\w]+/g, '') 8 | .trim() 9 | .toLowerCase() || 'notitle'; 10 | } 11 | 12 | export function getMessageId(entityType: EntityType, entityTitle: string, eventCounter: number) { 13 | return `${entityType}_${cleanFullTitle(entityTitle)}:${v4().substr(0, 8)}_${eventCounter}`; 14 | } 15 | -------------------------------------------------------------------------------- /src/subprocess/worker.ts: -------------------------------------------------------------------------------- 1 | import { workerData } from 'worker_threads'; 2 | 3 | import { runMocha } from './runner'; 4 | import { WorkerData } from '../main/thread/worker'; 5 | 6 | const { file, options } = workerData as WorkerData; 7 | runMocha(file, options, false); 8 | -------------------------------------------------------------------------------- /src/thread.ts: -------------------------------------------------------------------------------- 1 | import { SubprocessResult, SubprocessOutputMessage, SubprocessRunnerMessage } from './message-channel'; 2 | 3 | export type ListenerMessage = (message: Buffer) => void; 4 | export type ListenerStandardStream = (message: Buffer) => void; 5 | export type ExitCode = number; 6 | 7 | export interface ThreadOptions { 8 | bail?: boolean; 9 | compilers: string[]; 10 | delay: boolean; 11 | enableTimeouts?: boolean; 12 | exitImmediately: boolean; 13 | fullTrace: boolean; 14 | grep?: string; 15 | isTypescriptRunMode: boolean; 16 | file: string[]; 17 | requires: string[]; 18 | retries?: number; 19 | slow?: boolean; 20 | timeout?: number; 21 | ui?: string; 22 | } 23 | 24 | export interface Thread { 25 | run(): Promise; 26 | } 27 | 28 | export type SubprocessMessage = SubprocessOutputMessage | SubprocessRunnerMessage; 29 | 30 | export function supportsWorkerThreads(): boolean { 31 | try { 32 | require('worker_threads'); 33 | return true; 34 | } catch (ex) { 35 | return false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { join, resolve } from 'path'; 3 | import Mocha from "mocha"; 4 | 5 | export interface CLICompilers { 6 | compilers: string[]; 7 | extensions: string[]; 8 | } 9 | 10 | export function setProcessExitListeners() { 11 | process.on('unhandledRejection', (reason) => { 12 | const message = reason && 'stack' in reason 13 | ? (reason as Error).stack 14 | : 'Unhandled asynchronous exception'; 15 | 16 | // eslint-disable-next-line no-console 17 | console.error(`Unhandled asynchronous exception: ${message}`); 18 | process.exit(1); 19 | }); 20 | 21 | process.on('uncaughtException', (err) => { 22 | // eslint-disable-next-line no-console 23 | console.error(`Uncaught exception: ${err.stack}`); 24 | process.exit(1); 25 | }); 26 | } 27 | 28 | export function applyFiles(mocha: Mocha, files: string | string[]) { 29 | const fileList: string[] = Array.isArray(files) ? files : [files]; 30 | 31 | if(fileList) { 32 | mocha.files = fileList.concat(mocha.files); 33 | } 34 | } 35 | 36 | export function applyRequires(requires: string | string[]): string[] { 37 | const requiresList: string[] = Array.isArray(requires) ? requires : [requires]; 38 | const output: string[] = []; 39 | 40 | // required file can be in the process CWD 41 | const cwd = process.cwd(); 42 | module.paths.push(cwd, join(cwd, 'node_modules')); 43 | 44 | for (const mod of requiresList) { 45 | const abs = existsSync(mod) || existsSync(`${mod}.js`); 46 | const requirePath = abs ? resolve(mod) : mod; 47 | 48 | require(requirePath); 49 | output.push(requirePath); 50 | } 51 | 52 | return output; 53 | } 54 | 55 | export function applyCompilers(compilers: string | string[]): CLICompilers { 56 | const compilersList: string[] = Array.isArray(compilers) ? compilers : [compilers]; 57 | const output: CLICompilers = { 58 | compilers: compilersList, 59 | extensions: ['js'], 60 | }; 61 | 62 | // required compiler can be in the process CWD 63 | const cwd = process.cwd(); 64 | module.paths.push(cwd, join(cwd, 'node_modules')); 65 | 66 | for (const compiler of compilersList) { 67 | const idx = compiler.indexOf(':'); 68 | const ext = compiler.slice(0, idx); 69 | let mod = compiler.slice(idx + 1); 70 | 71 | if (mod.startsWith('.')) { 72 | mod = join(process.cwd(), mod); 73 | } 74 | 75 | require(mod); 76 | output.extensions.push(ext); 77 | } 78 | 79 | return output; 80 | } 81 | 82 | export function applyDelay(mocha: Mocha, delay?: boolean) { 83 | if (delay) { 84 | mocha.delay(); 85 | } 86 | } 87 | 88 | export function applyGrepPattern(mocha: Mocha, stringPattern?: string) { 89 | if (stringPattern) { 90 | mocha.grep(stringPattern); 91 | } 92 | } 93 | 94 | export function applyNoTimeouts(mocha: Mocha, allowTimeouts?: boolean) { 95 | if (allowTimeouts === false) { 96 | mocha.enableTimeouts(false); 97 | } 98 | } 99 | 100 | export function applyTimeouts(mocha: Mocha, timeout?: number) { 101 | if (timeout !== undefined) { 102 | mocha.suite.timeout(timeout); 103 | } 104 | } 105 | 106 | export function applyUi(mocha: Mocha, ui?: string) { 107 | if(ui) { 108 | mocha.ui(ui); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "mocha": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": "2017", 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 4 16 | ], 17 | "linebreak-style": [ 18 | "error", 19 | "unix" 20 | ], 21 | "quotes": [ 22 | "error", 23 | "single" 24 | ], 25 | "semi": [ 26 | "error", 27 | "always" 28 | ], 29 | "no-console": 0 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /test/bail-and-retries/README: -------------------------------------------------------------------------------- 1 | Check that --bail and --retries option work well together 2 | -------------------------------------------------------------------------------- /test/bail-and-retries/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert'); 6 | const { resolve } = require('path'); 7 | const { exec } = require('child_process'); 8 | 9 | const libExecutable = resolve(__dirname, '../../dist/bin/cli.js'); 10 | const spec = resolve(__dirname, 'index.spec.js'); 11 | const reporter = resolve(__dirname, '../util/events-reporter.js'); 12 | 13 | const cleanOutput = (buf) => String(buf) 14 | .split('\n') 15 | .filter(Boolean); 16 | 17 | exec(`${libExecutable} -R ${reporter} --retries 2 --bail ${spec}`, { 18 | cwd: __dirname, 19 | }, (err, stdout) => { 20 | if (!err) { 21 | console.error('Process should have finished with an error'); 22 | process.exit(1); 23 | } 24 | 25 | const events = cleanOutput(stdout); 26 | const lastEvent = events.pop(); 27 | assert.strictEqual(lastEvent, 'end', `Last event is not end: ${lastEvent}`); 28 | }); 29 | -------------------------------------------------------------------------------- /test/bail-and-retries/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('suite', () => { 2 | it('case', () => { 3 | throw new Error('fail'); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/cli-target/README: -------------------------------------------------------------------------------- 1 | Check that CLI returns error is no target is specified 2 | -------------------------------------------------------------------------------- /test/cli-target/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js 2>&1) 4 | MPT_STATUS_CODE=$? 5 | 6 | node_modules/.bin/mocha 1>/dev/null 2>&1 7 | MOCHA_STATUS_CODE=$? 8 | 9 | if [ $MPT_STATUS_CODE -eq $MOCHA_STATUS_CODE ]; then 10 | if [[ $OUTPUT == *"No test files found"* ]]; then 11 | exit 0 12 | else 13 | echo "Unexpected CLI output: $OUTPUT" 14 | exit 1 15 | fi 16 | else 17 | echo "Exit codes differ: $MPT_STATUS_CODE (actual) vs $MOCHA_STATUS_CODE (expected)" 18 | exit 1 19 | fi 20 | -------------------------------------------------------------------------------- /test/console-log-inject/README: -------------------------------------------------------------------------------- 1 | Check that tests' console.logs do not interfere into other tests output 2 | -------------------------------------------------------------------------------- /test/console-log-inject/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var assert = require('assert'); 6 | var path = require('path'); 7 | var exec = require('child_process').exec; 8 | var mptExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 9 | 10 | const runTests = (libExecutable) => { 11 | return new Promise((resolve, reject) => { 12 | exec(`${libExecutable} --timeout 60000 --slow 30000 test/console-log-inject/tests`, { 13 | cwd: path.resolve(__dirname, '../../') 14 | }, function (err, stdout) { 15 | if (err) { 16 | reject(err); 17 | return; 18 | } 19 | 20 | const logs = stdout.toString().split('\n').reduce(function (logs, chunk) { 21 | chunk = chunk.trim(); 22 | 23 | if (/^suite\s#[\d]+/i.test(chunk)) { 24 | logs.push(chunk.toLowerCase()); 25 | } 26 | 27 | return logs; 28 | }, []); 29 | 30 | resolve(logs); 31 | }); 32 | }); 33 | }; 34 | 35 | Promise.all([ 36 | runTests(mptExecutable), 37 | runTests('mocha'), 38 | ]).then(([logsParallel, logsBasic]) => { 39 | assert.deepStrictEqual( 40 | logsParallel, 41 | logsBasic, 42 | `Logs are different. Parallel: ${JSON.stringify(logsParallel)}. Basic: ${JSON.stringify(logsBasic)}`, 43 | ); 44 | }).catch((ex) => { 45 | console.error(ex); 46 | process.exit(1); 47 | }); 48 | -------------------------------------------------------------------------------- /test/console-log-inject/tests/foo.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /test/console-log-inject/tests/parallel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Suite #1', function () { 4 | console.log('suite #1 log at the beginning'); 5 | 6 | it('should end in 3 seconds', function (done) { 7 | console.log('suite #1 test #1 log at the beginning'); 8 | 9 | setTimeout(function () { 10 | console.log('suite #1 test #1 log before end'); 11 | done(); 12 | }, 3000); 13 | }); 14 | 15 | console.log('suite #1 log at the end'); 16 | }); 17 | -------------------------------------------------------------------------------- /test/console-log-inject/tests/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Suite #2', function () { 4 | it('should end in 5 seconds', function (done) { 5 | console.log('suite #2 test #1 log at the beginning'); 6 | 7 | setTimeout(function () { 8 | console.log('suite #2 test #1 log before end'); 9 | done(); 10 | }, 5000); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/delay/README: -------------------------------------------------------------------------------- 1 | Support mocha --delay option 2 | -------------------------------------------------------------------------------- /test/delay/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert'); 6 | const { resolve } = require('path'); 7 | const { exec } = require('child_process'); 8 | const libExecutable = resolve(__dirname, '../../dist/bin/cli.js'); 9 | 10 | exec(`${libExecutable} --delay -R test/delay/reporter.js test/delay/index.spec.js`, { 11 | cwd: resolve(__dirname, '../../'), 12 | }, (err, stdout) => { 13 | if (err) { 14 | console.error(err); 15 | process.exit(1); 16 | } 17 | 18 | const runnerEvents = stdout 19 | .toString() 20 | .trim() 21 | .split('\n') 22 | .map((evtName) => evtName.trim()); 23 | 24 | assert.strictEqual(runnerEvents[0], 'start'); 25 | assert.strictEqual(runnerEvents[1], 'suite'); // main process root suite 26 | assert.strictEqual(runnerEvents[2], 'waiting'); // subprocess waiting event 27 | assert.strictEqual(runnerEvents[3], 'suite'); // subprocess root suite 28 | assert.strictEqual(runnerEvents[4], 'suite'); // subprocess top level suite 29 | assert(runnerEvents.includes('pass'), 'Test case was not executed'); 30 | }); 31 | -------------------------------------------------------------------------------- /test/delay/index.spec.js: -------------------------------------------------------------------------------- 1 | setTimeout(() => { 2 | describe('suite', () => { 3 | it('should finish immediately', () => {}); 4 | }); 5 | 6 | run(); 7 | }, 100); 8 | -------------------------------------------------------------------------------- /test/delay/reporter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('mocha').reporters.Base; 4 | 5 | class Reporter extends Base { 6 | constructor(runner) { 7 | super(runner); 8 | 9 | [ 10 | 'waiting', 11 | 'start', 12 | 'end', 13 | 'suite', 14 | 'suite end', 15 | 'test', 16 | 'test end', 17 | 'pass', 18 | 'fail', 19 | 'pending', 20 | 'hook', 21 | 'hook end', 22 | ].forEach((evtName) => { 23 | runner.on(evtName, this.onRunnerEventFired.bind(this, evtName)); 24 | }); 25 | } 26 | 27 | onRunnerEventFired(evtName) { 28 | console.log(evtName); 29 | } 30 | } 31 | 32 | exports = module.exports = Reporter; 33 | -------------------------------------------------------------------------------- /test/describe-inside-describe/README: -------------------------------------------------------------------------------- 1 | Check that tests not only run in parallel but also output their own results -------------------------------------------------------------------------------- /test/describe-inside-describe/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var assert = require('assert'); 4 | var path = require('path'); 5 | var exec = require('child_process').exec; 6 | var cheerio = require('cheerio'); 7 | var libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 8 | 9 | exec(libExecutable + ' -R doc --timeout 60000 --slow 30000 test/describe-inside-describe/tests', { 10 | cwd: path.resolve(__dirname, '../../') 11 | }, function (err, stderr) { 12 | var xmlReporterOutput = stderr.toString(); 13 | var $ = cheerio.load(xmlReporterOutput); 14 | var firstSuite = $('.suite .suite'); 15 | 16 | assert.strictEqual($('.suite').length, 5, 'Suites length is wrong'); 17 | assert.strictEqual(firstSuite.find('.suite').length, 1, 'Inner suites length is wrong'); 18 | }); 19 | -------------------------------------------------------------------------------- /test/describe-inside-describe/tests/parallel1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | 8 | it('should end in 1 second', function (done) { 9 | setTimeout(done, 1000); 10 | }); 11 | 12 | describe('Inner test suite', function () { 13 | it('should end now', function () {}); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/describe-inside-describe/tests/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should fail', function (done) { 5 | done(new Error('some error')); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/exit/README: -------------------------------------------------------------------------------- 1 | Support --exit option 2 | -------------------------------------------------------------------------------- /test/exit/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const { spawn } = require('child_process'); 5 | const { resolve } = require('path'); 6 | 7 | const libExecutable = resolve(__dirname, '../../dist/bin/cli.js'); 8 | const testPath = resolve(__dirname, 'index.spec.js'); 9 | const test = spawn(libExecutable, ['--exit', testPath]); 10 | 11 | const timeoutId = setTimeout(() => { 12 | test.removeListener('close', onTestFinished); 13 | test.kill('SIGTERM'); 14 | 15 | console.error('CLI process hangs :('); 16 | process.exit(1); 17 | }, 10000); 18 | 19 | const onTestFinished = (code) => { 20 | assert.strictEqual(code, 0, `CLI exit code is wrong: ${code}`); 21 | clearTimeout(timeoutId); 22 | }; 23 | 24 | test.on('close', onTestFinished); 25 | -------------------------------------------------------------------------------- /test/exit/index.spec.js: -------------------------------------------------------------------------------- 1 | setInterval(() => { 2 | // empty function which keeps the event loop running 3 | }, 1000); 4 | 5 | describe('suite', () => { 6 | it('case', () => { 7 | // pass 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/file/README: -------------------------------------------------------------------------------- 1 | Support mocha --file option 2 | -------------------------------------------------------------------------------- /test/file/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.log('>>>>> If we see this log then the file got loaded by the --file option'); 4 | -------------------------------------------------------------------------------- /test/file/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert'); 6 | const { resolve } = require('path'); 7 | const { exec } = require('child_process'); 8 | const libExecutable = resolve(__dirname, '../../dist/bin/cli.js'); 9 | 10 | exec(`${libExecutable} --file test/file/config.js -R test/util/events-reporter.js test/file/index.spec.js`, { 11 | cwd: resolve(__dirname, '../../'), 12 | }, (err, stdout) => { 13 | if (err) { 14 | console.error(err); 15 | process.exit(1); 16 | } 17 | 18 | const runnerEvents = stdout 19 | .toString() 20 | .trim() 21 | .split('\n') 22 | .map((evtName) => evtName.trim()); 23 | 24 | assert(runnerEvents.includes('>>>>> If we see this log then the file got loaded by the --file option')); 25 | }); 26 | -------------------------------------------------------------------------------- /test/file/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('suite', () => { 2 | it('should finish immediately', () => {}); 3 | }); 4 | -------------------------------------------------------------------------------- /test/full-trace/README: -------------------------------------------------------------------------------- 1 | --full-trace option is supported 2 | -------------------------------------------------------------------------------- /test/full-trace/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var assert = require('assert'); 4 | var path = require('path'); 5 | var libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 6 | const {exec, execSync} = require('child_process'); 7 | 8 | function getMajorMochaVersion() { 9 | const mochaExec = path.resolve(__dirname, '../../node_modules/.bin/mocha'); 10 | const version = execSync(`${mochaExec} --version`).toString('utf-8').trim(); 11 | 12 | return Number(version.split('.')[0]); 13 | } 14 | 15 | function getMajorNodeVersion() { 16 | return Number(process.versions.node.split('.')[0]); 17 | } 18 | 19 | function main() { 20 | const majorNodeVersion = getMajorNodeVersion(); 21 | const majorMochaVersion = getMajorMochaVersion(); 22 | const shouldProduceFullTrace = !(majorMochaVersion >= 7 && majorNodeVersion < 12); 23 | 24 | if (!shouldProduceFullTrace) { 25 | console.warn(`This test does not work on this environment: ${JSON.stringify({ majorNodeVersion, majorMochaVersion })}`); 26 | return; 27 | } 28 | 29 | exec(libExecutable + ' --full-trace --reporter json test/full-trace/index.spec.js', { 30 | cwd: path.resolve(__dirname, '../../') 31 | }, function (err, stderr) { 32 | var jsonReporterOutput = stderr.toString(); 33 | 34 | try { 35 | jsonReporterOutput = JSON.parse(jsonReporterOutput); 36 | } catch (ex) { 37 | console.error('Native JSON reporter output is not valid JSON: ' + jsonReporterOutput); 38 | process.exit(1); 39 | } 40 | 41 | const { stack } = jsonReporterOutput.failures[0].err; 42 | assert(stack.split('\n').length > 2, `Stack doesn't look full: ${stack}`); 43 | }); 44 | } 45 | 46 | main(); 47 | -------------------------------------------------------------------------------- /test/full-trace/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('Throwing error', () => { 2 | it('should throw', () => { 3 | throw new Error('foo'); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/global-hooks-directory/README: -------------------------------------------------------------------------------- 1 | Check that global hooks get executed if directory tests path is set 2 | -------------------------------------------------------------------------------- /test/global-hooks-directory/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js test/global-hooks-directory/tests/ 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 2 ]; then 7 | # if [[ $OUTPUT == *"run global before hook"* ]] && [[ $OUTPUT == *"run global after hook"* ]]; then 8 | exit 0 9 | # else 10 | # echo "Output doesn't contain information about running hooks" 11 | # echo "Output: $OUTPUT" 12 | 13 | # exit 1 14 | # fi 15 | else 16 | echo "Exit code is wrong: $STATUS" 17 | exit 1 18 | fi 19 | -------------------------------------------------------------------------------- /test/global-hooks-directory/tests/global-hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | before(() => { 4 | console.log('run global before hook'); 5 | throw new Error('foo'); 6 | }); 7 | 8 | after(() => { 9 | console.log('run global after hook'); 10 | throw new Error('bar'); 11 | }); 12 | -------------------------------------------------------------------------------- /test/global-hooks-directory/tests/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./global-hooks'); 4 | 5 | describe('Test suite', () => { 6 | it('should end immediately', () => {}); 7 | }); 8 | -------------------------------------------------------------------------------- /test/global-hooks-require/README: -------------------------------------------------------------------------------- 1 | Check that global hooks get executed even for required files 2 | -------------------------------------------------------------------------------- /test/global-hooks-require/global-hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | before(() => { 4 | console.log('run global before hook'); 5 | throw new Error('foo'); 6 | }); 7 | 8 | after(() => { 9 | console.log('run global after hook'); 10 | throw new Error('bar'); 11 | }); 12 | -------------------------------------------------------------------------------- /test/global-hooks-require/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js test/global-hooks-require/test.js 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 2 ]; then 7 | if [[ $OUTPUT == *"run global before hook"* ]] && [[ $OUTPUT == *"run global after hook"* ]]; then 8 | exit 0 9 | else 10 | echo "Output doesn't contain information about running hooks" 11 | echo "Output: $OUTPUT" 12 | 13 | exit 1 14 | fi 15 | else 16 | echo "Exit code is wrong: $STATUS" 17 | exit 1 18 | fi 19 | -------------------------------------------------------------------------------- /test/global-hooks-require/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./global-hooks'); 4 | 5 | describe('Test suite', () => { 6 | it('should end immediately', () => {}); 7 | }); 8 | -------------------------------------------------------------------------------- /test/global-hooks/README: -------------------------------------------------------------------------------- 1 | Check that global hooks get executed 2 | -------------------------------------------------------------------------------- /test/global-hooks/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js test/global-hooks/test.js 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 2 ]; then 7 | if [[ $OUTPUT == *"run global before hook"* ]] && [[ $OUTPUT == *"run global after hook"* ]]; then 8 | exit 0 9 | else 10 | echo "Output doesn't contain information about running hooks" 11 | echo "Output: $OUTPUT" 12 | 13 | exit 1 14 | fi 15 | else 16 | echo "Exit code is wrong: $STATUS" 17 | exit 1 18 | fi 19 | -------------------------------------------------------------------------------- /test/global-hooks/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | before(() => { 4 | console.log('run global before hook'); 5 | throw new Error('foo'); 6 | }); 7 | 8 | after(() => { 9 | console.log('run global after hook'); 10 | throw new Error('bar'); 11 | }); 12 | 13 | describe('Test suite', () => { 14 | it('should end immediately', () => {}); 15 | }); 16 | -------------------------------------------------------------------------------- /test/grep/README: -------------------------------------------------------------------------------- 1 | - index.js check that --grep flag run tests by string pattern 'grep' 2 | - indexProgrammatic.js check that --grep flag run tests by string pattern 'grep' with programmatic using mocha 3 | -------------------------------------------------------------------------------- /test/grep/index-alias.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var assert = require('assert'); 4 | var path = require('path'); 5 | var exec = require('child_process').exec; 6 | var libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 7 | 8 | exec(libExecutable + ' -R json -g grep test/grep/tests', { 9 | cwd: path.resolve(__dirname, '../../') 10 | }, function (err, stderr) { 11 | if (err) { 12 | console.error(err); 13 | process.exit(1); 14 | } 15 | 16 | var jsonReporterOutput = stderr.toString(); 17 | 18 | try { 19 | jsonReporterOutput = JSON.parse(jsonReporterOutput); 20 | } catch (ex) { 21 | console.error('Native JSON reporter output is not valid JSON: ' + jsonReporterOutput); 22 | process.exit(1); 23 | } 24 | 25 | assert.strictEqual(jsonReporterOutput.stats.passes, 3); 26 | }); 27 | -------------------------------------------------------------------------------- /test/grep/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var assert = require('assert'); 4 | var path = require('path'); 5 | var exec = require('child_process').exec; 6 | var libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 7 | 8 | exec(libExecutable + ' -R json --grep grep test/grep/tests', { 9 | cwd: path.resolve(__dirname, '../../') 10 | }, function (err, stderr) { 11 | if (err) { 12 | console.error(err); 13 | process.exit(1); 14 | } 15 | 16 | var jsonReporterOutput = stderr.toString(); 17 | 18 | try { 19 | jsonReporterOutput = JSON.parse(jsonReporterOutput); 20 | } catch (ex) { 21 | console.error('Native JSON reporter output is not valid JSON: ' + jsonReporterOutput); 22 | process.exit(1); 23 | } 24 | 25 | assert.strictEqual(jsonReporterOutput.stats.passes, 3); 26 | }); 27 | -------------------------------------------------------------------------------- /test/grep/indexProgrammatic.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert'); 6 | const MochaParallelTests = require('../../dist/main/mocha').default; 7 | const { setProcessExitListeners } = require('../../dist/util'); 8 | const SilentReporter = require('../util/silent-reporter'); 9 | 10 | setProcessExitListeners(); 11 | 12 | const mocha = new MochaParallelTests; 13 | mocha.addFile(`${__dirname}/tests/test.js`) 14 | .addFile(`${__dirname}/tests/test1.js`) 15 | .timeout(10000) 16 | .grep('grep') 17 | .reporter(SilentReporter); 18 | 19 | mocha.run().on('end', function () { 20 | assert.strictEqual(this.stats.passes, 3); 21 | }); 22 | -------------------------------------------------------------------------------- /test/grep/tests/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | describe('tests', () => { 6 | it('should be run 1. grep', () => { 7 | assert.ok(true); 8 | }); 9 | 10 | it('should be skipped', () => { 11 | assert.ok(true); 12 | }); 13 | }); 14 | 15 | describe('tests should be run. grep', () => { 16 | it('should be run 2', () => { 17 | assert.ok(true); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/grep/tests/test1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | describe('tests file 2', () => { 6 | it('should be run 3. grep', () => { 7 | assert.ok(true); 8 | }); 9 | 10 | it('should be skipped', () => { 11 | assert.ok(true); 12 | }); 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /test/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PASSES=0 4 | FAILES=0 5 | 6 | GREEN='\033[0;32m' 7 | RED='\033[0;31m' 8 | NO_COLOUR='\033[0m' 9 | 10 | function test { 11 | testcase=$1 12 | testexecutable=$2 13 | 14 | echo -n "TESTCASE: ${testcase}.. " 15 | 16 | output=`$testexecutable 2>&1` 17 | status=$? 18 | 19 | if [ $status -ne 0 ]; then 20 | ((FAILES++)) 21 | echo -e "${RED}FAIL${NO_COLOUR}" 22 | echo $output 23 | else 24 | ((PASSES++)) 25 | echo -e "${GREEN}OK${NO_COLOUR}" 26 | fi 27 | } 28 | 29 | MOCHA_VERSION=`mocha --version` 30 | echo "You're running tests with mocha version $MOCHA_VERSION" 31 | 32 | test 'native (json) reporter' test/reporter-native-json/index.sh 33 | test 'native (tap) reporter' test/reporter-native-tap/index.sh 34 | test 'custom (teamcity) reporter' test/reporter-custom-teamcity/index.sh 35 | test 'custom (jenkins) reporter' test/reporter-custom-jenkins/index.sh 36 | test 'custom (mochawesome) reporter' test/reporter-custom-mochawesome/index.sh 37 | test 'custom (allure) reporter' test/reporter-custom-allure/index.sh 38 | test 'cli target' test/cli-target/index.sh 39 | test 'pwd-based reporter' test/reporter-pwd/index.sh 40 | test 'reporter emits events as soon as they come from subprocess' test/reporter-log/index.js 41 | test 'parallel' test/parallel/parallel.sh 42 | test 'parallel order' test/parallel-order/index.js 43 | test 'max parallel tests' test/max-parallel/index.sh 44 | test 'max parallel equal 1' test/max-parallel-1/index.sh 45 | test 'max parallel tests with empty ones' test/max-parallel-empty/index.sh 46 | test 'only tests run' test/only-tests-run/index.js 47 | test 'nesting' test/nesting/nesting.sh 48 | test 'describe inside describe' test/describe-inside-describe/index.js 49 | test 'missing test' test/missing-test/index.js 50 | test 'console logs' test/console-log-inject/index.js 51 | test 'global hooks' test/global-hooks/index.sh 52 | test 'global hooks with required files' test/global-hooks-require/index.sh 53 | test 'global hooks with directory as a tests source' test/global-hooks-directory/index.sh 54 | test '--recursive option if no target is set' test/recursive-no-target/index.js 55 | test 'total time' test/total-time/index.js 56 | test 'timeouts exit code' test/timeouts-exit-code/index.sh 57 | test 'no timeouts' test/no-timeouts/index.sh 58 | test 'js compilers support' test/js-compilers/index.sh 59 | test 'js compilers with files support' test/js-compilers-1/index.sh 60 | test 'js compilers with --require support' test/js-compilers-2/index.sh 61 | test 'reporter with options foundation' test/reporter-options/foundation/index.sh 62 | test 'reporter with options CLI flag' test/reporter-options/cli-once/index.js 63 | test 'mocha.opts' test/mocha-opts/index.sh 64 | test 'syntax errors' test/syntax-errors/index.js 65 | test '--require option support' test/require-option/index.sh 66 | test '--file option support' test/file/index.js 67 | test 'run programmatically (base API)' test/run-programmatically/callback/index.js 68 | test 'run programmatically (reporter.done is called)' test/run-programmatically/reporter-done/index.js 69 | test 'run programmatically (worker TTY)' test/run-programmatically/tty-worker/index.js 70 | test '--no-exit option support' test/reporter-end-no-exit/index.js 71 | test 'node native add-on' test/node-native-addon/index.sh 72 | test 'skip-suite' test/skip-suite/index.sh 73 | test 'skip-test' test/skip-test/index.sh 74 | test '--delay option support' test/delay/index.js 75 | test '--retries option support' test/retries/index.js 76 | test '--exit option support' test/exit/index.js 77 | test 'race condition with --exit' test/race-condition-timeout/index.js 78 | test '--retries plus all tests fail' test/retries-all-fail/index.js 79 | test '--retries plus hooks' test/retries-all-fail-2/index.js 80 | test '--retries with test cases defined in a loop' test/retries-all-fail-3/index.js 81 | test '--retries and --bail should work well together' test/bail-and-retries/index.js 82 | test 'subprocess exits before sending an end message' test/no-subprocess-end/index.js 83 | test 'unhandled rejections should not force subprocess to exit' test/q-promises/index.js 84 | test 'uncaught exceptions should not force subprocess to exit' test/uncaught-exception/index.js 85 | test 'grep option' test/grep/index.js 86 | test 'grep option alias support' test/grep/index-alias.js 87 | test 'grep option - programmatic API support' test/grep/indexProgrammatic.js 88 | test '--full-trace option support' test/full-trace/index.js 89 | test 'report with same describe' test/reporter-same-describes/index.js 90 | 91 | echo "Passes: $PASSES Failes: $FAILES" 92 | echo "" 93 | 94 | if [ $FAILES -gt 0 ]; then 95 | exit 1 96 | fi 97 | -------------------------------------------------------------------------------- /test/js-compilers-1/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaFeatures": { 4 | "jsx": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/js-compilers-1/README: -------------------------------------------------------------------------------- 1 | Check that mocha-parallel-tests support file-based compilers 2 | -------------------------------------------------------------------------------- /test/js-compilers-1/babel-register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('@babel/register')({ 4 | plugins: [ 5 | ['@babel/plugin-transform-react-jsx', { 6 | pragma: 'createElement' 7 | }] 8 | ] 9 | }); 10 | -------------------------------------------------------------------------------- /test/js-compilers-1/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js --compilers js:test/js-compilers-1/babel-register.js test/js-compilers-1/test.js 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 0 ]; then 7 | exit 0 8 | else 9 | echo "Exit code is wrong: $STATUS" 10 | echo "Output: $OUTPUT" 11 | 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /test/js-compilers-1/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | /* eslint-disable no-unused-vars */ 5 | const createElement = () => ({}); 6 | /* eslint-enable no-unused-vars */ 7 | 8 | const reactElem =
9 | 10 |
; 11 | 12 | describe('Test suite with specific language feature', () => { 13 | it('should end immediately', () => { 14 | assert.strictEqual(typeof reactElem, 'object'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/js-compilers-2/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaFeatures": { 4 | "jsx": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/js-compilers-2/README: -------------------------------------------------------------------------------- 1 | Check that mocha-parallel-tests support compilers together with --require option 2 | -------------------------------------------------------------------------------- /test/js-compilers-2/babel-register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('@babel/register')({ 4 | plugins: [ 5 | ['@babel/plugin-transform-react-jsx', { 6 | pragma: 'createElement' 7 | }] 8 | ] 9 | }); 10 | -------------------------------------------------------------------------------- /test/js-compilers-2/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TESTDIR="test/js-compilers-2" 4 | OUTPUT=$(dist/bin/cli.js --compilers js:${TESTDIR}/babel-register.js --require ${TESTDIR}/setup.js ${TESTDIR}/test.js 2>&1) 5 | STATUS=$? 6 | 7 | if [ $STATUS -eq 0 ]; then 8 | exit 0 9 | else 10 | echo "Exit code is wrong: $STATUS" 11 | echo "Output: $OUTPUT" 12 | 13 | exit 1 14 | fi 15 | -------------------------------------------------------------------------------- /test/js-compilers-2/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const createElement = () => ({}); 3 | /* eslint-enable no-unused-vars */ 4 | 5 | global.reactElem =
6 | 7 |
; 8 | -------------------------------------------------------------------------------- /test/js-compilers-2/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | describe('Test suite with specific language feature', () => { 6 | it('should end immediately', () => { 7 | assert.strictEqual(typeof reactElem, 'object'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/js-compilers/README: -------------------------------------------------------------------------------- 1 | Check that mocha-parallel-tests support compilers -------------------------------------------------------------------------------- /test/js-compilers/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js --compilers js:@babel/register test/js-compilers/test.js 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 0 ]; then 7 | exit 0 8 | else 9 | echo "Exit code is $STATUS" 10 | echo "Output: $OUTPUT" 11 | 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /test/js-compilers/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite with specific language feature', () => { 4 | it('should end immediately', () => {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/max-parallel-1/README: -------------------------------------------------------------------------------- 1 | Check that parallel running tests run okay if --max-parallel is 1 2 | -------------------------------------------------------------------------------- /test/max-parallel-1/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TIMESTAMP_START="$(date +%s)" 4 | OUTPUT=$(dist/bin/cli.js -R spec test/max-parallel-1/tests --timeout 30000 --slow 10000 --max-parallel 1) 5 | TIMESTAMP_FINISH="$(date +%s)" 6 | TIMESTAMP_DIFF=`expr $TIMESTAMP_FINISH - $TIMESTAMP_START` 7 | 8 | if [ $TIMESTAMP_DIFF -ge 6 ]; then 9 | if [[ $OUTPUT == *"3 passing"* ]]; then 10 | exit 0 11 | else 12 | echo "Not all tests launched" 13 | exit 1 14 | fi 15 | else 16 | echo "Wrong tests execution time: $TIMESTAMP_DIFF second(s)" 17 | exit 1 18 | fi 19 | -------------------------------------------------------------------------------- /test/max-parallel-1/tests/parallel1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should end in 1 second', function (done) { 5 | setTimeout(done, 1000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/max-parallel-1/tests/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should end in 2 seconds', function (done) { 5 | setTimeout(done, 2000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/max-parallel-1/tests/parallel3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #3', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/max-parallel-empty/README: -------------------------------------------------------------------------------- 1 | Check that parallel running tests number doesn't go beyond max parallel option even if there are simple CommonJS files among tests 2 | -------------------------------------------------------------------------------- /test/max-parallel-empty/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TIMESTAMP_START="$(date +%s)" 4 | 5 | OUTPUT=$(dist/bin/cli.js -R spec test/max-parallel-empty/tests --timeout 30000 --slow 10000 --max-parallel 3) 6 | 7 | # parallel1.js ends in 1 second, parallel2.js in 2 seconds etc 8 | # when any test is over, next should start 9 | # the problem is: mocha doesn't know how much time is needed for test to execute 10 | # expectations: tests will run less than two max times (5 + 4) + 1 second for flaky tests 11 | # expectations: tests will run more than max test time (5 seconds) + 1 second for flaky tests 12 | 13 | TIMESTAMP_FINISH="$(date +%s)" 14 | TIMESTAMP_DIFF=`expr $TIMESTAMP_FINISH - $TIMESTAMP_START` 15 | 16 | if [ $TIMESTAMP_DIFF -ge 3 ]; then 17 | if [[ $OUTPUT == *"1 passing"* ]]; then 18 | exit 0 19 | else 20 | echo "Wrong output: $OUTPUT" 21 | exit 1 22 | fi 23 | else 24 | echo "Wrong tests execution time: $TIMESTAMP_DIFF seconds" 25 | exit 1 26 | fi 27 | -------------------------------------------------------------------------------- /test/max-parallel-empty/tests/parallel-empty1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = null; 4 | -------------------------------------------------------------------------------- /test/max-parallel-empty/tests/parallel-empty2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = null; 4 | -------------------------------------------------------------------------------- /test/max-parallel-empty/tests/parallel-empty3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = null; 4 | -------------------------------------------------------------------------------- /test/max-parallel-empty/tests/parallel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #3', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/max-parallel/README: -------------------------------------------------------------------------------- 1 | Check that parallel running tests number doesn't go beyond max parallel option 2 | -------------------------------------------------------------------------------- /test/max-parallel/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TIMESTAMP_START="$(date +%s)" 4 | 5 | OUTPUT=$(dist/bin/cli.js -R spec test/max-parallel/tests --timeout 30000 --slow 10000 --max-parallel 3) 6 | 7 | # parallel1.js ends in 1 second, parallel2.js in 2 seconds etc 8 | # when any test is over, next should start 9 | # the problem is: mocha doesn't know how much time is needed for test to execute 10 | # expectations: tests will run less than two max times (5 + 4) + 1 second for flaky tests 11 | # expectations: tests will run more than max test time (5 seconds) + 1 second for flaky tests 12 | 13 | TIMESTAMP_FINISH="$(date +%s)" 14 | TIMESTAMP_DIFF=`expr $TIMESTAMP_FINISH - $TIMESTAMP_START` 15 | 16 | if [ $TIMESTAMP_DIFF -gt 6 ] && [ $TIMESTAMP_DIFF -lt 10 ]; then 17 | exit 0 18 | else 19 | echo "Tests running time was $TIMESTAMP_DIFF seconds" 20 | exit 1 21 | fi 22 | -------------------------------------------------------------------------------- /test/max-parallel/tests/parallel1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should end in 1 second', function (done) { 5 | setTimeout(done, 1000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/max-parallel/tests/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should end in 2 seconds', function (done) { 5 | setTimeout(done, 2000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/max-parallel/tests/parallel3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #3', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/max-parallel/tests/parallel4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #4', function () { 4 | it('should end in 4 seconds', function (done) { 5 | setTimeout(done, 4000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/max-parallel/tests/parallel5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #5', function () { 4 | it('should end in 5 seconds', function (done) { 5 | setTimeout(done, 5000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/missing-test/README: -------------------------------------------------------------------------------- 1 | Check that executable fails if non-existing test is scheduled to run 2 | -------------------------------------------------------------------------------- /test/missing-test/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var assert = require('assert'); 4 | var path = require('path'); 5 | var exec = require('child_process').exec; 6 | var libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 7 | 8 | exec(libExecutable + ' -R doc --timeout 60000 --slow 30000 test/missing-test/missing.js', { 9 | cwd: path.resolve(__dirname, '../../') 10 | }, function (err) { 11 | assert(err, 'Err should exist'); 12 | 13 | // "8 - Unused. In previous versions of Node, exit code 8 sometimes indicated an uncaught exception" 14 | // @see https://github.com/nodejs/node-v0.x-archive/blob/master/doc/api/process.markdown#exit-codes 15 | var expectedExitCode = (process.version.indexOf('v0.10.') === 0) 16 | ? 8 17 | : 1; 18 | 19 | assert.strictEqual(err.code, expectedExitCode, 'Error code should equal ' + expectedExitCode); 20 | }); 21 | -------------------------------------------------------------------------------- /test/mocha-opts/README: -------------------------------------------------------------------------------- 1 | Check that mocha-parallel-tests supports mocha.opts file -------------------------------------------------------------------------------- /test/mocha-opts/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TIMESTAMP_START="$(date +%s)" 4 | 5 | OUTPUT=$(dist/bin/cli.js -R spec test/mocha-opts/tests --slow 10000 --opts test/mocha-opts/mocha.opts) 6 | STATUS=$? 7 | TIMESTAMP_FINISH="$(date +%s)" 8 | TIMESTAMP_DIFF=`expr $TIMESTAMP_FINISH - $TIMESTAMP_START` 9 | 10 | if [ $STATUS -eq 0 ]; then 11 | if [[ $TIMESTAMP_DIFF -lt 10 ]]; then 12 | exit 0 13 | else 14 | echo "Running time is too big: $TIMESTAMP_DIFF" 15 | exit 1 16 | fi 17 | else 18 | echo "Exit code is wrong: $STATUS" 19 | exit 1 20 | fi 21 | -------------------------------------------------------------------------------- /test/mocha-opts/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 30000 -------------------------------------------------------------------------------- /test/mocha-opts/tests/parallel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/mocha-opts/tests/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should end in 5 seconds', function (done) { 5 | setTimeout(done, 5000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/mocha-opts/tests/parallel3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #3', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/nesting/nesting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Check nested tests displays right - parent', function () { 4 | describe('Check nested tests displays right - child', function () { 5 | it('should be shild', function (done) { 6 | done(); 7 | }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/nesting/nesting.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | OUTPUT=$(dist/bin/cli.js -R spec --timeout 60000 --slow 30000 test/nesting/nesting.js) 3 | strindex() { 4 | x="${1%%$2*}" 5 | [[ $x = $1 ]] && echo -1 || echo ${#x} 6 | } 7 | PARENT=$(strindex "$OUTPUT" "parent") 8 | CHILD=$(strindex "$OUTPUT" "child") 9 | 10 | if [[ PARENT -lt CHILD ]] 11 | then 12 | exit 0 13 | else 14 | echo "${PARENT}" 15 | echo "${CHILD}" 16 | exit 1 17 | fi 18 | -------------------------------------------------------------------------------- /test/no-subprocess-end/README: -------------------------------------------------------------------------------- 1 | Check that if subprocess exits before sending an "end" message the main process still renders its output 2 | -------------------------------------------------------------------------------- /test/no-subprocess-end/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const { expect } = require('chai'); 5 | const { exec } = require('child_process'); 6 | const { resolve } = require('path'); 7 | const libExecutable = resolve(__dirname, '../../dist/bin/cli.js'); 8 | const reporterPath = resolve(__dirname, '../util/events-reporter.js'); 9 | 10 | const cleanOutput = (buf) => String(buf) 11 | .split('\n') 12 | .filter(Boolean); 13 | 14 | const runSpec = (spec) => { 15 | const cwd = resolve(__dirname, '../../'); 16 | const specPath = resolve(__dirname, 'spec', spec) + '/'; 17 | 18 | return new Promise((resolve) => { 19 | exec(`${libExecutable} -R ${reporterPath} --max-parallel 2 ${specPath}`, { cwd }, function (err, stdout, stderr) { 20 | if (err) { 21 | resolve({ 22 | stdout: cleanOutput(stdout), 23 | stderr: cleanOutput(stderr), 24 | code: err.code, 25 | }); 26 | } else { 27 | resolve({ 28 | stdout: cleanOutput(stdout), 29 | stderr: cleanOutput(stderr), 30 | code: 0, 31 | }); 32 | } 33 | }); 34 | }); 35 | }; 36 | 37 | Promise.resolve() 38 | // one simple crash in the middle of a process 39 | .then(() => runSpec('one-crash')) 40 | .then(function oneCrash({ code, stdout }) { 41 | expect(stdout).to.deep.equal(['start', 'suite', 'suite', 'suite', 'test', 'pass', 'test end', 'test']); 42 | assert.strictEqual(code, 255, `"one-crash" spec finished with wrong exit code: ${code}`); 43 | }) 44 | // one simple crash in the beginning of a process 45 | .then(() => runSpec('one-crash-early')) 46 | .then(function oneCrashEarly({ code, stderr, stdout }) { 47 | expect(stderr).to.deep.equal(['I am about to exit...']); 48 | expect(stdout).to.deep.equal(['start', 'suite']); // top level main process suite 49 | assert.strictEqual(code, 255, `"one-crash-early" spec finished with wrong exit code: ${code}`); 50 | }) 51 | // one simple crash in the middle, one okay 52 | .then(() => runSpec('one-crash-one-ok')) 53 | .then(function oneCrashOneOk({ code, stdout }) { 54 | expect(stdout).to.deep.equal(['start', 'suite', 'suite', 'suite', 'test', 'pass', 'test end', 'suite end', 'suite end', 'suite', 'suite', 'test', 'pass', 'test end', 'test']); 55 | assert.strictEqual(code, 255, `"one-crash-one-ok" spec finished with wrong exit code: ${code}`); 56 | }) 57 | // one simple crash in the beginning, one okay 58 | .then(() => runSpec('one-crash-early-one-ok')) 59 | .then(function oneCrashEarlyOneOk({ code, stdout }) { 60 | expect(stdout).to.deep.equal(['start', 'suite']); 61 | assert.strictEqual(code, 255, `"one-crash-early-one-ok" spec finished with wrong exit code: ${code}`); 62 | }) 63 | .catch((err) => { 64 | console.error(err); 65 | process.exit(1); 66 | }); 67 | -------------------------------------------------------------------------------- /test/no-subprocess-end/spec/one-crash-early-one-ok/crash.spec.js: -------------------------------------------------------------------------------- 1 | process.exit(255); 2 | 3 | describe('suite', () => { 4 | it('should pass', () => {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/no-subprocess-end/spec/one-crash-early-one-ok/ok.spec.js: -------------------------------------------------------------------------------- 1 | describe('suite', () => { 2 | it('should pass', (done) => { 3 | setTimeout(done, 100); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/no-subprocess-end/spec/one-crash-early/crash.spec.js: -------------------------------------------------------------------------------- 1 | console.error('I am about to exit...'); 2 | process.exit(255); 3 | 4 | describe('suite', () => { 5 | it('should pass', () => {}); 6 | }); 7 | -------------------------------------------------------------------------------- /test/no-subprocess-end/spec/one-crash-one-ok/crash.spec.js: -------------------------------------------------------------------------------- 1 | describe('suite', () => { 2 | it('should pass', () => {}); 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | it('should exit after a 100ms', (done) => { 6 | setTimeout(() => { 7 | process.exit(255); 8 | }, 100); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/no-subprocess-end/spec/one-crash-one-ok/ok.spec.js: -------------------------------------------------------------------------------- 1 | describe('suite', () => { 2 | it('should pass', () => {}); 3 | }); 4 | -------------------------------------------------------------------------------- /test/no-subprocess-end/spec/one-crash/crash.spec.js: -------------------------------------------------------------------------------- 1 | describe('suite', () => { 2 | it('should pass', () => {}); 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | it('should exit after a 100ms', (done) => { 6 | setTimeout(() => { 7 | process.exit(255); 8 | }, 100); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/no-timeouts/README: -------------------------------------------------------------------------------- 1 | Check that tests with timeouts exit with error status code 2 | -------------------------------------------------------------------------------- /test/no-timeouts/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dist/bin/cli.js -R test/util/silent-reporter.js test/no-timeouts/tests #1>/dev/null 2>&1 4 | MPT_STATUS_CODE=$? 5 | 6 | dist/bin/cli.js --no-timeouts -R test/util/silent-reporter.js test/no-timeouts/tests #1>/dev/null 2>&1 7 | MPT_STATUS_CODE_NO_TIMEOUT=$? 8 | 9 | # currently mocha exits with 2 if 2 tests failed, 3 if 3 tests failed etc 10 | # though it's a strange behaviour, mocha-parallel-tests behaviour should be the same 11 | if [ $MPT_STATUS_CODE -ne 2 ]; then 12 | echo "The flag no-timeouts did not work as expected: $MPT_STATUS_CODE (no flag) instead of 2" 13 | exit 1 14 | fi 15 | 16 | if [ $MPT_STATUS_CODE_NO_TIMEOUT -ne 0 ]; then 17 | echo "The flag no-timeouts did not work as expected: $MPT_STATUS_CODE_NO_TIMEOUT (flag) instead of 0" 18 | exit 1 19 | fi 20 | -------------------------------------------------------------------------------- /test/no-timeouts/tests/1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Run async test 2', function () { 4 | /* eslint-disable no-unused-vars */ 5 | it('should fail with timeout', function (done) { 6 | setTimeout(done, 4000); 7 | }); 8 | /* eslint-enable no-unused-vars */ 9 | }); 10 | -------------------------------------------------------------------------------- /test/no-timeouts/tests/2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Run async test', function () { 4 | /* eslint-disable no-unused-vars */ 5 | it('should fail with timeout', function (done) { 6 | setTimeout(done, 4000); 7 | }); 8 | /* eslint-enable no-unused-vars */ 9 | }); 10 | -------------------------------------------------------------------------------- /test/node-native-addon/README: -------------------------------------------------------------------------------- 1 | Compiled modules work with mocha-parallel-tests 2 | -------------------------------------------------------------------------------- /test/node-native-addon/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js test/node-native-addon/spec.js 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 0 ]; then 7 | exit 0 8 | else 9 | echo "Exit code is wrong: $STATUS" 10 | echo "Output: $OUTPUT" 11 | 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /test/node-native-addon/spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const microtime = require('microtime'); 3 | 4 | describe('native module', () => { 5 | it('microtime', () => { 6 | assert.strictEqual(typeof microtime.now(), 'number'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/only-tests-run/README: -------------------------------------------------------------------------------- 1 | Check that only files with mocha tests are processed 2 | -------------------------------------------------------------------------------- /test/only-tests-run/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var assert = require('assert'); 4 | var path = require('path'); 5 | var exec = require('child_process').exec; 6 | var libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 7 | 8 | exec(libExecutable + ' -R json --timeout 60000 --slow 30000 test/only-tests-run/tests', { 9 | cwd: path.resolve(__dirname, '../../') 10 | }, function (err, stderr) { 11 | if (err) { 12 | console.error(err); 13 | process.exit(1); 14 | } 15 | 16 | var jsonReporterOutput = stderr.toString(); 17 | 18 | try { 19 | jsonReporterOutput = JSON.parse(jsonReporterOutput); 20 | } catch (ex) { 21 | console.error('Native JSON reporter output is not valid JSON: ' + jsonReporterOutput); 22 | process.exit(1); 23 | } 24 | 25 | assert.strictEqual(jsonReporterOutput.stats.suites, 4, 'Suites number is wrong'); 26 | assert.strictEqual(jsonReporterOutput.stats.tests, 3, 'Tests number is wrong'); 27 | assert.strictEqual(jsonReporterOutput.stats.passes, 3, 'Passes number is wrong'); 28 | }); 29 | -------------------------------------------------------------------------------- /test/only-tests-run/tests/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | foo: 'bar' 5 | }; 6 | -------------------------------------------------------------------------------- /test/only-tests-run/tests/parallel1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/only-tests-run/tests/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | 8 | it('should end now', function () {}); 9 | }); 10 | -------------------------------------------------------------------------------- /test/parallel-order/README: -------------------------------------------------------------------------------- 1 | Check that tests not only run in parallel but also output their own results -------------------------------------------------------------------------------- /test/parallel-order/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var assert = require('assert'); 4 | var path = require('path'); 5 | var exec = require('child_process').exec; 6 | var libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 7 | var start = Date.now(); 8 | 9 | exec(libExecutable + ' -R json --timeout 60000 --slow 30000 test/parallel-order/tests', { 10 | cwd: path.resolve(__dirname, '../../') 11 | }, function (err, stdout) { 12 | var diffTime = Date.now() - start; 13 | assert.equal(Math.round(diffTime / 1000) < 11, true, 'Tests should run in parallel'); 14 | 15 | var jsonReporterOutput = stdout.toString(); 16 | 17 | try { 18 | jsonReporterOutput = JSON.parse(jsonReporterOutput); 19 | } catch (ex) { 20 | console.error('Native JSON reporter output is not valid JSON: ' + jsonReporterOutput); 21 | process.exit(1); 22 | } 23 | 24 | assert.equal(typeof jsonReporterOutput.stats, 'object', 'Reporter should contain stats object'); 25 | assert.equal(jsonReporterOutput.stats.suites, 6, 'Reporter should contain information about 3 suites'); 26 | assert.equal(jsonReporterOutput.stats.tests, 4, 'Reporter should contain information about all run tests'); 27 | assert.equal(jsonReporterOutput.stats.passes, 3, 'Reporter should contain information about all passes'); 28 | assert.equal(jsonReporterOutput.stats.pending, 0, 'Reporter should contain information about all pendings'); 29 | assert.equal(jsonReporterOutput.stats.failures, 1, 'Reporter should contain information about all failures'); 30 | 31 | // check time diff 32 | var startDate = new Date(jsonReporterOutput.stats.start); 33 | var endDate = new Date(jsonReporterOutput.stats.end); 34 | var diffMs = endDate - startDate; 35 | 36 | assert(startDate.getTime(), 'Start date is invalid'); 37 | assert(endDate.getTime(), 'End date is invalid'); 38 | assert.strictEqual(diffMs >= 3000, true, 'Diff between end and start dates is too small'); 39 | 40 | // common structure 41 | assert.equal(Array.isArray(jsonReporterOutput.tests), true, 'Reporter should contain tests array'); 42 | assert.equal(Array.isArray(jsonReporterOutput.pending), true, 'Reporter should contain pendings array'); 43 | assert.equal(Array.isArray(jsonReporterOutput.passes), true, 'Reporter should contain passes array'); 44 | 45 | // check failure 46 | assert.equal(Array.isArray(jsonReporterOutput.failures), true, 'Reporter should contain failures array'); 47 | assert.equal(jsonReporterOutput.failures.length, 1, 'Reporter should contain one error'); 48 | assert.equal(jsonReporterOutput.failures[0].fullTitle.trim(), 'Test suite #2 should fail'); 49 | assert.equal(jsonReporterOutput.failures[0].err.message, 'some error'); 50 | 51 | return; 52 | 53 | // TODO 54 | 55 | // first output should be from parallel1.js 56 | // assert.equal(jsonReporterOutput.tests[0].fullTitle, 'Test suite #1 should end in 3 seconds', 'First output should be from parallel1.js'); 57 | // assert.equal(jsonReporterOutput.tests[0].duration >= 3000 && jsonReporterOutput.tests[0].duration < 4000, true, 'parallel1.js suite should end in 3 seconds'); 58 | 59 | // second output should be from parallel3.js 60 | // because parallel1.js ended and parallel2.js is still running 61 | // assert.equal(jsonReporterOutput.tests[1].fullTitle, 'Test suite #3 should end in 3 seconds', 'Second output should be from parallel3.js'); 62 | // assert.equal(jsonReporterOutput.tests[1].duration >= 3000 && jsonReporterOutput.tests[1].duration < 4000, true, 'parallel3.js suite should end in 3 seconds'); 63 | 64 | // and the last output should be from parallel2.js 65 | // assert.equal(jsonReporterOutput.tests[2].fullTitle, 'Test suite #2 should end in 5 seconds', 'Last output should be from parallel2.js'); 66 | // assert.equal(jsonReporterOutput.tests[2].duration >= 5000 && jsonReporterOutput.tests[2].duration < 6000, true, 'parallel2.js suite should end in 5 seconds'); 67 | // assert.equal(jsonReporterOutput.tests[3].fullTitle, 'Test suite #2 should fail', 'parallel2.js second test should fail'); 68 | // assert(jsonReporterOutput.tests[3].err, true, 'parallel2.js second test should fail'); 69 | }); 70 | -------------------------------------------------------------------------------- /test/parallel-order/tests/parallel1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/parallel-order/tests/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should end in 5 seconds', function (done) { 5 | setTimeout(done, 5000); 6 | }); 7 | 8 | it('should fail', function (done) { 9 | done(new Error('some error')); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/parallel-order/tests/parallel3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #3', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/parallel/parallel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TIMESTAMP_START="$(date +%s)" 4 | 5 | OUTPUT=$(dist/bin/cli.js -R spec test/parallel/tests --timeout 10000 --slow 10000) 6 | 7 | TIMESTAMP_FINISH="$(date +%s)" 8 | TIMESTAMP_DIFF=`expr $TIMESTAMP_FINISH - $TIMESTAMP_START` 9 | 10 | if [[ $TIMESTAMP_DIFF -lt 10 ]]; then 11 | exit 0 12 | else 13 | echo "Tests running time was $TIMESTAMP_DIFF seconds" 14 | exit 1 15 | fi 16 | -------------------------------------------------------------------------------- /test/parallel/tests/parallel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/parallel/tests/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should end in 5 seconds', function (done) { 5 | setTimeout(done, 5000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/parallel/tests/parallel3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #3', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/q-promises/README: -------------------------------------------------------------------------------- 1 | Make sure that q-based promises do not force subprocess to exit 2 | -------------------------------------------------------------------------------- /test/q-promises/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | 6 | const MochaParallelTests = require('../../dist/main/mocha').default; 7 | const { setProcessExitListeners } = require('../../dist/util'); 8 | const SilentReporter = require('../util/silent-reporter'); 9 | 10 | setProcessExitListeners(); 11 | 12 | const onRunnerEnd = sinon.fake(); 13 | const mocha = new MochaParallelTests; 14 | mocha.reporter(SilentReporter); 15 | mocha.addFile(`${__dirname}/index.spec.js`); 16 | mocha.run().on('end', onRunnerEnd); 17 | 18 | process.on('exit', () => { 19 | assert.strictEqual(onRunnerEnd.callCount, 1, `runner "end" event occured wrong number of times: ${onRunnerEnd.callCount}`); 20 | }); 21 | -------------------------------------------------------------------------------- /test/q-promises/index.spec.js: -------------------------------------------------------------------------------- 1 | const { reject } = require('q'); 2 | 3 | describe('suite', function () { 4 | it('case', function () { 5 | reject(new Error('foo')); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/race-condition-timeout/README: -------------------------------------------------------------------------------- 1 | race condition with --exit and and empty reporter output 2 | -------------------------------------------------------------------------------- /test/race-condition-timeout/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var exec = require('child_process').exec; 5 | var libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 6 | 7 | exec(libExecutable + ' --exit --timeout 15000 --reporter json test/race-condition-timeout/index.spec.js', { 8 | cwd: path.resolve(__dirname, '../../') 9 | }, function (err, stderr) { 10 | var jsonReporterOutput = stderr.toString(); 11 | 12 | try { 13 | jsonReporterOutput = JSON.parse(jsonReporterOutput); 14 | } catch (ex) { 15 | console.error('Native JSON reporter output is not valid JSON: ' + jsonReporterOutput); 16 | process.exit(1); 17 | } 18 | 19 | // we just want to make sure that the test output is a JSON string 20 | }); 21 | -------------------------------------------------------------------------------- /test/race-condition-timeout/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('boom', () => { 2 | beforeEach(async () => { 3 | setTimeout(() => { 4 | throw new Error('boom!'); 5 | }, 1); 6 | }); 7 | 8 | describe('nested describe', () => { 9 | it('should pass #1', () => { 10 | // pass 11 | }); 12 | 13 | it('should pass #2', () => { 14 | // pass 15 | }); 16 | 17 | it('should pass #3', () => { 18 | // pass 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/recursive-no-target/README: -------------------------------------------------------------------------------- 1 | Check that tests with no target are executed with --recursive option 2 | -------------------------------------------------------------------------------- /test/recursive-no-target/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert'); 6 | const path = require('path'); 7 | const exec = require('child_process').exec; 8 | const libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 9 | 10 | exec(`${libExecutable} -R json --recursive`, { 11 | cwd: path.dirname(__filename) 12 | }, (err, stderr) => { 13 | if (err) { 14 | console.error(err); 15 | process.exit(1); 16 | } 17 | 18 | let jsonReporterOutput = stderr.toString(); 19 | try { 20 | jsonReporterOutput = JSON.parse(jsonReporterOutput); 21 | } catch (ex) { 22 | console.error('Native JSON reporter output is not valid JSON: ' + jsonReporterOutput); 23 | process.exit(1); 24 | } 25 | 26 | // 2 suite events for each file (:root suite and own top level suite) 27 | assert.strictEqual(jsonReporterOutput.stats.suites, 4); 28 | assert.strictEqual(jsonReporterOutput.stats.tests, 2); 29 | assert.strictEqual(jsonReporterOutput.stats.passes, 2); 30 | assert.strictEqual(jsonReporterOutput.stats.failures, 0); 31 | }); 32 | -------------------------------------------------------------------------------- /test/recursive-no-target/test/1/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should be okay', function () {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/recursive-no-target/test/2/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should be okay', function () {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/reporter-custom-allure/.gitignore: -------------------------------------------------------------------------------- 1 | allure-results 2 | -------------------------------------------------------------------------------- /test/reporter-custom-allure/README: -------------------------------------------------------------------------------- 1 | Check if custom mochawesome reporter is supported and used 2 | -------------------------------------------------------------------------------- /test/reporter-custom-allure/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | REPORT_DIR="allure-results" 5 | 6 | # clean directory 7 | rm -fr $DIR/$REPORT_DIR 8 | cd $DIR && ../../dist/bin/cli.js -R mocha-allure-reporter $DIR/suite.js 9 | 10 | if [ ! -d "$DIR/$REPORT_DIR" ]; then 11 | echo "$REPORT_DIR directory doesn't exist" 12 | exit 1 13 | fi 14 | 15 | if [ -z "$(ls -A $DIR/$REPORT_DIR)" ]; then 16 | echo "$REPORT_DIR directory is empty" 17 | exit 1 18 | fi 19 | -------------------------------------------------------------------------------- /test/reporter-custom-allure/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Suite', function suiteName() { 4 | it('should finish immediately', function testCase1Name() {}); 5 | it('should finish after 100ms', function testCase2Name(done) { setTimeout(done, 100); }); 6 | it('should fail', function testCase3Name() { throw new Error('foo'); }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/reporter-custom-jenkins/README: -------------------------------------------------------------------------------- 1 | Check if custom jenkins reporter is supported and used 2 | -------------------------------------------------------------------------------- /test/reporter-custom-jenkins/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | RESULT="$DIR/result.xml" 5 | OUTPUT=$(JUNIT_REPORT_PATH=$RESULT dist/bin/cli.js -R mocha-jenkins-reporter test/reporter-custom-jenkins/suite.js 2>&1) 6 | STATUS=$? 7 | 8 | if [ $STATUS -eq 0 ]; then 9 | RESULT_XML=$(cat $RESULT) 10 | 11 | if [[ $RESULT_XML == *" { 19 | fs.readFile(filePath, (err, data) => { 20 | if (err) { 21 | throw new Error(`expected ${filePath} file read`); 22 | } 23 | assert.equal(data.toString('utf8'), 'test', `expected ${filePath} to contain 'test' string`); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/reporter-end-no-exit/tests/1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', () => { 4 | it('test case 1', () => {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/reporter-end-no-exit/tests/2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', () => { 4 | it('test case 2', () => {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/reporter-end-no-exit/tests/3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #3', () => { 4 | it('test case 3', () => {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/reporter-end-no-exit/tests/reporter/spec-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const os = require('os'); 4 | const fs = require('fs'); 5 | 6 | const Base = require('mocha').reporters.base; 7 | const inherits = require('mocha').utils.inherits; 8 | 9 | const FILE_PATH = `${os.tmpdir()}/${process.env.FILE}`; 10 | 11 | function extraSpec(runner) { 12 | Base.call(this, runner); 13 | 14 | runner.on('end', () => { 15 | // write file to temp directory 16 | fs.writeFile(FILE_PATH, 'test', err => { 17 | if (err) { 18 | throw new Error(`expected ${FILE_PATH} file write`); 19 | } 20 | }); 21 | }); 22 | } 23 | 24 | module.exports = extraSpec; 25 | 26 | inherits(extraSpec, Base); 27 | -------------------------------------------------------------------------------- /test/reporter-log/README: -------------------------------------------------------------------------------- 1 | Print test log after each passed test 2 | -------------------------------------------------------------------------------- /test/reporter-log/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const MochaParallelTests = require('../../dist/main/mocha').default; 5 | const SilentReporter = require('../util/silent-reporter'); 6 | 7 | const mocha = new MochaParallelTests; 8 | let suite1FinishTime; 9 | let suite2FinishTime; 10 | 11 | mocha 12 | // the first test will be passed after few milliseconds 13 | .addFile(`${__dirname}/tests/log1.js`) 14 | // the second test will be passed after 3 seconds 15 | .addFile(`${__dirname}/tests/log2.js`) 16 | .timeout(3000) 17 | .reporter(SilentReporter) 18 | .run() 19 | .on('suite end', (suite) => { 20 | if (suite.title === 'suite1') { 21 | suite1FinishTime = Date.now(); 22 | } else if (suite.title === 'suite2') { 23 | suite2FinishTime = Date.now(); 24 | } 25 | }) 26 | // suite end time of each test should be not the same 27 | // .on('suite end', () => timings.push((new Date()).getSeconds())) 28 | .on('end', () => { 29 | assert(suite1FinishTime, 'Suite 1 was not finished'); 30 | assert(suite2FinishTime, 'Suite 2 was not finished'); 31 | 32 | const diffSeconds = Math.round((suite2FinishTime - suite1FinishTime) / 1000); 33 | assert(diffSeconds >= 2, `Should have at least 2 seconds diff between suite end events but got ${diffSeconds}`); 34 | }); 35 | -------------------------------------------------------------------------------- /test/reporter-log/tests/log1.js: -------------------------------------------------------------------------------- 1 | describe('suite1', () => { 2 | it('case1', () => {}); 3 | }); 4 | -------------------------------------------------------------------------------- /test/reporter-log/tests/log2.js: -------------------------------------------------------------------------------- 1 | describe('suite2', () => { 2 | it('case2', () => { 3 | return new Promise(resolve => { 4 | setTimeout(resolve, 2000); 5 | }); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/reporter-native-json/README: -------------------------------------------------------------------------------- 1 | Check if native JSON reporter is supported and used 2 | -------------------------------------------------------------------------------- /test/reporter-native-json/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const exec = require('child_process').exec; 5 | 6 | const libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 7 | 8 | exec(`${libExecutable} -R json --timeout 60000 --slow 30000 test/reporter-native-json/suite.js`, { 9 | cwd: path.resolve(__dirname, '../../') 10 | }, (err, stdout) => { 11 | if (err) { 12 | throw err; 13 | } 14 | 15 | try { 16 | JSON.parse(stdout); 17 | } catch (ex) { 18 | console.warn(`Reporter output doesn't have much common with JSON: ${stdout}`); 19 | process.exit(1); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /test/reporter-native-json/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js -R json --timeout 60000 --slow 30000 test/reporter-native-json/suite.js) 4 | 5 | if [[ $OUTPUT == *"stats"* ]]; then 6 | exit 0 7 | else 8 | echo "Reporter output doesn't have much common with JSON: $OUTPUT" 9 | exit 1 10 | fi 11 | -------------------------------------------------------------------------------- /test/reporter-native-json/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Check if JSON native reporter used', function () { 4 | it('should use JSON reporter', function () {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/reporter-native-tap/README: -------------------------------------------------------------------------------- 1 | Check if native TAP reporter is supported 2 | -------------------------------------------------------------------------------- /test/reporter-native-tap/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js -R tap test/reporter-native-tap/suite.js 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 0 ]; then 7 | exit 0 8 | else 9 | echo "Exit code is unexpected: $STATUS" 10 | echo "Debug output: $OUTPUT" 11 | 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /test/reporter-native-tap/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Check if tap native reporter used', function () { 4 | it('should use tap reporter', function () {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/reporter-options/README: -------------------------------------------------------------------------------- 1 | foundation: check if reporter with options works 2 | cli-once: check if --reporter-options flag is used 3 | -------------------------------------------------------------------------------- /test/reporter-options/cli-once/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const { existsSync } = require('fs'); 6 | const os = require('os'); 7 | const path = require('path'); 8 | const exec = require('child_process').exec; 9 | 10 | const fileBasePath = `${os.tmpdir()}/${Date.now()}`; 11 | const libExecutable = path.resolve(__dirname, '../../../dist/bin/cli.js'); 12 | 13 | exec(`${libExecutable} -R mochawesome --reporter-options reportFilename=${fileBasePath},json=true,html=false test/reporter-options/spec.js`, { 14 | cwd: path.resolve(__dirname, '../../../'), 15 | }, (err) => { 16 | if (err) { 17 | console.error(err); 18 | process.exit(1); 19 | } 20 | 21 | if (!existsSync(`${fileBasePath}.json`)) { 22 | console.error('JSON file doesn\'t exist'); 23 | process.exit(1); 24 | } 25 | 26 | if (existsSync(`${fileBasePath}.html`)) { 27 | console.error('HTML file unexpectedly exists'); 28 | process.exit(1); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /test/reporter-options/foundation/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js -R xunit test/reporter-options/spec.js 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 0 ]; then 7 | exit 0 8 | else 9 | echo "Exit code is $STATUS" 10 | echo "Output: $OUTPUT" 11 | 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /test/reporter-options/spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Suite', function suiteName() { 4 | it('should finish immediately', function testCase1Name() {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/reporter-pwd/README: -------------------------------------------------------------------------------- 1 | Check if pwd-based reporter is supported and used 2 | -------------------------------------------------------------------------------- /test/reporter-pwd/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | OUTPUT=$(cd ${CURRENT_DIR} && ../../dist/bin/cli.js -R reporter-pwd suite.js) 5 | 6 | if [[ $OUTPUT == *"startfinish"* ]]; then 7 | exit 0 8 | else 9 | echo "PWD-based reporter is not supported. OUTPUT: $OUTPUT" 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /test/reporter-pwd/reporter-pwd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Base = require('mocha').reporters.Base; 4 | 5 | /** 6 | * Initialize a new `Teamcity` reporter. 7 | * 8 | * @param {Runner} runner 9 | * @api public 10 | */ 11 | 12 | function FileReporter(runner) { 13 | Base.call(this, runner); 14 | 15 | runner.on('start', function () { 16 | process.stdout.write('start'); 17 | }); 18 | 19 | runner.on('end', function () { 20 | process.stdout.write('finish'); 21 | }); 22 | } 23 | 24 | exports = module.exports = FileReporter; 25 | -------------------------------------------------------------------------------- /test/reporter-pwd/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Check if file reporter used', function () { 4 | it('should use file reporter', function () {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/reporter-same-describes/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const MochaParallelTests = require('../../dist/main/mocha').default; 5 | const SilentReporter = require('../util/silent-reporter'); 6 | const { basename } = require('path'); 7 | 8 | const mocha = new MochaParallelTests; 9 | const reportedFiles = []; 10 | 11 | mocha 12 | .addFile(`${__dirname}/tests/first.js`) 13 | .addFile(`${__dirname}/tests/second.js`) 14 | .reporter(SilentReporter) 15 | .run() 16 | .on('suite', (suite) => { 17 | if (suite.file) { 18 | reportedFiles.push(basename(suite.file)); 19 | } 20 | }); 21 | 22 | process.on('exit', () => { 23 | assert(reportedFiles.includes('first.js'), 'suite in first.js not reported'); 24 | assert(reportedFiles.includes('second.js'), 'suite in second.js not reported'); 25 | }); 26 | -------------------------------------------------------------------------------- /test/reporter-same-describes/tests/first.js: -------------------------------------------------------------------------------- 1 | describe('parent-describe', function() { 2 | it('first', function() {}); 3 | }); 4 | -------------------------------------------------------------------------------- /test/reporter-same-describes/tests/second.js: -------------------------------------------------------------------------------- 1 | describe('parent-describe', function() { 2 | it('second', function() {}); 3 | }); 4 | -------------------------------------------------------------------------------- /test/require-option/README: -------------------------------------------------------------------------------- 1 | Check that mocha-parallel-tests supports --require option 2 | -------------------------------------------------------------------------------- /test/require-option/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js --require test/require-option/require-module.js test/require-option/test.js) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 0 ]; then 7 | exit 0 8 | else 9 | echo "Exit code is wrong: $STATUS" 10 | echo "Output: $OUTPUT" 11 | 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /test/require-option/require-module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | global.foo = () => {}; 4 | -------------------------------------------------------------------------------- /test/require-option/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite', function () { 4 | it('should not throw', function () { 5 | /* eslint-disable no-undef */ 6 | foo(); 7 | /* eslint-enable no-undef */ 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/retries-all-fail-2/README: -------------------------------------------------------------------------------- 1 | --retries option support where all retries fail with hook 2 | -------------------------------------------------------------------------------- /test/retries-all-fail-2/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | const MochaParallelTests = require('../../dist/main/mocha').default; 6 | const { setProcessExitListeners } = require('../../dist/util'); 7 | const SilentReporter = require('../util/silent-reporter'); 8 | 9 | setProcessExitListeners(); 10 | 11 | const onRunnerEnd = sinon.fake(); 12 | const mocha = new MochaParallelTests; 13 | 14 | mocha.reporter(SilentReporter); 15 | mocha.suite.retries(1); 16 | mocha.addFile(`${__dirname}/index.spec.js`); 17 | mocha.run().on('end', onRunnerEnd); 18 | 19 | process.on('exit', () => { 20 | assert.strictEqual(onRunnerEnd.callCount, 1, `runner "end" event occured wrong number of times: ${onRunnerEnd.callCount}`); 21 | }); 22 | -------------------------------------------------------------------------------- /test/retries-all-fail-2/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('Throwing error', () => { 2 | before(() => {}); 3 | after(() => {}); 4 | afterEach(() => {}); 5 | 6 | it('error', () => { 7 | console.log('Ready to throw error'); 8 | throw new Error('foo'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/retries-all-fail-3/README: -------------------------------------------------------------------------------- 1 | --retries option support where test cases are defined in a loop 2 | -------------------------------------------------------------------------------- /test/retries-all-fail-3/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | const MochaParallelTests = require('../../dist/main/mocha').default; 6 | const { setProcessExitListeners } = require('../../dist/util'); 7 | const SilentReporter = require('../util/silent-reporter'); 8 | 9 | setProcessExitListeners(); 10 | 11 | const onRunnerEnd = sinon.fake(); 12 | const mocha = new MochaParallelTests; 13 | 14 | mocha.reporter(SilentReporter); 15 | mocha.suite.retries(1); 16 | mocha.addFile(`${__dirname}/index.spec.js`); 17 | mocha.run().on('end', onRunnerEnd); 18 | 19 | process.on('exit', () => { 20 | assert.strictEqual(onRunnerEnd.callCount, 1, `runner "end" event occured wrong number of times: ${onRunnerEnd.callCount}`); 21 | }); 22 | -------------------------------------------------------------------------------- /test/retries-all-fail-3/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('Throwing error', () => { 2 | for (let i = 0; i < 3; i++) { 3 | it(`case ${i}`, () => { 4 | throw new Error('foo'); 5 | }); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /test/retries-all-fail/README: -------------------------------------------------------------------------------- 1 | --retries option support where all retries fail 2 | -------------------------------------------------------------------------------- /test/retries-all-fail/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | const MochaParallelTests = require('../../dist/main/mocha').default; 6 | const { setProcessExitListeners } = require('../../dist/util'); 7 | const SilentReporter = require('../util/silent-reporter'); 8 | 9 | setProcessExitListeners(); 10 | 11 | const onRunnerEnd = sinon.fake(); 12 | const mocha = new MochaParallelTests; 13 | 14 | mocha.reporter(SilentReporter); 15 | mocha.suite.retries(2); 16 | mocha.addFile(`${__dirname}/index.spec.js`); 17 | mocha.run().on('end', onRunnerEnd); 18 | 19 | process.on('exit', () => { 20 | assert.strictEqual(onRunnerEnd.callCount, 1, `runner "end" event occured wrong number of times: ${onRunnerEnd.callCount}`); 21 | }); 22 | -------------------------------------------------------------------------------- /test/retries-all-fail/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('suite', () => { 2 | it('case', () => { 3 | throw new Error('fail'); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/retries/README: -------------------------------------------------------------------------------- 1 | --retries option support 2 | -------------------------------------------------------------------------------- /test/retries/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const MochaParallelTests = require('../../dist/main/mocha').default; 5 | const { setProcessExitListeners } = require('../../dist/util'); 6 | const SilentReporter = require('../util/silent-reporter'); 7 | 8 | setProcessExitListeners(); 9 | 10 | const mocha = new MochaParallelTests; 11 | mocha.reporter(SilentReporter); 12 | mocha.suite.retries(2); 13 | mocha.addFile(`${__dirname}/index.spec.js`); 14 | 15 | mocha.run().on('end', function () { 16 | assert.strictEqual(this.stats.passes, 1, `Passes number is wrong: ${this.stats.passes}`); 17 | assert.strictEqual(this.stats.failures, 0, `Failures number is wrong: ${this.stats.failures}`); 18 | }); 19 | -------------------------------------------------------------------------------- /test/retries/index.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | let run = 0; 3 | 4 | describe('suite', () => { 5 | it('case', () => { 6 | run++; 7 | assert(run === 3); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/run-programmatically/_spec/dummy.js: -------------------------------------------------------------------------------- 1 | describe('Dummy suite', () => { 2 | it('should finish now', () => { 3 | // pass 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/run-programmatically/_spec/parallel1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should end after 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/run-programmatically/_spec/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should end after 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/run-programmatically/callback/README: -------------------------------------------------------------------------------- 1 | Check that mocha-parallel-tests can run programmatically 2 | -------------------------------------------------------------------------------- /test/run-programmatically/callback/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert'); 6 | const MochaParallelTests = require('../../../dist/main/mocha').default; 7 | 8 | const STREAMS = ['stdout', 'stderr']; 9 | const originalWrites = {}; 10 | const mocha = new MochaParallelTests; 11 | 12 | let failuresTotal; 13 | let jsonResult; 14 | let globalException; 15 | 16 | const patchStreams = () => { 17 | for (let streamName of STREAMS) { 18 | const stream = process[streamName]; 19 | originalWrites[streamName] = stream.write.bind(stream); 20 | 21 | // mute standard streams 22 | // also replace process.stdout.write with process.stderr.write 23 | // because this is current mocha behaviour 24 | stream.write = () => { 25 | return stream; 26 | }; 27 | } 28 | }; 29 | 30 | const restoreOriginalStreams = () => { 31 | for (let streamName of STREAMS) { 32 | const stream = process[streamName]; 33 | stream.write = originalWrites[streamName]; 34 | } 35 | }; 36 | 37 | process.on('exit', () => { 38 | restoreOriginalStreams(); 39 | 40 | assert(globalException === undefined, `Failed running mocha-parallel-tests: ${globalException && globalException.stack}`); 41 | 42 | assert(failuresTotal !== undefined, 'Run() callback was not executed'); 43 | assert.strictEqual(failuresTotal, 0, `Run() callback argument is wrong: ${failuresTotal}`); 44 | 45 | assert(jsonResult !== undefined, '"end" event was not fired'); 46 | assert(jsonResult !== null && typeof jsonResult === 'object', `Reporter output is not valid JSON: ${jsonResult}`); 47 | assert.strictEqual(jsonResult.stats.suites, 4); 48 | assert.strictEqual(jsonResult.stats.tests, 2); 49 | assert.strictEqual(jsonResult.stats.passes, 2); 50 | assert(jsonResult.stats.duration < 4000, `Duration is too long: ${jsonResult.stats.duration}`); 51 | }); 52 | 53 | // patch streams so that stdout is muted 54 | patchStreams(); 55 | 56 | try { 57 | mocha 58 | .reporter('json') 59 | .addFile(`${__dirname}/../_spec/parallel1.js`) 60 | .addFile(`${__dirname}/../_spec/parallel2.js`) 61 | .slow(8000) 62 | .timeout(10000) 63 | .run(failures => { 64 | failuresTotal = failures; 65 | }).on('end', function () { 66 | jsonResult = this.testResults || null; 67 | }); 68 | } catch (ex) { 69 | globalException = ex; 70 | } 71 | -------------------------------------------------------------------------------- /test/run-programmatically/reporter-done/README: -------------------------------------------------------------------------------- 1 | Check that reporter's "done" function is executed 2 | -------------------------------------------------------------------------------- /test/run-programmatically/reporter-done/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert'); 6 | const Mocha = require('mocha'); 7 | const MochaParallelTests = require('../../../dist/main/mocha').default; 8 | 9 | const mocha = new MochaParallelTests; 10 | let doneExecuted = false; 11 | 12 | class Reporter extends Mocha.reporters.Base { 13 | done(failsOccured, fn) { 14 | doneExecuted = true; 15 | fn && fn(failsOccured); 16 | } 17 | } 18 | 19 | mocha 20 | .reporter(Reporter) 21 | .addFile(`${__dirname}/../_spec/parallel1.js`) 22 | .addFile(`${__dirname}/../_spec/parallel2.js`) 23 | .slow(8000) 24 | .timeout(10000) 25 | .run(); 26 | 27 | process.on('exit', () => { 28 | assert(doneExecuted, 'reporter.done() has not been executed'); 29 | }); 30 | -------------------------------------------------------------------------------- /test/run-programmatically/tty-worker/README: -------------------------------------------------------------------------------- 1 | Check that spec reporter can work in a non-TTY window 2 | TODO: this test fails when you run index.js from the terminal but doesn't fail when it's executed from tests/index.sh 3 | -------------------------------------------------------------------------------- /test/run-programmatically/tty-worker/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const MochaParallelTests = require('../../../dist/main/mocha').default; 6 | const mocha = new MochaParallelTests(); 7 | 8 | mocha 9 | .reporter('spec') 10 | .addFile(`${__dirname}/../_spec/dummy.js`) 11 | .run(); 12 | 13 | process.on('unhandledRejection', (reason) => { 14 | console.log(`Unhandled promise rejection: ${reason.stack}`); 15 | process.exit(1); 16 | }); 17 | -------------------------------------------------------------------------------- /test/skip-suite/README: -------------------------------------------------------------------------------- 1 | Check that xdescribe, xcontext, describe.skip() skips tests 2 | -------------------------------------------------------------------------------- /test/skip-suite/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js test/skip-suite/test.js 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 0 ]; then 7 | exit 0 8 | else 9 | echo "Exit code is wrong: $STATUS" 10 | echo "Output: $OUTPUT" 11 | 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /test/skip-suite/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | describe.skip('skipped describe block with describe.skip', () => { 6 | it('should be skipped', () => { 7 | assert(false); 8 | }); 9 | 10 | it('should be skipped', () => { 11 | assert(false); 12 | }); 13 | }); 14 | 15 | xdescribe('skipped describe block with xdescribe', () => { 16 | it('should be skipped', () => { 17 | assert(false); 18 | }); 19 | 20 | it('should be skipped', () => { 21 | assert(false); 22 | }); 23 | }); 24 | 25 | xcontext('skipped describe block with xcontext', () => { 26 | it('should be skipped', () => { 27 | assert(false); 28 | }); 29 | 30 | it('should be skipped', () => { 31 | assert(false); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/skip-test/README: -------------------------------------------------------------------------------- 1 | Check that xit, xcontext, it.skip skips test 2 | -------------------------------------------------------------------------------- /test/skip-test/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(dist/bin/cli.js test/skip-test/test.js 2>&1) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 0 ]; then 7 | exit 0 8 | else 9 | echo "Exit code is wrong: $STATUS" 10 | echo "Output: $OUTPUT" 11 | 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /test/skip-test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | describe('describe block', () => { 6 | xit('should be skipped with xit', () => { 7 | assert(false); 8 | }); 9 | 10 | xspecify('should be skipped with xspecify', () => { 11 | assert(false); 12 | }); 13 | 14 | it.skip('should be skipped with it.skip', () => { 15 | assert(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/syntax-errors/README: -------------------------------------------------------------------------------- 1 | Check that syntax errors inside tests result in error code 2 | -------------------------------------------------------------------------------- /test/syntax-errors/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const path = require('path'); 5 | const exec = require('child_process').exec; 6 | const libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 7 | 8 | exec(`${libExecutable} test/syntax-errors/tests/`, { 9 | cwd: path.resolve(__dirname, '../../') 10 | }, function (err) { 11 | // node.js doesn't support destructuring assignment 12 | const majorNodeVersion = Number(process.versions.node.split('.')[0]); 13 | const shouldProduceError = (majorNodeVersion < 6); 14 | 15 | if (shouldProduceError) { 16 | assert(err, `Error should have happened (node version ${process.versions.node})`); 17 | assert.strictEqual(err.code, 1); 18 | } else { 19 | assert.strictEqual(err, null, 'Unexpected error occured'); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /test/syntax-errors/tests/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', () => { 4 | it('should do smth', () => {}); 5 | }); 6 | -------------------------------------------------------------------------------- /test/syntax-errors/tests/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {readFile} = require('fs'); 4 | module.exports = readFile; 5 | -------------------------------------------------------------------------------- /test/timeouts-exit-code/README: -------------------------------------------------------------------------------- 1 | Check that tests with timeouts exit with error status code 2 | -------------------------------------------------------------------------------- /test/timeouts-exit-code/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dist/bin/cli.js --timeout 3000 --slow 30000 test/timeouts-exit-code/suite.js 1>/dev/null 2>&1 4 | MPT_STATUS_CODE=$? 5 | 6 | node_modules/.bin/mocha --timeout 3000 --slow 30000 test/timeouts-exit-code/suite.js 1>/dev/null 2>&1 7 | MOCHA_STATUS_CODE=$? 8 | 9 | # currently mocha exits with 2 if 2 tests failed, 3 if 3 tests failed etc 10 | # though it's a strange behaviour, mocha-parallel-tests behaviour should be the same 11 | if [ $MPT_STATUS_CODE -eq $MOCHA_STATUS_CODE ]; then 12 | exit 0 13 | else 14 | echo "Timeout status code is not valid: $MPT_STATUS_CODE (actual) vs $MOCHA_STATUS_CODE (expected)" 15 | exit 1 16 | fi 17 | -------------------------------------------------------------------------------- /test/timeouts-exit-code/tests/1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Run async test', function () { 4 | /* eslint-disable no-unused-vars */ 5 | it('should fail with timeout', function (done) { 6 | 7 | }); 8 | 9 | it('should fail with timeout #2', function (done) { 10 | 11 | }); 12 | /* eslint-enable no-unused-vars */ 13 | }); 14 | -------------------------------------------------------------------------------- /test/timeouts-exit-code/tests/2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Run async test', function () { 4 | /* eslint-disable no-unused-vars */ 5 | it('should fail with timeout', function (done) { 6 | 7 | }); 8 | /* eslint-enable no-unused-vars */ 9 | }); 10 | -------------------------------------------------------------------------------- /test/total-time/README: -------------------------------------------------------------------------------- 1 | Check that total tests time is the mocha-parallel-tests time 2 | -------------------------------------------------------------------------------- /test/total-time/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var assert = require('assert'); 4 | var path = require('path'); 5 | var exec = require('child_process').exec; 6 | var libExecutable = path.resolve(__dirname, '../../dist/bin/cli.js'); 7 | 8 | exec(libExecutable + ' -R json --timeout 60000 --slow 60000 --max-parallel 2 test/total-time/tests', { 9 | cwd: path.resolve(__dirname, '../../') 10 | }, function (err, stdout) { 11 | if (err) { 12 | console.error(err); 13 | process.exit(1); 14 | } 15 | 16 | var jsonReporterOutput = stdout.toString(); 17 | 18 | try { 19 | jsonReporterOutput = JSON.parse(jsonReporterOutput); 20 | } catch (ex) { 21 | console.error('Native JSON reporter output is not valid JSON: ' + jsonReporterOutput); 22 | process.exit(1); 23 | } 24 | 25 | // when parallel1.js is over it's about 3000ms passed from the beginning 26 | // by this time parallel2.js has been started executing and is still on 27 | // parallel2.js should finish after 5 seconds from the beginning 28 | // parallel3.js starts right after parallel1.js finishes, so its finish time is about 6000ms 29 | assert.ok(jsonReporterOutput.stats.duration >= 6000, 'Tests duration is too small'); 30 | assert.ok(jsonReporterOutput.stats.duration < 7000, 'Tests duration is too big'); 31 | }); 32 | -------------------------------------------------------------------------------- /test/total-time/tests/parallel1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #1', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/total-time/tests/parallel2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #2', function () { 4 | it('should end in 5 seconds', function (done) { 5 | setTimeout(done, 5000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/total-time/tests/parallel3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Test suite #3', function () { 4 | it('should end in 3 seconds', function (done) { 5 | setTimeout(done, 3000); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/ui-option/README: -------------------------------------------------------------------------------- 1 | Check that mocha-parallel-tests supports --ui option 2 | -------------------------------------------------------------------------------- /test/ui-option/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(../../dist/bin/cli.js --ui tdd) 4 | STATUS=$? 5 | 6 | if [ $STATUS -eq 0 ]; then 7 | exit 0 8 | else 9 | echo "Exit code is wrong: $STATUS" 10 | echo "Output: $OUTPUT" 11 | 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /test/ui-option/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require( 'assert' ).strict.ok; 4 | 5 | // Assert 'tdd' interface is present 6 | assert(typeof global.suite === 'function', 'Expected global.suite() to be a function'); 7 | assert(typeof global.test === 'function', 'Expected global.test() to be a function'); 8 | -------------------------------------------------------------------------------- /test/uncaught-exception/README: -------------------------------------------------------------------------------- 1 | Make sure that uncaught exceptions do not stop executing the tests 2 | -------------------------------------------------------------------------------- /test/uncaught-exception/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | 6 | const MochaParallelTests = require('../../dist/main/mocha').default; 7 | const { setProcessExitListeners } = require('../../dist/util'); 8 | const SilentReporter = require('../util/silent-reporter'); 9 | 10 | setProcessExitListeners(); 11 | 12 | const onRunnerEnd = sinon.fake(); 13 | const onTestEnd = sinon.fake(); 14 | const mocha = new MochaParallelTests; 15 | mocha.reporter(SilentReporter); 16 | mocha.addFile(`${__dirname}/index.spec.js`); 17 | 18 | mocha.run() 19 | .on('end', onRunnerEnd) 20 | .on('test end', onTestEnd); 21 | 22 | process.on('exit', () => { 23 | assert.strictEqual(onRunnerEnd.callCount, 1, `runner "end" event occured wrong number of times: ${onRunnerEnd.callCount}`); 24 | assert.strictEqual(onTestEnd.callCount, 4, `runner "test end" event occured wrong number of times: ${onTestEnd.callCount}`); 25 | }); 26 | -------------------------------------------------------------------------------- /test/uncaught-exception/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('suite', function () { 2 | it('case 1', function () { 3 | throw new Error('foo'); 4 | }); 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | it('case 2', function (done) { 8 | setTimeout(() => { 9 | throw new Error('foo'); 10 | }, 100); 11 | }); 12 | 13 | it('case 3', () => {}); 14 | 15 | it('case 4', () => { 16 | return new Promise(() => { 17 | throw new Error('foo'); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/util/events-reporter.js: -------------------------------------------------------------------------------- 1 | const Base = require('mocha').reporters.Base; 2 | 3 | module.exports = class Reporter extends Base { 4 | constructor(runner) { 5 | super(runner); 6 | 7 | [ 8 | 'waiting', 9 | 'start', 10 | 'end', 11 | 'suite', 12 | 'suite end', 13 | 'test', 14 | 'test end', 15 | 'pass', 16 | 'fail', 17 | 'pending', 18 | 'hook', 19 | 'hook end', 20 | ].forEach((evtName) => { 21 | runner.on(evtName, this.onRunnerEventFired.bind(this, evtName)); 22 | }); 23 | } 24 | 25 | onRunnerEventFired(evtName) { 26 | console.log(evtName); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/util/silent-reporter.js: -------------------------------------------------------------------------------- 1 | const Base = require('mocha').reporters.Base; 2 | module.exports = class SilentReporter extends Base {}; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "allowJs": false, 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "classic", 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitAny": false, 12 | "noImplicitThis": false, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "sourceMap": true, 16 | "strictNullChecks": true, 17 | "target": "esnext" 18 | }, 19 | "include": [ 20 | "./src/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------