Skip to content

Commit

Permalink
feat(mocha 6): support all config formats (#1511)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nicojs committed Apr 19, 2019
1 parent fd399a8 commit baa374d
Show file tree
Hide file tree
Showing 20 changed files with 550 additions and 146 deletions.
2 changes: 1 addition & 1 deletion packages/mocha-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
26 changes: 25 additions & 1 deletion packages/mocha-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' */],
Expand All @@ -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'`
Expand All @@ -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'`
Expand Down
2 changes: 1 addition & 1 deletion packages/mocha-runner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@
},
"peerDependencies": {
"@stryker-mutator/core": "^1.0.0",
"mocha": ">= 2.3.3 < 6"
"mocha": ">= 2.3.3 < 7"
}
}
14 changes: 14 additions & 0 deletions packages/mocha-runner/src/LibWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
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
*/
export default class LibWrapper {
public static Mocha = Mocha;
public static require = require;
public static multimatch = multimatch;
public static loadOptions = loadOptions;
}
2 changes: 1 addition & 1 deletion packages/mocha-runner/src/MochaConfigEditor.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
30 changes: 23 additions & 7 deletions packages/mocha-runner/src/MochaOptionsLoader.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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 = { ...strykerOptions[mochaOptionsKey] } as MochaOptions;
return { ... 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');
Expand Down Expand Up @@ -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]) {
Expand Down
5 changes: 2 additions & 3 deletions packages/mocha-runner/src/MochaTestRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<string>, options: StrykerOptions) {
Expand Down
35 changes: 35 additions & 0 deletions packages/mocha-runner/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
133 changes: 133 additions & 0 deletions packages/mocha-runner/test/integration/MochaOptionsLoader.it.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit baa374d

Please sign in to comment.