Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mocha 6): support all config formats #1511

Merged
merged 3 commits into from
Apr 19, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = Object.assign({}, strykerOptions[mochaOptionsKey]) as MochaOptions;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use a spread operator for this { ...strykerOptions[mochaOptionsKey] }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return Object.assign(this.loadMochaOptions(mochaOptions), mochaOptions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use a spread operator for this { ...this.loadMochaOptions(mochaOptions), ...mochaOptions }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

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
Loading