From 6df4102dbe227cf99c8d483b183faa628ae39bc5 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 18 Apr 2019 16:11:11 +0200 Subject: [PATCH 1/3] feat(mocha 6): support all config formats Add support for all mocha's configuration formats: * Arguments specified on command-line * Configuration file (.mocharc.js, .mocharc.yml, etc.) 1. .mocharc.js 2. .mocharc.yaml 3. .mocharc.yml 4. .mocharc.jsonc 5. .mocharc.json * mocha property of package.json * mocha.opts This works by using mocha 6's [`loadOptions`](https://mochajs.org/api/module-lib_cli_options.html#.loadOptions) function if it is available. If not (mocha < 6), use old parsing logic. See https://mochajs.org/#configuring-mocha-nodejs --- packages/mocha-framework/package.json | 2 +- packages/mocha-runner/package.json | 2 +- packages/mocha-runner/src/LibWrapper.ts | 14 + .../mocha-runner/src/MochaConfigEditor.ts | 2 +- .../mocha-runner/src/MochaOptionsLoader.ts | 30 +- packages/mocha-runner/src/MochaTestRunner.ts | 5 +- packages/mocha-runner/src/utils.ts | 35 ++ .../integration/MochaOptionsLoader.it.spec.ts | 133 ++++++++ .../test/integration/SampleProject.it.spec.ts | 3 +- .../test/unit/MochaOptionsLoader.spec.ts | 322 +++++++++++------- .../test/unit/MochaTestRunner.spec.ts | 9 +- .../testResources/mocha-config/.mocharc.js | 16 + .../testResources/mocha-config/.mocharc.json | 14 + .../testResources/mocha-config/.mocharc.jsonc | 14 + .../testResources/mocha-config/.mocharc.yml | 47 +++ .../testResources/mocha-config/package.json | 10 + packages/mocha-runner/tsconfig.src.json | 3 +- packages/mocha-runner/tsconfig.test.json | 3 +- .../mocha.d.ts} | 6 +- 19 files changed, 525 insertions(+), 145 deletions(-) create mode 100644 packages/mocha-runner/test/integration/MochaOptionsLoader.it.spec.ts create mode 100644 packages/mocha-runner/testResources/mocha-config/.mocharc.js create mode 100644 packages/mocha-runner/testResources/mocha-config/.mocharc.json create mode 100644 packages/mocha-runner/testResources/mocha-config/.mocharc.jsonc create mode 100644 packages/mocha-runner/testResources/mocha-config/.mocharc.yml create mode 100644 packages/mocha-runner/testResources/mocha-config/package.json rename packages/mocha-runner/{src/MochaRunnerOptions.ts => typings/mocha.d.ts} (60%) diff --git a/packages/mocha-framework/package.json b/packages/mocha-framework/package.json index bafbf7ceab..b902cfa454 100644 --- a/packages/mocha-framework/package.json +++ b/packages/mocha-framework/package.json @@ -41,7 +41,7 @@ }, "peerDependencies": { "@stryker-mutator/core": "^1.0.0", - "mocha": ">= 2.3.3 < 6" + "mocha": ">= 2.3.3 < 7" }, "dependencies": { "@stryker-mutator/api": "^1.2.0" diff --git a/packages/mocha-runner/package.json b/packages/mocha-runner/package.json index ea272d41a7..1aebe0c555 100644 --- a/packages/mocha-runner/package.json +++ b/packages/mocha-runner/package.json @@ -46,6 +46,6 @@ }, "peerDependencies": { "@stryker-mutator/core": "^1.0.0", - "mocha": ">= 2.3.3 < 6" + "mocha": ">= 2.3.3 < 7" } } diff --git a/packages/mocha-runner/src/LibWrapper.ts b/packages/mocha-runner/src/LibWrapper.ts index bc7ff7e2d1..a5a2eae3bd 100644 --- a/packages/mocha-runner/src/LibWrapper.ts +++ b/packages/mocha-runner/src/LibWrapper.ts @@ -1,6 +1,19 @@ import * as Mocha from 'mocha'; import * as multimatch from 'multimatch'; +let loadOptions: undefined | ((argv?: string[] | string) => MochaOptions | undefined); + +try { + /* + * If read, object containing parsed arguments + * @since 6.0.0' + * @see https://mochajs.org/api/module-lib_cli_options.html#.loadOptions + */ + loadOptions = require('mocha/lib/cli/options').loadOptions; +} catch { + // Mocha < 6 doesn't support `loadOptions` +} + /** * Wraps Mocha class and require for testability */ @@ -8,4 +21,5 @@ export default class LibWrapper { public static Mocha = Mocha; public static require = require; public static multimatch = multimatch; + public static loadOptions = loadOptions; } diff --git a/packages/mocha-runner/src/MochaConfigEditor.ts b/packages/mocha-runner/src/MochaConfigEditor.ts index 076ee24d4d..4b0a1066b0 100644 --- a/packages/mocha-runner/src/MochaConfigEditor.ts +++ b/packages/mocha-runner/src/MochaConfigEditor.ts @@ -1,5 +1,5 @@ import { ConfigEditor, Config } from '@stryker-mutator/api/config'; -import { mochaOptionsKey } from './MochaRunnerOptions'; +import { mochaOptionsKey } from './utils'; import MochaOptionsLoader from './MochaOptionsLoader'; import { tokens } from '@stryker-mutator/api/plugin'; diff --git a/packages/mocha-runner/src/MochaOptionsLoader.ts b/packages/mocha-runner/src/MochaOptionsLoader.ts index 5c79f147e2..d2199816ed 100644 --- a/packages/mocha-runner/src/MochaOptionsLoader.ts +++ b/packages/mocha-runner/src/MochaOptionsLoader.ts @@ -1,9 +1,10 @@ import * as path from 'path'; import * as fs from 'fs'; import { StrykerOptions } from '@stryker-mutator/api/core'; -import MochaRunnerOptions, { mochaOptionsKey } from './MochaRunnerOptions'; import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; import { Logger } from '@stryker-mutator/api/logging'; +import { serializeArguments, filterConfig, mochaOptionsKey } from './utils'; +import LibWrapper from './LibWrapper'; export default class MochaOptionsLoader { @@ -12,12 +13,27 @@ export default class MochaOptionsLoader { public static inject = tokens(commonTokens.logger); constructor(private readonly log: Logger) { } - public load(config: StrykerOptions): MochaRunnerOptions { - const mochaOptions = Object.assign({}, config[mochaOptionsKey]) as MochaRunnerOptions; - return Object.assign(this.loadMochaOptsFile(mochaOptions.opts), mochaOptions); + public load(strykerOptions: StrykerOptions): MochaOptions { + const mochaOptions = Object.assign({}, strykerOptions[mochaOptionsKey]) as MochaOptions; + return Object.assign(this.loadMochaOptions(mochaOptions), mochaOptions); } - private loadMochaOptsFile(opts: false | string | undefined): MochaRunnerOptions { + private loadMochaOptions(overrides: MochaOptions) { + if (LibWrapper.loadOptions) { + this.log.debug('Mocha > 6 detected. Using mocha\'s `%s` to load mocha options', LibWrapper.loadOptions.name); + const args = serializeArguments(overrides); + const rawConfig = LibWrapper.loadOptions(args) || {}; + if (this.log.isTraceEnabled()) { + this.log.trace(`Mocha: ${LibWrapper.loadOptions.name}([${args.map(arg => `'${arg}'`).join(',')}]) => ${JSON.stringify(rawConfig)}`); + } + return filterConfig(rawConfig); + } else { + this.log.debug('Mocha < 6 detected. Using custom logic to parse mocha options'); + return this.loadMochaOptsFile(overrides.opts); + } + } + + private loadMochaOptsFile(opts: false | string | undefined): MochaOptions { switch (typeof opts) { case 'boolean': this.log.debug('Not reading additional mochaOpts from a file'); @@ -46,9 +62,9 @@ export default class MochaOptionsLoader { return this.parseOptsFile(fs.readFileSync(optsFileName, 'utf8')); } - private parseOptsFile(optsFileContent: string): MochaRunnerOptions { + private parseOptsFile(optsFileContent: string): MochaOptions { const options = optsFileContent.split('\n').map(val => val.trim()); - const mochaRunnerOptions: MochaRunnerOptions = Object.create(null); + const mochaRunnerOptions: MochaOptions = Object.create(null); options.forEach(option => { const args = option.split(' ').filter(Boolean); if (args[0]) { diff --git a/packages/mocha-runner/src/MochaTestRunner.ts b/packages/mocha-runner/src/MochaTestRunner.ts index cc68ef1ed7..c24e8b4126 100644 --- a/packages/mocha-runner/src/MochaTestRunner.ts +++ b/packages/mocha-runner/src/MochaTestRunner.ts @@ -3,8 +3,7 @@ import * as path from 'path'; import { TestRunner, RunResult, RunStatus } from '@stryker-mutator/api/test_runner'; import LibWrapper from './LibWrapper'; import { StrykerMochaReporter } from './StrykerMochaReporter'; -import MochaRunnerOptions, { mochaOptionsKey } from './MochaRunnerOptions'; -import { evalGlobal } from './utils'; +import { mochaOptionsKey, evalGlobal } from './utils'; import { StrykerOptions } from '@stryker-mutator/api/core'; import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; @@ -13,7 +12,7 @@ const DEFAULT_TEST_PATTERN = 'test/**/*.js'; export default class MochaTestRunner implements TestRunner { private testFileNames: string[]; - private readonly mochaRunnerOptions: MochaRunnerOptions; + private readonly mochaRunnerOptions: MochaOptions; public static inject = tokens(commonTokens.logger, commonTokens.sandboxFileNames, commonTokens.options); constructor(private readonly log: Logger, private readonly allFileNames: ReadonlyArray, options: StrykerOptions) { diff --git a/packages/mocha-runner/src/utils.ts b/packages/mocha-runner/src/utils.ts index 458600fb1f..7ef4872752 100644 --- a/packages/mocha-runner/src/utils.ts +++ b/packages/mocha-runner/src/utils.ts @@ -6,3 +6,38 @@ export function evalGlobal(body: string) { const fn = new Function('require', body); fn(require); } + +export function serializeArguments(mochaOptions: MochaOptions) { + const args: string[] = []; + Object.keys(mochaOptions).forEach(key => { + args.push(`--${key}`); + args.push((mochaOptions as any)[key].toString()); + }); + return args; +} + +export const mochaOptionsKey = 'mochaOptions'; + +const SUPPORTED_MOCHA_OPTIONS = Object.freeze([ + 'extension', + 'require', + 'timeout', + 'async-only', + 'ui', + 'grep', + 'exclude', + 'file' +]); + +/** + * Filter out those config values that are actually useful to run mocha with Stryker + * @param rawConfig The raw parsed mocha configuration + */ +export function filterConfig(rawConfig: { [key: string]: any }): MochaOptions { + return Object.keys(rawConfig).reduce((options, nextValue) => { + if (SUPPORTED_MOCHA_OPTIONS.some(o => nextValue === o)) { + (options as any)[nextValue] = (rawConfig as any)[nextValue]; + } + return options; + }, {} as MochaOptions); +} diff --git a/packages/mocha-runner/test/integration/MochaOptionsLoader.it.spec.ts b/packages/mocha-runner/test/integration/MochaOptionsLoader.it.spec.ts new file mode 100644 index 0000000000..f948444ee4 --- /dev/null +++ b/packages/mocha-runner/test/integration/MochaOptionsLoader.it.spec.ts @@ -0,0 +1,133 @@ +import * as path from 'path'; +import { testInjector } from '@stryker-mutator/test-helpers'; +import MochaOptionsLoader from '../../src/MochaOptionsLoader'; +import { expect } from 'chai'; +import { mochaOptionsKey } from '../../src/utils'; + +describe(`${MochaOptionsLoader.name} integration`, () => { + let sut: MochaOptionsLoader; + const cwd = process.cwd(); + + beforeEach(() => { + sut = createSut(); + }); + + afterEach(() => { + process.chdir(cwd); + }); + + it('should support loading from ".mocharc.js"', () => { + const configFile = resolveMochaConfig('.mocharc.js'); + const actualConfig = actLoad({ config: configFile }); + expect(actualConfig).deep.eq({ + config: configFile, + extension: ['js'], + timeout: 2000, + ui: 'bdd' + }); + }); + + it('should support loading from ".mocharc.json"', () => { + const configFile = resolveMochaConfig('.mocharc.json'); + const actualConfig = actLoad({ config: configFile }); + expect(actualConfig).deep.eq({ + config: configFile, + extension: ['json', 'js'], + timeout: 2000, + ui: 'bdd' + }); + }); + + it('should support loading from ".mocharc.jsonc"', () => { + const configFile = resolveMochaConfig('.mocharc.jsonc'); + const actualConfig = actLoad({ config: configFile }); + expect(actualConfig).deep.eq({ + config: configFile, + extension: ['jsonc', 'js'], + timeout: 2000, + ui: 'bdd' + }); + }); + + it('should support loading from ".mocharc.yml"', () => { + const configFile = resolveMochaConfig('.mocharc.yml'); + const actualConfig = actLoad({ config: configFile }); + expect(actualConfig).deep.eq({ + ['async-only']: false, + config: configFile, + exclude: [ + '/path/to/some/excluded/file' + ], + extension: [ + 'yml', + 'js' + ], + file: [ + '/path/to/some/file', + '/path/to/some/other/file' + ], + require: [ + '@babel/register' + ], + timeout: 0, + ui: 'bdd' + }); + }); + + it('should support loading from "package.json"', () => { + const pkgFile = resolveMochaConfig('package.json'); + const actualConfig = actLoad({ package: pkgFile }); + expect(actualConfig).deep.eq({ + ['async-only']: true, + extension: ['json', 'js'], + package: pkgFile, + timeout: 20, + ui: 'tdd' + }); + }); + + it('should respect mocha default file order', () => { + process.chdir(resolveMochaConfig('.')); + const actualConfig = actLoad({}); + expect(actualConfig).deep.eq({ + ['async-only']: true, + extension: [ + 'js', + 'json' + ], + timeout: 2000, + ui: 'bdd' + }); + }); + + it('should support `no-config`, `no-opts` and `no-package` keys', () => { + process.chdir(resolveMochaConfig('.')); + const actualConfig = actLoad({ + ['no-config']: true, + ['no-package']: true, + ['no-opts']: true + }); + const expectedOptions = { + extension: ['js'], + ['no-config']: true, + ['no-opts']: true, + ['no-package']: true, + timeout: 2000, + ui: 'bdd' + }; + expect(actualConfig).deep.eq(expectedOptions); + }); + + function resolveMochaConfig(relativeName: string) { + return path.resolve(__dirname, '..', '..', 'testResources', 'mocha-config', relativeName); + } + + function actLoad(mochaConfig: { [key: string]: any }): MochaOptions { + testInjector.options[mochaOptionsKey] = mochaConfig; + return sut.load(testInjector.options); + } + + function createSut() { + return testInjector.injector.injectClass(MochaOptionsLoader); + } +}); diff --git a/packages/mocha-runner/test/integration/SampleProject.it.spec.ts b/packages/mocha-runner/test/integration/SampleProject.it.spec.ts index 6d5f1541da..bebbc7c297 100644 --- a/packages/mocha-runner/test/integration/SampleProject.it.spec.ts +++ b/packages/mocha-runner/test/integration/SampleProject.it.spec.ts @@ -3,7 +3,6 @@ import MochaTestRunner from '../../src/MochaTestRunner'; import { TestResult, RunResult, TestStatus, RunStatus } from '@stryker-mutator/api/test_runner'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; -import MochaRunnerOptions from '../../src/MochaRunnerOptions'; import { testInjector } from '@stryker-mutator/test-helpers'; import { commonTokens } from '@stryker-mutator/api/plugin'; chai.use(chaiAsPromised); @@ -66,7 +65,7 @@ describe('Running a sample project', () => { resolve('testResources/sampleProject/MyMath.js'), resolve('testResources/sampleProject/MyMathSpec.js'), ]; - const mochaOptions: MochaRunnerOptions = { + const mochaOptions: MochaOptions = { files }; testInjector.options.mochaOptions = mochaOptions; diff --git a/packages/mocha-runner/test/unit/MochaOptionsLoader.spec.ts b/packages/mocha-runner/test/unit/MochaOptionsLoader.spec.ts index ade8af3db9..4cf22e935d 100644 --- a/packages/mocha-runner/test/unit/MochaOptionsLoader.spec.ts +++ b/packages/mocha-runner/test/unit/MochaOptionsLoader.spec.ts @@ -3,11 +3,12 @@ import * as fs from 'fs'; import { Config } from '@stryker-mutator/api/config'; import MochaOptionsLoader from '../../src/MochaOptionsLoader'; import { expect } from 'chai'; -import MochaRunnerOptions from '../../src/MochaRunnerOptions'; import sinon = require('sinon'); import { testInjector } from '@stryker-mutator/test-helpers'; +import LibWrapper from '../../src/LibWrapper'; +import { mochaOptionsKey } from '../../src/utils'; -describe('MochaOptionsLoader', () => { +describe(MochaOptionsLoader.name, () => { let readFileStub: sinon.SinonStub; let existsFileStub: sinon.SinonStub; @@ -15,140 +16,219 @@ describe('MochaOptionsLoader', () => { let sut: MochaOptionsLoader; beforeEach(() => { - readFileStub = sinon.stub(fs, 'readFileSync'); - existsFileStub = sinon.stub(fs, 'existsSync').returns(true); sut = testInjector.injector.injectClass(MochaOptionsLoader); - config = new Config(); }); - afterEach(() => { - sinon.restore(); - }); + describe('with mocha >= 6', () => { - it('should load a mocha.opts file if specified', () => { - readFileStub.returns(''); - config.mochaOptions = { - opts: 'some/mocha.opts/file' - }; - sut.load(config); - expect(testInjector.logger.info).calledWith(`Loading mochaOpts from "${path.resolve('some/mocha.opts/file')}"`); - expect(fs.readFileSync).calledWith(path.resolve('some/mocha.opts/file')); - }); + let rawOptions: { [option: string]: any }; - it('should log an error if specified mocha.opts file doesn\'t exist', () => { - readFileStub.returns(''); - existsFileStub.returns(false); - config.mochaOptions = { - opts: 'some/mocha.opts/file' - }; + beforeEach(() => { + rawOptions = Object.create(null); + sinon.stub(LibWrapper, 'loadOptions').returns(rawOptions); + }); - sut.load(config); - expect(testInjector.logger.error).calledWith(`Could not load opts from "${path.resolve('some/mocha.opts/file')}". Please make sure opts file exists.`); - }); + it('should log about mocha >= 6', () => { + sut.load(testInjector.options); + expect(testInjector.logger.debug).calledWith( + 'Mocha > 6 detected. Using mocha\'s `%s` to load mocha options', LibWrapper.loadOptions && LibWrapper.loadOptions.name + ); + }); - it('should load default mocha.opts file if not specified', () => { - readFileStub.returns(''); - sut.load(config); - expect(testInjector.logger.info).calledWith(`Loading mochaOpts from "${path.resolve('test/mocha.opts')}"`); - expect(fs.readFileSync).calledWith(path.resolve('test/mocha.opts')); - }); + it('should call `loadOptions` with serialized arguments', () => { + testInjector.options[mochaOptionsKey] = { + baz: true, + foo: 'bar' + }; + sut.load(testInjector.options); + expect(LibWrapper.loadOptions).calledWith(['--baz', 'true', '--foo', 'bar']); + }); - it('shouldn\'t load anything if mocha.opts = false', () => { - config.mochaOptions = { - opts: false - }; - sut.load(config); - expect(fs.readFileSync).not.called; - expect(testInjector.logger.debug).calledWith('Not reading additional mochaOpts from a file'); - }); + it('should filter out invalid options from the `loadOptions` result', () => { + testInjector.options[mochaOptionsKey] = { + override: true + }; + + // Following are valid options + rawOptions.extension = 'foo'; + rawOptions.require = 'bar'; + rawOptions.timeout = 'baz'; + rawOptions['async-only'] = 'qux'; + rawOptions.ui = 'quux'; + rawOptions.grep = 'quuz'; + rawOptions.exclude = 'corge'; + rawOptions.file = 'grault'; + + rawOptions.garply = 'waldo'; // this should be filtered out + const result = sut.load(testInjector.options); + expect(result).deep.eq({ + exclude: 'corge', + extension: 'foo', + file: 'grault', + grep: 'quuz', + override: true, + require: 'bar', + timeout: 'baz', + ['async-only']: 'qux', + ui: 'quux', + }); + }); - it('should not load default mocha.opts file if not found', () => { - existsFileStub.returns(false); - const options = sut.load(config); - expect(options).deep.eq({}); - expect(testInjector.logger.debug).calledWith('No mocha opts file found, not loading additional mocha options (%s.opts was not defined).', 'mochaOptions'); + it('should trace log the mocha call', () => { + testInjector.logger.isTraceEnabled.returns(true); + testInjector.options[mochaOptionsKey] = { + foo: 'bar' + }; + rawOptions.baz = 'qux'; + sut.load(testInjector.options); + const fnName = LibWrapper.loadOptions && LibWrapper.loadOptions.name; + expect(testInjector.logger.trace).calledWith( + `Mocha: ${fnName}(['--foo','bar']) => {"baz":"qux"}` + ); + }); }); - it('should load `--require` and `-r` properties if specified in mocha.opts file', () => { - readFileStub.returns(` - --require src/test/support/setup - -r babel-require - `); - config.mochaOptions = { opts: '.' }; - const options = sut.load(config); - expect(options).deep.include({ - require: [ - 'src/test/support/setup', - 'babel-require' - ] + describe('with mocha < 6', () => { + + beforeEach(() => { + sinon.stub(LibWrapper, 'loadOptions').value(undefined); + readFileStub = sinon.stub(fs, 'readFileSync'); + existsFileStub = sinon.stub(fs, 'existsSync').returns(true); + config = new Config(); }); - }); - function itShouldLoadProperty(property: string, value: string, expectedConfig: Partial) { - it(`should load '${property} if specified`, () => { - readFileStub.returns(`${property} ${value}`); - config.mochaOptions = { opts: 'path/to/opts/file' }; - expect(sut.load(config)).deep.include(expectedConfig); - }); - } - - itShouldLoadProperty('--timeout', '2000', { timeout: 2000 }); - itShouldLoadProperty('-t', '2000', { timeout: 2000 }); - itShouldLoadProperty('-A', '', { asyncOnly: true }); - itShouldLoadProperty('--async-only', '', { asyncOnly: true }); - itShouldLoadProperty('--ui', 'qunit', { ui: 'qunit' }); - itShouldLoadProperty('-u', 'qunit', { ui: 'qunit' }); - itShouldLoadProperty('-g', 'grepthis', { grep: /grepthis/ }); - itShouldLoadProperty('--grep', '/grep(this|that)/', { grep: /grep(this|that)/ }); - itShouldLoadProperty('--grep', 'grep(this|that)?', { grep: /grep(this|that)?/ }); - - it('should not override additional properties', () => { - readFileStub.returns(` - -u qunit - -t 2000 - -A - -r babel-register - `); - config.mochaOptions = { - asyncOnly: false, - opts: 'path/to/opts/file', - require: ['ts-node/register'], - timeout: 4000, - ui: 'exports' - }; - const options = sut.load(config); - expect(options).deep.equal({ - asyncOnly: false, - opts: 'path/to/opts/file', - require: ['ts-node/register'], - timeout: 4000, - ui: 'exports' + it('should log about mocha < 6', () => { + existsFileStub.returns(false); + sut.load(config); + expect(testInjector.logger.debug).calledWith('Mocha < 6 detected. Using custom logic to parse mocha options'); }); - }); - it('should ignore additional properties', () => { - readFileStub.returns(` - --reporter dot - --ignore-leaks - `); - config.mochaOptions = { - opts: 'some/mocha.opts/file', - }; - const options = sut.load(config); - expect(options).deep.eq({ opts: 'some/mocha.opts/file' }); - expect(testInjector.logger.debug).calledWith('Ignoring option "--reporter" as it is not supported.'); - expect(testInjector.logger.debug).calledWith('Ignoring option "--ignore-leaks" as it is not supported.'); - }); + it('should load a mocha.opts file if specified', () => { + readFileStub.returns(''); + config.mochaOptions = { + opts: 'some/mocha.opts/file' + }; + sut.load(config); + expect(testInjector.logger.info).calledWith(`Loading mochaOpts from "${path.resolve('some/mocha.opts/file')}"`); + expect(fs.readFileSync).calledWith(path.resolve('some/mocha.opts/file')); + }); + + it('should log an error if specified mocha.opts file doesn\'t exist', () => { + readFileStub.returns(''); + existsFileStub.returns(false); + config.mochaOptions = { + opts: 'some/mocha.opts/file' + }; - it('should ignore invalid --ui and --timeout options', () => { - readFileStub.returns(` - --timeout - --ui - `); - config.mochaOptions = { - opts: 'some/mocha.opts/file', - }; - const options = sut.load(config); - expect(options).deep.eq({ opts: 'some/mocha.opts/file', timeout: undefined, ui: undefined }); + sut.load(config); + expect(testInjector.logger.error).calledWith(`Could not load opts from "${path.resolve('some/mocha.opts/file')}". Please make sure opts file exists.`); + }); + + it('should load default mocha.opts file if not specified', () => { + readFileStub.returns(''); + sut.load(config); + expect(testInjector.logger.info).calledWith(`Loading mochaOpts from "${path.resolve('test/mocha.opts')}"`); + expect(fs.readFileSync).calledWith(path.resolve('test/mocha.opts')); + }); + + it('shouldn\'t load anything if mocha.opts = false', () => { + config.mochaOptions = { + opts: false + }; + sut.load(config); + expect(fs.readFileSync).not.called; + expect(testInjector.logger.debug).calledWith('Not reading additional mochaOpts from a file'); + }); + + it('should not load default mocha.opts file if not found', () => { + existsFileStub.returns(false); + const options = sut.load(config); + expect(options).deep.eq({}); + expect(testInjector.logger.debug).calledWith('No mocha opts file found, not loading additional mocha options (%s.opts was not defined).', 'mochaOptions'); + }); + + it('should load `--require` and `-r` properties if specified in mocha.opts file', () => { + readFileStub.returns(` + --require src/test/support/setup + -r babel-require + `); + config.mochaOptions = { opts: '.' }; + const options = sut.load(config); + expect(options).deep.include({ + require: [ + 'src/test/support/setup', + 'babel-require' + ] + }); + }); + + function itShouldLoadProperty(property: string, value: string, expectedConfig: Partial) { + it(`should load '${property} if specified`, () => { + readFileStub.returns(`${property} ${value}`); + config.mochaOptions = { opts: 'path/to/opts/file' }; + expect(sut.load(config)).deep.include(expectedConfig); + }); + } + + itShouldLoadProperty('--timeout', '2000', { timeout: 2000 }); + itShouldLoadProperty('-t', '2000', { timeout: 2000 }); + itShouldLoadProperty('-A', '', { asyncOnly: true }); + itShouldLoadProperty('--async-only', '', { asyncOnly: true }); + itShouldLoadProperty('--ui', 'qunit', { ui: 'qunit' }); + itShouldLoadProperty('-u', 'qunit', { ui: 'qunit' }); + itShouldLoadProperty('-g', 'grepthis', { grep: /grepthis/ }); + itShouldLoadProperty('--grep', '/grep(this|that)/', { grep: /grep(this|that)/ }); + itShouldLoadProperty('--grep', 'grep(this|that)?', { grep: /grep(this|that)?/ }); + + it('should not override additional properties', () => { + readFileStub.returns(` + -u qunit + -t 2000 + -A + -r babel-register + `); + config.mochaOptions = { + asyncOnly: false, + opts: 'path/to/opts/file', + require: ['ts-node/register'], + timeout: 4000, + ui: 'exports' + }; + const options = sut.load(config); + expect(options).deep.equal({ + asyncOnly: false, + opts: 'path/to/opts/file', + require: ['ts-node/register'], + timeout: 4000, + ui: 'exports' + }); + }); + + it('should ignore additional properties', () => { + readFileStub.returns(` + --reporter dot + --ignore-leaks + `); + config.mochaOptions = { + opts: 'some/mocha.opts/file', + }; + const options = sut.load(config); + expect(options).deep.eq({ opts: 'some/mocha.opts/file' }); + expect(testInjector.logger.debug).calledWith('Ignoring option "--reporter" as it is not supported.'); + expect(testInjector.logger.debug).calledWith('Ignoring option "--ignore-leaks" as it is not supported.'); + }); + + it('should ignore invalid --ui and --timeout options', () => { + readFileStub.returns(` + --timeout + --ui + `); + config.mochaOptions = { + opts: 'some/mocha.opts/file', + }; + const options = sut.load(config); + expect(options).deep.eq({ opts: 'some/mocha.opts/file', timeout: undefined, ui: undefined }); + }); }); + }); diff --git a/packages/mocha-runner/test/unit/MochaTestRunner.spec.ts b/packages/mocha-runner/test/unit/MochaTestRunner.spec.ts index 698af3a56c..a74938e132 100644 --- a/packages/mocha-runner/test/unit/MochaTestRunner.spec.ts +++ b/packages/mocha-runner/test/unit/MochaTestRunner.spec.ts @@ -6,7 +6,6 @@ import { RunOptions } from '@stryker-mutator/api/test_runner'; import MochaTestRunner from '../../src/MochaTestRunner'; import LibWrapper from '../../src/LibWrapper'; import * as utils from '../../src/utils'; -import MochaRunnerOptions from '../../src/MochaRunnerOptions'; import { testInjector } from '@stryker-mutator/test-helpers'; import sinon = require('sinon'); import { commonTokens } from '@stryker-mutator/api/plugin'; @@ -38,7 +37,7 @@ describe(MochaTestRunner.name, () => { delete StrykerMochaReporter.log; }); - function createSut(mochaSettings: Partial<{ fileNames: ReadonlyArray, mochaOptions: MochaRunnerOptions }>) { + function createSut(mochaSettings: Partial<{ fileNames: ReadonlyArray, mochaOptions: MochaOptions }>) { testInjector.options.mochaOptions = mochaSettings.mochaOptions || {}; return testInjector.injector .provideValue(commonTokens.sandboxFileNames, mochaSettings.fileNames || ['src/math.js', 'test/mathSpec.js']) @@ -81,7 +80,7 @@ describe(MochaTestRunner.name, () => { it('should pass along supported options to mocha', async () => { // Arrange multimatchStub.returns(['foo.js', 'bar.js', 'foo2.js']); - const mochaOptions: MochaRunnerOptions = { + const mochaOptions: MochaOptions = { asyncOnly: true, grep: /grepme/, opts: 'opts', @@ -103,7 +102,7 @@ describe(MochaTestRunner.name, () => { }); it('should pass require additional require options when constructed', () => { - const mochaOptions: MochaRunnerOptions = { require: ['ts-node', 'babel-register'] }; + const mochaOptions: MochaOptions = { require: ['ts-node', 'babel-register'] }; createSut({ mochaOptions }); expect(requireStub).calledTwice; expect(requireStub).calledWith('ts-node'); @@ -111,7 +110,7 @@ describe(MochaTestRunner.name, () => { }); it('should pass and resolve relative require options when constructed', () => { - const mochaOptions: MochaRunnerOptions = { require: ['./setup.js', 'babel-register'] }; + const mochaOptions: MochaOptions = { require: ['./setup.js', 'babel-register'] }; createSut({ mochaOptions }); const resolvedRequire = path.resolve('./setup.js'); expect(requireStub).calledTwice; diff --git a/packages/mocha-runner/testResources/mocha-config/.mocharc.js b/packages/mocha-runner/testResources/mocha-config/.mocharc.js new file mode 100644 index 0000000000..5d9c7fa770 --- /dev/null +++ b/packages/mocha-runner/testResources/mocha-config/.mocharc.js @@ -0,0 +1,16 @@ +'use strict'; + +// Here's a JavaScript-based config file. +// If you need conditional logic, you might want to use this type of config. +// Otherwise, JSON or YAML is recommended. + +module.exports = { + diff: true, + extension: ['js'], + opts: './test/mocha.opts', + package: './package.json', + reporter: 'spec', + slow: 75, + timeout: 2000, + ui: 'bdd' +}; diff --git a/packages/mocha-runner/testResources/mocha-config/.mocharc.json b/packages/mocha-runner/testResources/mocha-config/.mocharc.json new file mode 100644 index 0000000000..5ee7ea20c9 --- /dev/null +++ b/packages/mocha-runner/testResources/mocha-config/.mocharc.json @@ -0,0 +1,14 @@ +// This config file contains Mocha's defaults. +// As you can see, comments are allowed. +// This same configuration could be provided in the `mocha` property of your +// project's `package.json`. +{ + "diff": true, + "extension": ["json"], + "opts": "./test/mocha.opts", + "package": "./package.json", + "reporter": "spec", + "slow": 75, + "timeout": 2000, + "ui": "bdd" +} diff --git a/packages/mocha-runner/testResources/mocha-config/.mocharc.jsonc b/packages/mocha-runner/testResources/mocha-config/.mocharc.jsonc new file mode 100644 index 0000000000..4a6992099c --- /dev/null +++ b/packages/mocha-runner/testResources/mocha-config/.mocharc.jsonc @@ -0,0 +1,14 @@ +// This config file contains Mocha's defaults. +// As you can see, comments are allowed. +// This same configuration could be provided in the `mocha` property of your +// project's `package.json`. +{ + "diff": true, + "extension": ["jsonc"], + "opts": "./test/mocha.opts", + "package": /* 📦 */ "./package.json", + "reporter": /* 📋 */ "spec", + "slow": 75, + "timeout": 2000, + "ui": "bdd" +} diff --git a/packages/mocha-runner/testResources/mocha-config/.mocharc.yml b/packages/mocha-runner/testResources/mocha-config/.mocharc.yml new file mode 100644 index 0000000000..a97f1a9605 --- /dev/null +++ b/packages/mocha-runner/testResources/mocha-config/.mocharc.yml @@ -0,0 +1,47 @@ +# This is an example Mocha config containing every Mocha option plus others +allow-uncaught: false +async-only: false +bail: false +check-leaks: false +color: true +delay: false +diff: true +exclude: + - /path/to/some/excluded/file +exit: false # could be expressed as "no-exit: true" +extension: + - yml + - js +# fgrep and grep are mutually exclusive +# fgrep: something +file: + - /path/to/some/file + - /path/to/some/other/file +forbid-only: false +forbid-pending: false +full-trace: false +global: + - jQuery + - $ +# fgrep and grep are mutually exclusive +# grep: something +growl: false +inline-diffs: false +# needs to be used with grep or fgrep +# invert: false +opts: './test/mocha.opts' +recursive: false +reporter: spec +reporter-option: + - foo=bar + - baz=quux +require: '@babel/register' +retries: 1 +slow: 75 +sort: false +spec: test/**/*.spec.js # the positional arguments! +v8-stack-trace-limit: 100 # V8 flags are prepended with "v8-" +timeout: false # same as "no-timeout: true" or "timeout: 0" +trace-warnings: true # node flags ok +ui: bdd +watch: false diff --git a/packages/mocha-runner/testResources/mocha-config/package.json b/packages/mocha-runner/testResources/mocha-config/package.json new file mode 100644 index 0000000000..eaeaecb54c --- /dev/null +++ b/packages/mocha-runner/testResources/mocha-config/package.json @@ -0,0 +1,10 @@ +{ + "mocha": { + "async-only": true, + "extension": ["json"], + "slow": 75, + "timeout": 20, + "foo": "bar", + "ui": "tdd" + } +} \ No newline at end of file diff --git a/packages/mocha-runner/tsconfig.src.json b/packages/mocha-runner/tsconfig.src.json index 1833a1b064..c6082eb424 100644 --- a/packages/mocha-runner/tsconfig.src.json +++ b/packages/mocha-runner/tsconfig.src.json @@ -4,7 +4,8 @@ "rootDir": "." }, "include": [ - "src" + "src", + "typings" ], "references": [ { diff --git a/packages/mocha-runner/tsconfig.test.json b/packages/mocha-runner/tsconfig.test.json index 222941072d..099a53f9b5 100644 --- a/packages/mocha-runner/tsconfig.test.json +++ b/packages/mocha-runner/tsconfig.test.json @@ -8,7 +8,8 @@ ] }, "include": [ - "test" + "test", + "typings" ], "references": [ { diff --git a/packages/mocha-runner/src/MochaRunnerOptions.ts b/packages/mocha-runner/typings/mocha.d.ts similarity index 60% rename from packages/mocha-runner/src/MochaRunnerOptions.ts rename to packages/mocha-runner/typings/mocha.d.ts index 74a0a8c197..3bbda8d6cc 100644 --- a/packages/mocha-runner/src/MochaRunnerOptions.ts +++ b/packages/mocha-runner/typings/mocha.d.ts @@ -1,11 +1,13 @@ -export const mochaOptionsKey = 'mochaOptions'; -export default interface MochaRunnerOptions { +declare interface MochaOptions { require?: string[]; opts?: string; + config?: string; + package?: string; timeout?: number; asyncOnly?: boolean; ui?: string; files?: string[] | string; grep?: RegExp; + extension?: string[]; } From e17493d31f1314e3faf8de26bf40e4def5a02f6c Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 18 Apr 2019 16:28:06 +0200 Subject: [PATCH 2/3] docs(mocha 6): document new way of config loading --- packages/mocha-runner/README.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/mocha-runner/README.md b/packages/mocha-runner/README.md index 4af30224af..4fb9a8299b 100644 --- a/packages/mocha-runner/README.md +++ b/packages/mocha-runner/README.md @@ -36,7 +36,9 @@ module.exports = function (config) { mochaOptions: { // Optional mocha options files: [ 'test/**/*.js' ] - opts: 'path/to/mocha.opts', + config: 'path/to/mocha/config/.mocharc.json', + package: 'path/to/custom/package/package.json', + opts: 'path/to/custom/mocha.opts', ui: 'bdd', timeout: 3000, require: [ /*'babel-register' */], @@ -47,6 +49,11 @@ module.exports = function (config) { } ``` +When using Mocha version 6, @stryker-mutator/mocha-runner will use [mocha's internal file loading mechanism](https://mochajs.org/api/module-lib_cli_options.html#.loadOptions) to load your mocha configuration. +So feel free to _leave out the mochaOptions entirely_ if you're using one of the [default file locations](https://mochajs.org/#configuring-mocha-nodejs). + +Alternatively, use `['no-config']: true`, `['no-package']: true` or `['no-opts']: true` to ignore the default mocha config, default mocha package.json and default mocha opts locations respectively. + ### `mochaOptions.files` [`string` or `string[]`] Default: `'test/**/*.js'` @@ -55,6 +62,23 @@ Choose which files to include. This is comparable to [mocha's test directory](ht If you want to load all files recursively: use a globbing expression (`'test/**/*.js'`). If you want to decide on the order of files, use multiple globbing expressions. For example: use `['test/helpers/**/*.js', 'test/unit/**/*.js']` if you want to make sure your helpers are loaded before your unit tests. +### `mochaOptions.config` [`string` | `undefined`] + +Default: `undefined` + +Explicit path to the [mocha config file](https://mochajs.org/#-config-path) + +*New since Mocha 6* + +### `mochaOptions.package` [`string` | `undefined`] + +Default: `undefined` + +Specify an explicit path to a package.json file (ostensibly containing configuration in a mocha property). +See https://mochajs.org/#-package-path. + +*New since Mocha 6* + ### `mochaOptions.opts` [`string` | false] Default: `'test/mocha.opts'` From 26943e753d5053a31dc91f7e0b4d0818d8719a1b Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 19 Apr 2019 11:13:03 +0200 Subject: [PATCH 3/3] refactor: use spread operator instead of Object.assign --- packages/mocha-runner/src/MochaOptionsLoader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mocha-runner/src/MochaOptionsLoader.ts b/packages/mocha-runner/src/MochaOptionsLoader.ts index d2199816ed..602135afec 100644 --- a/packages/mocha-runner/src/MochaOptionsLoader.ts +++ b/packages/mocha-runner/src/MochaOptionsLoader.ts @@ -14,8 +14,8 @@ export default class MochaOptionsLoader { constructor(private readonly log: Logger) { } public load(strykerOptions: StrykerOptions): MochaOptions { - const mochaOptions = Object.assign({}, strykerOptions[mochaOptionsKey]) as MochaOptions; - return Object.assign(this.loadMochaOptions(mochaOptions), mochaOptions); + const mochaOptions = { ...strykerOptions[mochaOptionsKey] } as MochaOptions; + return { ... this.loadMochaOptions(mochaOptions), ...mochaOptions }; } private loadMochaOptions(overrides: MochaOptions) {