├── .gitignore ├── .circleci └── config.yml ├── test └── dummy-spec.js ├── .eslintrc.json ├── LICENSE ├── package.json ├── verify.js ├── README.md ├── verify └── mocha-multi.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | file.* 17 | .idea 18 | 19 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | workflows: 3 | workflow: 4 | jobs: 5 | - test 6 | jobs: 7 | test: 8 | docker: 9 | - image: cimg/node:lts 10 | steps: 11 | - checkout 12 | - run: npm ci 13 | - run: npm run ci 14 | -------------------------------------------------------------------------------- /test/dummy-spec.js: -------------------------------------------------------------------------------- 1 | describe('A test', () => { 2 | it("isn't really a test", () => { 3 | 1 + 1; 4 | }); 5 | it('is only here to create output', () => { 6 | 2 + 2; 7 | }); 8 | it('runs through and generates data', () => { 9 | throw new Error('to check that reporting is sorta working'); 10 | }); 11 | it('handles pending stuff as well'); 12 | }); 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["node"], 3 | "extends": ["airbnb-base", "plugin:node/recommended"], 4 | "rules": { 5 | "global-require": 0, 6 | "import/no-dynamic-require": 0, 7 | "import/no-extraneous-dependencies": 0, 8 | "no-process-exit": 0, 9 | "no-underscore-dangle": 0, 10 | "no-unused-expressions": 0, 11 | "node/no-unpublished-require": 0, 12 | "operator-linebreak": [ "error", "after" ], 13 | "strict": 0 14 | }, 15 | "overrides": [ 16 | { 17 | "files": ["test/**"], 18 | "env": { 19 | "mocha": true 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Glen Mailer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocha-multi", 3 | "version": "1.1.7", 4 | "description": "A bit of a hack to get multiple reporters working with mocha", 5 | "main": "mocha-multi.js", 6 | "scripts": { 7 | "test": "run-s lint verify:*", 8 | "ci": "run-s -c lint verify:*", 9 | "verify:node": "node ./verify.js", 10 | "verify:sh": "./verify all", 11 | "lint": "eslint . --max-warnings 0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/glenjamin/mocha-multi.git" 16 | }, 17 | "keywords": [ 18 | "mocha" 19 | ], 20 | "author": "Glen Mailer ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/glenjamin/mocha-multi/issues" 24 | }, 25 | "homepage": "https://github.com/glenjamin/mocha-multi", 26 | "devDependencies": { 27 | "async": "^3.0.1", 28 | "chalk": "^2.4.2", 29 | "eslint": "^5.16.0", 30 | "eslint-config-airbnb-base": "^13.1.0", 31 | "eslint-plugin-import": "^2.17.3", 32 | "eslint-plugin-node": "^9.1.0", 33 | "mocha": "^9.0.0", 34 | "npm-run-all": "^4.1.5", 35 | "q": "^1.5.1", 36 | "should": "^13.2.3" 37 | }, 38 | "dependencies": { 39 | "debug": "^4.1.1", 40 | "is-string": "^1.0.4", 41 | "lodash.once": "^4.1.1", 42 | "mkdirp": "^1.0.4", 43 | "object-assign": "^4.1.1" 44 | }, 45 | "peerDependencies": { 46 | "mocha": ">=2.2.0 <7 || >=9" 47 | }, 48 | "engines": { 49 | "node": ">=6.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /verify.js: -------------------------------------------------------------------------------- 1 | const Mocha = require('mocha'); 2 | const should = require('should'); 3 | const fs = require('fs'); 4 | const debug = require('debug')('mocha:verify:multi'); 5 | const async = require('async'); 6 | const chalk = require('chalk'); 7 | 8 | const reporters = [ 9 | 'dot', 'doc', 'spec', 'json', 'progress', 10 | 'list', 'tap', 'landing', 'xunit', 'min', 11 | 'json-stream', 'markdown', 'nyan', 12 | ]; 13 | const now = new Date(); 14 | 15 | function tempName(reporter) { 16 | return `/tmp/mocha-multi.${reporter}.${+now}`; 17 | } 18 | 19 | const reportersWithOptions = [] 20 | .concat(reporters.map((reporter) => { 21 | const outFilename = tempName(`${reporter}-stdout`); 22 | const options = {}; 23 | options[reporter] = { 24 | stdout: outFilename, 25 | }; 26 | return { 27 | testName: `${reporter} (with options.stdout)`, 28 | outFilename, 29 | options, 30 | }; 31 | })) 32 | .concat(reporters.map((reporter) => { 33 | const outFilename = tempName(`${reporter}-str`); 34 | const options = {}; 35 | options[reporter] = outFilename; 36 | return { 37 | testName: `${reporter} (with options as string)`, 38 | outFilename, 39 | options, 40 | }; 41 | })); 42 | 43 | 44 | should(process.env.multi).not.be.ok; 45 | 46 | process.setMaxListeners(reportersWithOptions.length); 47 | 48 | async.eachSeries(reportersWithOptions, (reporter, next) => { 49 | debug('reporter %s', reporter.testName); 50 | debug('reporterOptions %j', reporter.options); 51 | const mocha = new Mocha({ 52 | ui: 'bdd', 53 | reporter: 'mocha-multi', 54 | reporterOptions: reporter.options, 55 | }); 56 | mocha.addFile('test/dummy-spec.js'); 57 | mocha.run(() => { 58 | debug('done running %j', reporter.testName); 59 | process.nextTick(next); 60 | }); 61 | }, () => { 62 | reportersWithOptions.forEach((reporter) => { 63 | fs.statSync.bind(fs, reporter.outFilename).should.not.throw(); 64 | fs.unlinkSync(reporter.outFilename); 65 | // eslint-disable-next-line no-console 66 | console.log(chalk.green('%s OK'), reporter.testName); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mocha-multi 2 | =========== 3 | 4 | A bit of a hack to get multiple reporters working with mocha 5 | 6 | [![Build Status](https://travis-ci.org/glenjamin/mocha-multi.svg?branch=master)](https://travis-ci.org/glenjamin/mocha-multi) 7 | [![NPM version](https://img.shields.io/npm/v/mocha-multi.svg)](https://www.npmjs.com/package/mocha-multi) 8 | 9 | Usage 10 | ----- 11 | 12 | npm install mocha-multi --save-dev 13 | mocha --reporter mocha-multi 14 | 15 | Choosing Reporters 16 | ------------------ 17 | 18 | For both methods below, the special value of `-` (hyphen) for destination uses normal stdout/stderr. 19 | 20 | ### With the `multi` Environment Variable 21 | 22 | Set the environment variable `multi` to whitespace-separated type=destination pairs. 23 | 24 | ```bash 25 | multi='dot=- xunit=file.xml doc=docs.html' mocha -R mocha-multi 26 | ``` 27 | 28 | ### With `--reporter-options` 29 | 30 | Pass `--reporter-options` with comma-separated type=destination pairs. 31 | 32 | ```bash 33 | mocha -R mocha-multi --reporter-options dot=-,xunit=file.xml,doc=docs.html 34 | ``` 35 | 36 | ### From a file 37 | 38 | Using either of the above methods, include a type=destination pair where the type is mocha-multi and the destination is a filename, e.g. `mocha-multi=mocha-multi-reporters.json` 39 | 40 | More reporters will be loaded from the named file, which must be valid JSON in the same data format described below for passing reporterOptions to Mocha programmatically. 41 | 42 | Using mocha-multi programmatically 43 | ---------------------------------- 44 | 45 | You may specify the desired reporters (and their options) by passing `reporterOptions` to the Mocha contructor. 46 | 47 | For example: the following config is the equivalent of setting `multi='spec=- Progress=/tmp/mocha-multi.Progress.out'`, with the addition of passing the `verbose: true` option to the Progress reporter. 48 | 49 | ```sh 50 | var reporterOptions = { 51 | Progress: { 52 | stdout: "/tmp/mocha-multi.Progress.out", 53 | options: { 54 | verbose: true 55 | } 56 | }, 57 | spec: "-" 58 | }; 59 | 60 | var mocha = new Mocha({ 61 | ui: "bdd" 62 | reporter: "mocha-multi", 63 | reporterOptions: reporterOptions 64 | }); 65 | mocha.addFile("test/dummy-spec.js"); 66 | mocha.run(function onRun(failures){ 67 | console.log(failures); 68 | }); 69 | ``` 70 | 71 | The options will be passed as the second argument to the reporter constructor. 72 | 73 | How it works 74 | ------------ 75 | 76 | A big hack that keeps changing the value of process.stdout and process.stderr whenever a reporter is doing its thing. 77 | 78 | Seriously? 79 | ---------- 80 | 81 | Yeah, Sorry! 82 | 83 | All the hacks 84 | ------------- 85 | 86 | This is very hacky, specifically: 87 | 88 | * The `process` and `console` objects get their internal state messed with 89 | * `process.exit` is hacked to wait for streams to finish writing 90 | * Only works if reporters queue writes synchronously in event handlers 91 | 92 | Could this be a bit less hacky? 93 | ------------------------------- 94 | 95 | * Now that https://github.com/mochajs/mocha/pull/1059 is released the process.exit hack could maybe be tidier 96 | 97 | * Having each reporter run in a child process would make it eaiser to capture their streams, but might lead to other issues 98 | 99 | TODO 100 | ---- 101 | 102 | * Add tests for coverage reports 103 | * Add tests which produce multiple reports at once 104 | * Add test for help text 105 | * Add test that uses --no-exit 106 | 107 | HISTORY 108 | ------- 109 | 110 | ### 1.0.0 (unreleased) 111 | 112 | The breaking changes are mostly around internals, and shouldn't affect most people. 113 | 114 | * BREAKING: MochaMulti.prototype.done removed, new MochaMulti(...).done now optional 115 | * BREAKING: new MochaMulti(...).options removed 116 | * BREAKING: Must run at least mocha@>=2.2.0 117 | * BREAKING: Must run at least node@>=6.0.0 118 | * Correctly set exit code when writing to files 119 | * Declare support for mocha@^4.0.0 120 | * Support running mocha without a run callback 121 | * Upgrade to ES2015+ via eslint-preset-airbnb-base (MochaMulti is an ES class) 122 | * Avoid patching stderr, now that mocha does not write to it 123 | -------------------------------------------------------------------------------- /verify: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mocha_multi="mocha-multi.js" 4 | normal="\033[0m" 5 | 6 | function log { 7 | local color 8 | if [ "$2" = "info" ]; then 9 | color="" # normal 10 | elif [ "$2" = "fail" ]; then 11 | color="\033[01;31m" # red 12 | elif [ "$2" = "pass" ]; then 13 | color="\033[01;32m" # green 14 | else 15 | color="\033[01;30m" # grey 16 | fi 17 | echo -e "${color}VERIFY: $1${normal}" 1>&2 18 | } 19 | 20 | function normalise_timers { 21 | local file=$1 22 | cmd="sed -i'.bak' -e 's/[0-9]\{1,\}\(\.[0-9]\{1,\}\)\{0,1\}/0/g' '$file'" 23 | eval $cmd 24 | rm "$file.bak" 25 | } 26 | 27 | # pipe through cat to ensure istty = false, but capture exit code 28 | function eval_notty { 29 | log "Running formatter $1: $2" 30 | eval $2 | cat 31 | return ${PIPESTATUS[0]} 32 | } 33 | 34 | function compare { 35 | local reporter=$1 36 | 37 | log "Running comparison for $reporter" info 38 | 39 | local builtin_out=$(mktemp /tmp/mocha-multi.XXXXXXXXX) 40 | local multi_env_out=$(mktemp /tmp/mocha-multi.XXXXXXXXX) 41 | local multi_arg_out=$(mktemp /tmp/mocha-multi.XXXXXXXXX) 42 | local multi_file_in=$(mktemp /tmp/mocha-multi.XXXXXXXXX) 43 | local multi_file_out=$(mktemp /tmp/mocha-multi.XXXXXXXXX) 44 | 45 | local builtin_cmd="mocha -R $reporter &> $builtin_out" 46 | eval_notty "normally" "$builtin_cmd" 47 | local builtin_result=$? 48 | normalise_timers $builtin_out 49 | 50 | local multi_env_cmd="multi='$reporter=$multi_env_out' mocha -R $mocha_multi" 51 | eval_notty "via mocha-multi using environment variable" "$multi_env_cmd" 52 | local multi_env_result=$? 53 | normalise_timers $multi_env_out 54 | 55 | local multi_arg_cmd="mocha -R $mocha_multi --reporter-options '$reporter=$multi_arg_out'" 56 | eval_notty "via mocha-multi using --reporter-options" "$multi_arg_cmd" 57 | local multi_arg_result=$? 58 | normalise_timers $multi_arg_out 59 | 60 | echo "{\"$reporter\": \"$multi_file_out\"}" > "$multi_file_in" 61 | local multi_file_cmd="mocha -R $mocha_multi --reporter-options 'mocha-multi=$multi_file_in'" 62 | eval_notty "via mocha-multi using file" "$multi_file_cmd" 63 | local multi_file_result=$? 64 | normalise_timers $multi_file_out 65 | 66 | log "Comparing exit codes" 67 | 68 | if [ "$builtin_result" = "$multi_arg_result" -a "$builtin_result" = "$multi_env_result" -a "$builtin_result" = "$multi_file_result" ]; then 69 | log 'Codes match, hooray!' pass 70 | else 71 | log "Codes do not match" fail 72 | log "Result ${normal}$builtin_result" 73 | log "Result (env) ${normal}$multi_env_result" 74 | log "Result (arg) ${normal}$multi_arg_result" 75 | log "Result (file) ${normal}$multi_file_result" 76 | return 1 77 | fi 78 | 79 | log "Comparing output" 80 | 81 | local diff_cmd_env="diff -U1 -Lbuiltin -Lmulti $builtin_out $multi_env_out" 82 | log "Running $diff_cmd_env" 83 | local difference_env=$($diff_cmd_env) 84 | 85 | local diff_cmd_arg="diff -U1 -Lbuiltin -Lmulti $builtin_out $multi_arg_out" 86 | log "Running $diff_cmd_arg" 87 | local difference_arg=$($diff_cmd_arg) 88 | 89 | local diff_cmd_file="diff -U1 -Lbuiltin -Lmulti $builtin_out $multi_file_out" 90 | log "Running $diff_cmd_file" 91 | local difference_file=$($diff_cmd_file) 92 | 93 | rm "$builtin_out" "$multi_env_out" "$multi_arg_out" "$multi_file_out" "$multi_file_in" 94 | 95 | if [ "$difference_env" = "" -a "$difference_arg" = "" -a "$difference_file" = "" ]; then 96 | log 'Output matches, hooray!' pass 97 | return 0 98 | else 99 | log "Output does not match" fail 100 | log "Difference (env)\n${normal}$difference_env" 101 | log "Difference (arg)\n${normal}$difference_arg" 102 | log "Difference (file)\n${normal}$difference_file" 103 | return 1 104 | fi 105 | } 106 | 107 | badfilecheck="ERROR: Missing or malformed options file 'missing.json' -- Error: ENOENT: no such file or directory, open 'missing.json'" 108 | badfile=`mocha --reporter "$mocha_multi" --reporter-options mocha-multi=missing.json --recursive test 2>&1` 109 | log "$badfile" 110 | if [ "$badfile" != "$badfilecheck" ]; then 111 | log "Wrong error message trying to load missing options file" fail 112 | exit 1 113 | else 114 | log "Correct error message trying to load missing options file" pass 115 | fi 116 | 117 | if [ "$1" = "" ]; then 118 | log "No reporter chosen" 119 | exit 1 120 | fi 121 | 122 | if [ "$1" = "all" ]; then 123 | reporters=(\ 124 | dot doc spec json progress \ 125 | list tap landing xunit min \ 126 | json-stream markdown nyan\ 127 | ) 128 | result=0 129 | for reporter in ${reporters[@]}; do 130 | compare $reporter 131 | result=$(($result + $?)) 132 | done 133 | exit $result 134 | else 135 | compare $1 136 | fi 137 | -------------------------------------------------------------------------------- /mocha-multi.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const once = require('lodash.once'); 3 | const util = require('util'); 4 | const assign = require('object-assign'); 5 | const debug = require('debug')('mocha:multi'); 6 | const path = require('path'); 7 | const isString = require('is-string'); 8 | const mkdirp = require('mkdirp'); 9 | 10 | // Let mocha decide about tty early 11 | require('mocha/lib/reporters/base'); 12 | 13 | // Make sure we don't lose these! 14 | const { stdout } = process; 15 | 16 | function defineGetter(obj, prop, get, set) { 17 | Object.defineProperty(obj, prop, { get, set }); 18 | } 19 | const waitOn = fn => v => new Promise(resolve => fn(v, () => resolve())); 20 | const waitStream = waitOn((r, fn) => r.end(fn)); 21 | 22 | function awaitOnExit(waitFor) { 23 | if (!waitFor) { 24 | return; 25 | } 26 | const { exit } = process; 27 | process.exit = function mochaMultiExitPatch(...args) { 28 | const quit = exit.bind(this, ...args); 29 | if (process._exiting) { 30 | return quit(); 31 | } 32 | waitFor().then(quit); 33 | return undefined; 34 | }; 35 | } 36 | 37 | function identity(x) { 38 | return x; 39 | } 40 | 41 | const msgs = { 42 | no_definitions: 'reporter definitions should be set in ' + 43 | 'the `multi` shell variable\n' + 44 | "eg. `multi='dot=- xunit=file.xml' mocha`", 45 | invalid_definition: "'%s' is an invalid definition\n" + 46 | 'expected =', 47 | invalid_reporter: "Unable to find '%s' reporter", 48 | invalid_setup: "Invalid setup for reporter '%s' (%s)", 49 | invalid_outfile: "Invalid stdout filename for reporter '%s' (%s)", 50 | bad_file: "Missing or malformed options file '%s' -- Error: %s", 51 | }; 52 | function bombOut(id, ...args) { 53 | const newArgs = [`ERROR: ${msgs[id]}`, ...args]; 54 | process.stderr.write(`${util.format(...newArgs)}\n`); 55 | process.exit(1); 56 | } 57 | 58 | function parseReporter(definition) { 59 | const pair = definition.split('='); 60 | if (pair.length !== 2) { 61 | bombOut('invalid_definition', definition); 62 | } 63 | return pair; 64 | } 65 | 66 | function convertSetup(reporters) { 67 | let setup = []; 68 | Object.keys(reporters).forEach((reporter) => { 69 | if (reporter === 'mocha-multi') { 70 | debug('loading reporters from file %j', reporters[reporter]); 71 | try { 72 | setup = setup.concat(convertSetup(JSON.parse(fs.readFileSync(reporters[reporter])))); 73 | } catch (e) { 74 | bombOut('bad_file', reporters[reporter], e.message); 75 | } 76 | } else { 77 | const r = reporters[reporter]; 78 | debug('adding reporter %j %j', reporter, r); 79 | if (isString(r)) { 80 | setup.push([reporter, r, null]); 81 | } else if (typeof r !== 'object') { 82 | bombOut('invalid_setup', reporter, typeof r); 83 | } else { 84 | if (typeof r.stdout !== 'string') { bombOut('invalid_setup', reporter, typeof r); } 85 | setup.push([reporter, r.stdout, r.options]); 86 | } 87 | } 88 | }); 89 | return setup; 90 | } 91 | 92 | function parseSetup() { 93 | const reporterDefinition = process.env.multi || ''; 94 | const reporterDefs = reporterDefinition.trim().split(/\s/).filter(identity); 95 | if (!reporterDefs.length) { bombOut('no_definitions'); } 96 | debug('Got reporter defs: %j', reporterDefs); 97 | const reporters = {}; // const but not readonly 98 | reporterDefs.forEach((def) => { 99 | const [reporter, r] = parseReporter(def); 100 | reporters[reporter] = r; 101 | }); 102 | return convertSetup(reporters); 103 | } 104 | 105 | function resolveStream(destination) { 106 | if (destination === '-') { 107 | debug("Resolved stream '-' into stdout and stderr"); 108 | return null; 109 | } 110 | debug("Resolved stream '%s' into writeable file stream", destination); 111 | // Create directory if not existing 112 | const destinationDir = path.dirname(destination); 113 | if (!fs.existsSync(destinationDir)) { 114 | mkdirp.sync(destinationDir); 115 | } 116 | 117 | // Ensure we can write here 118 | fs.writeFileSync(destination, ''); 119 | return fs.createWriteStream(destination); 120 | } 121 | 122 | function safeRequire(module) { 123 | try { 124 | return require(module); 125 | } catch (err) { 126 | if (!/Cannot find/.exec(err.message)) { 127 | throw err; 128 | } 129 | return null; 130 | } 131 | } 132 | 133 | function resolveReporter(name) { 134 | // Cribbed from Mocha.prototype.reporter() 135 | const reporter = ( 136 | safeRequire(`mocha/lib/reporters/${name}`) || 137 | safeRequire(name) || 138 | safeRequire(path.resolve(process.cwd(), name)) || 139 | bombOut('invalid_reporter', name) 140 | ); 141 | debug("Resolved reporter '%s' into '%s'", name, util.inspect(reporter)); 142 | return reporter; 143 | } 144 | 145 | function withReplacedStdout(stream, func) { 146 | if (!stream) { 147 | return func(); 148 | } 149 | 150 | // The hackiest of hacks 151 | debug('Replacing stdout'); 152 | 153 | const stdoutGetter = Object.getOwnPropertyDescriptor(process, 'stdout').get; 154 | 155 | // eslint-disable-next-line no-console 156 | console._stdout = stream; 157 | defineGetter(process, 'stdout', () => stream); 158 | 159 | try { 160 | return func(); 161 | } finally { 162 | // eslint-disable-next-line no-console 163 | console._stdout = stdout; 164 | defineGetter(process, 'stdout', stdoutGetter); 165 | debug('stdout restored'); 166 | } 167 | } 168 | 169 | function createRunnerShim(runner, stream) { 170 | const shim = new (require('events').EventEmitter)(); 171 | 172 | function addDelegate(prop) { 173 | defineGetter(shim, prop, 174 | () => { 175 | const property = runner[prop]; 176 | if (typeof property === 'function') { 177 | return property.bind(runner); 178 | } 179 | return property; 180 | }, 181 | () => runner[prop]); 182 | } 183 | 184 | addDelegate('grepTotal'); 185 | addDelegate('suite'); 186 | addDelegate('total'); 187 | addDelegate('stats'); 188 | 189 | const delegatedEvents = {}; 190 | 191 | shim.on('newListener', (event) => { 192 | if (event in delegatedEvents) return; 193 | 194 | delegatedEvents[event] = true; 195 | debug("Shim: Delegating '%s'", event); 196 | 197 | runner.on(event, (...eventArgs) => { 198 | eventArgs.unshift(event); 199 | 200 | withReplacedStdout(stream, () => { 201 | shim.emit(...eventArgs); 202 | }); 203 | }); 204 | }); 205 | 206 | return shim; 207 | } 208 | 209 | function initReportersAndStreams(runner, setup, multiOptions) { 210 | return setup 211 | .map(([reporter, outstream, options]) => { 212 | debug("Initialising reporter '%s' to '%s' with options %j", reporter, outstream, options); 213 | 214 | const stream = resolveStream(outstream); 215 | const shim = createRunnerShim(runner, stream); 216 | 217 | debug("Shimming runner into reporter '%s' %j", reporter, options); 218 | 219 | return withReplacedStdout(stream, () => { 220 | const Reporter = resolveReporter(reporter); 221 | return { 222 | stream, 223 | reporter: new Reporter(shim, assign({}, multiOptions, { 224 | reporterOptions: options || {}, 225 | })), 226 | }; 227 | }); 228 | }); 229 | } 230 | 231 | function promiseProgress(items, fn) { 232 | let count = 0; 233 | fn(count); 234 | items.forEach(v => v.then(() => { 235 | count += 1; 236 | fn(count); 237 | })); 238 | return Promise.all(items); 239 | } 240 | 241 | 242 | /** 243 | * Override done to allow done processing for any reporters that have a done method. 244 | */ 245 | function done(failures, fn, reportersWithDone, waitFor = identity) { 246 | const count = reportersWithDone.length; 247 | const waitReporter = waitOn((r, f) => r.done(failures, f)); 248 | const progress = v => debug('Awaiting on %j reporters to invoke done callback.', count - v); 249 | promiseProgress(reportersWithDone.map(waitReporter), progress) 250 | .then(() => { 251 | debug('All reporters invoked done callback.'); 252 | }) 253 | .then(waitFor) 254 | .then(() => fn && fn(failures)); 255 | } 256 | 257 | function mochaMulti(runner, options) { 258 | // keep track of reporters that have a done method. 259 | const reporters = (options && options.reporterOptions); 260 | const setup = (() => { 261 | if (reporters && Object.keys(reporters).length > 0) { 262 | debug('options %j', options); 263 | return convertSetup(reporters); 264 | } 265 | return parseSetup(); 266 | })(); 267 | debug('setup %j', setup); 268 | // If the reporter possess a done() method register it so we can 269 | // wait for it to complete when done. 270 | const reportersAndStreams = initReportersAndStreams(runner, setup, options); 271 | const streams = reportersAndStreams 272 | .map(v => v.stream) 273 | .filter(identity); 274 | const reportersWithDone = reportersAndStreams 275 | .map(v => v.reporter) 276 | .filter(v => v.done); 277 | 278 | // we actually need to wait streams only if they are present 279 | const waitFor = streams.length > 0 ? 280 | once(() => Promise.all(streams.map(waitStream))) : 281 | undefined; 282 | 283 | awaitOnExit(waitFor); 284 | 285 | if (reportersWithDone.length > 0) { 286 | return { 287 | done: (failures, fn) => done(failures, fn, reportersWithDone, waitFor), 288 | }; 289 | } 290 | 291 | return {}; 292 | } 293 | 294 | class MochaMulti { 295 | constructor(runner, options) { 296 | Object.assign(this, mochaMulti(runner, options)); 297 | } 298 | } 299 | 300 | module.exports = MochaMulti; 301 | --------------------------------------------------------------------------------