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(Initializer): Initialize config file as JSON by default #2093

Merged
merged 10 commits into from
Mar 11, 2020
50 changes: 32 additions & 18 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,40 @@ The main `command` for Stryker is `run`, which kicks off mutation testing.

Although Stryker can run without any configuration, it is recommended to configure it when you can, as it can greatly improve performance of the mutation testing process. By default, Stryker will look for a `stryker.conf.js` or `stryker.conf.json` file in the current working directory (if it exists). This can be overridden by specifying a different file as the last parameter.

Before your first run, we recommend you try the `init` command, which helps you to set up this `stryker.conf.js` file and install any missing packages needed for your specific configuration. We recommend you verify the contents of the configuration file after this initialization, to make sure everything is setup correctly. Of course, you can still make changes to it, before you run Stryker for the first time.

The following is an example `stryker.conf.js` file. It specifies running mocha tests with the mocha test runner.

```javascript
module.exports = function(config){
config.set({
mutate: [
'src/**/*.js',
'!src/index.js'
],
testFramework: 'mocha',
testRunner: 'mocha',
reporters: ['progress', 'clear-text', 'html'],
coverageAnalysis: 'perTest'
});
Before your first run, we recommend you try the `init` command, which helps you to set up this config file and install any missing packages needed for your specific configuration. We recommend you verify the contents of the configuration file after this initialization, to make sure everything is setup correctly. Of course, you can still make changes to it, before you run Stryker for the first time.

The following is an example `stryker.conf.json` file. It specifies running mocha tests with the mocha test runner.

```json
{
"$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker/master/packages/api/schema/stryker-core.json",
"mutate": [
"src/**/*.js",
"!src/index.js"
],
"testFramework": "mocha",
"testRunner": "mocha",
"reporters": ["progress", "clear-text", "html"],
"coverageAnalysis": "perTest"
}
```

As you can see, the config file is *not* a simple JSON file. It should be a node module. You might recognize this way of working from the karma test runner.
As a `stryker.conf.js` file this looks like this:
```javascript
/**
* @type {import('@stryker-mutator/api/core').StrykerOptions}
*/
module.exports = {
mutate: [
'src/**/*.js',
'!src/index.js'
],
testFramework: 'mocha',
testRunner: 'mocha',
reporters: ['progress', 'clear-text', 'html'],
coverageAnalysis: 'perTest'
};
```

Make sure you *at least* specify the `testRunner` options when mixing the config file and/or command line options.

Expand All @@ -80,7 +94,7 @@ See our website for the [list of currently supported mutators](https://stryker-m

## Configuration

All configuration options can either be set via the command line or via the `stryker.conf.js` config file.
All configuration options can either be set via the command line or via a config file.

`files` and `mutate` both support globbing expressions using [node glob](https://github.com/isaacs/node-glob).
This is the same globbing format you might know from [Grunt](https://github.com/gruntjs/grunt) or [Karma](https://github.com/karma-runner/karma).
Expand Down
81 changes: 53 additions & 28 deletions packages/core/src/initializer/StrykerConfigWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,28 @@ import PromptOption from './PromptOption';

import { initializerTokens } from '.';

const STRYKER_CONFIG_FILE = 'stryker.conf.js';
const STRYKER_JS_CONFIG_FILE = 'stryker.conf.js';
const STRYKER_JSON_CONFIG_FILE = 'stryker.conf.json';

export default class StrykerConfigWriter {
public static inject = tokens(commonTokens.logger, initializerTokens.out);
constructor(private readonly log: Logger, private readonly out: typeof console.log) {}

public guardForExistingConfig() {
if (existsSync(STRYKER_CONFIG_FILE)) {
const msg = 'Stryker config file "stryker.conf.js" already exists in the current directory. Please remove it and try again.';
this.checkIfConfigFileExists(STRYKER_JS_CONFIG_FILE);
this.checkIfConfigFileExists(STRYKER_JSON_CONFIG_FILE);
}

private checkIfConfigFileExists(file: string) {
if (existsSync(file)) {
const msg = `Stryker config file "${file}" already exists in the current directory. Please remove it and try again.`;
this.log.error(msg);
throw new Error(msg);
}
}

/**
* Create stryker.conf.js based on the chosen framework and test runner
* Create config based on the chosen framework and test runner
* @function
*/
public write(
Expand All @@ -35,8 +41,9 @@ export default class StrykerConfigWriter {
selectedTranspilers: null | PromptOption[],
selectedReporters: PromptOption[],
selectedPackageManager: PromptOption,
additionalPiecesOfConfig: Array<Partial<StrykerOptions>>
): Promise<void> {
additionalPiecesOfConfig: Array<Partial<StrykerOptions>>,
exportAsJson: boolean
): Promise<string> {
const configObject: Partial<StrykerOptions> = {
mutator: selectedMutator ? selectedMutator.name : '',
packageManager: selectedPackageManager.name,
Expand All @@ -47,19 +54,20 @@ export default class StrykerConfigWriter {

this.configureTestFramework(configObject, selectedTestFramework);
Object.assign(configObject, ...additionalPiecesOfConfig);
return this.writeStrykerConfig(configObject);
return this.writeStrykerConfig(configObject, exportAsJson);
}

/**
* Create stryker.conf.js based on the chosen preset
* Create config based on the chosen preset
* @function
*/
public async writePreset(presetConfig: PresetConfiguration) {
return this.writeStrykerConfigRaw(
presetConfig.config,
`// This config was generated using a preset.
// Please see the handbook for more information: ${presetConfig.handbookUrl}`
);
public async writePreset(presetConfig: PresetConfiguration, exportAsJson: boolean) {
const config = {
comment: `This config was generated using a preset. Please see the handbook for more information: ${presetConfig.handbookUrl}`,
...presetConfig.config
};

return this.writeStrykerConfig(config, exportAsJson);
}

private configureTestFramework(configObject: Partial<StrykerOptions>, selectedTestFramework: null | PromptOption) {
Expand All @@ -71,28 +79,45 @@ export default class StrykerConfigWriter {
}
}

private async writeStrykerConfigRaw(rawConfig: string, rawHeader = '') {
this.out('Writing & formatting stryker.conf.js...');
const formattedConf = `${rawHeader}
module.exports = function(config){
config.set(
${rawConfig}
);
}`;
await fs.writeFile(STRYKER_CONFIG_FILE, formattedConf);
private writeStrykerConfig(config: Partial<StrykerOptions>, exportAsJson: boolean) {
if (exportAsJson) {
return this.writeJsonConfig(config);
} else {
return this.writeJsConfig(config);
}
}

private async writeJsConfig(commentedConfig: Partial<StrykerOptions>) {
this.out(`Writing & formatting ${STRYKER_JS_CONFIG_FILE}...`);
const rawConfig = this.stringify(commentedConfig);
const formattedConfig = `/**
* @type {import('@stryker-mutator/api/core').StrykerOptions}
*/
module.exports = ${rawConfig};`;
await fs.writeFile(STRYKER_JS_CONFIG_FILE, formattedConfig);
try {
await childProcessAsPromised.exec(`npx prettier --write ${STRYKER_CONFIG_FILE}`);
await childProcessAsPromised.exec(`npx prettier --write ${STRYKER_JS_CONFIG_FILE}`);
} catch (error) {
this.log.debug('Prettier exited with error', error);
this.out('Unable to format stryker.conf.js file for you. This is not a big problem, but it might look a bit messy 🙈.');
}

return STRYKER_JS_CONFIG_FILE;
}

private writeStrykerConfig(configObject: Partial<StrykerOptions>) {
return this.writeStrykerConfigRaw(this.wrapInModule(configObject));
private async writeJsonConfig(commentedConfig: Partial<StrykerOptions>) {
this.out(`Writing & formatting ${STRYKER_JSON_CONFIG_FILE}...`);
const typedConfig = {
$schema: 'https://raw.githubusercontent.com/stryker-mutator/stryker/master/packages/api/schema/stryker-core.json',
...commentedConfig
};
const formattedConfig = this.stringify(typedConfig);
await fs.writeFile(STRYKER_JSON_CONFIG_FILE, formattedConfig);

return STRYKER_JSON_CONFIG_FILE;
}

private wrapInModule(configObject: Partial<StrykerOptions>): string {
return JSON.stringify(configObject, null, 2);
private stringify(input: object): string {
return JSON.stringify(input, undefined, 2);
}
}
22 changes: 16 additions & 6 deletions packages/core/src/initializer/StrykerInitializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ export default class StrykerInitializer {
this.configWriter.guardForExistingConfig();
this.patchProxies();
const selectedPreset = await this.selectPreset();
let configFileName: string;
if (selectedPreset) {
await this.initiatePreset(this.configWriter, selectedPreset);
configFileName = await this.initiatePreset(this.configWriter, selectedPreset);
} else {
await this.initiateCustom(this.configWriter);
configFileName = await this.initiateCustom(this.configWriter);
}
await this.gitignoreWriter.addStrykerTempFolder();
this.out('Done configuring stryker. Please review `stryker.conf.js`, you might need to configure transpilers or your test runner correctly.');
this.out(`Done configuring stryker. Please review "${configFileName}", you might need to configure transpilers or your test runner correctly.`);
this.out("Let's kill some mutants with this command: `stryker run`");
}

Expand Down Expand Up @@ -86,9 +87,11 @@ export default class StrykerInitializer {

private async initiatePreset(configWriter: StrykerConfigWriter, selectedPreset: Preset) {
const presetConfig = await selectedPreset.createConfig();
await configWriter.writePreset(presetConfig);
const isJsonSelected = await this.selectJsonConfigType();
const configFileName = await configWriter.writePreset(presetConfig, isJsonSelected);
const selectedPackageManager = await this.selectPackageManager();
this.installNpmDependencies(presetConfig.dependencies, selectedPackageManager);
return configFileName;
}

private async initiateCustom(configWriter: StrykerConfigWriter) {
Expand All @@ -99,22 +102,25 @@ export default class StrykerInitializer {
const selectedTranspilers = await this.selectTranspilers();
const selectedReporters = await this.selectReporters();
const selectedPackageManager = await this.selectPackageManager();
const isJsonSelected = await this.selectJsonConfigType();
const npmDependencies = this.getSelectedNpmDependencies(
[selectedTestRunner, selectedTestFramework, selectedMutator].concat(selectedTranspilers).concat(selectedReporters)
);
await configWriter.write(
const configFileName = await configWriter.write(
selectedTestRunner,
selectedTestFramework,
selectedMutator,
selectedTranspilers,
selectedReporters,
selectedPackageManager,
await this.fetchAdditionalConfig(npmDependencies)
await this.fetchAdditionalConfig(npmDependencies),
isJsonSelected
);
this.installNpmDependencies(
npmDependencies.map(pkg => pkg.name),
selectedPackageManager
);
return configFileName;
}

private async selectTestRunner(): Promise<PromptOption | null> {
Expand Down Expand Up @@ -208,6 +214,10 @@ export default class StrykerInitializer {
]);
}

private async selectJsonConfigType(): Promise<boolean> {
return this.inquirer.promptJsonConfigType();
}

private getSelectedNpmDependencies(selectedOptions: Array<PromptOption | null>): PackageInfo[] {
return filterEmpty(filterEmpty(selectedOptions).map(option => option.pkg));
}
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/initializer/StrykerInquirer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,17 @@ export class StrykerInquirer {
});
return options.filter(_ => _.name === answers.packageManager)[0];
}

public async promptJsonConfigType(): Promise<boolean> {
const json = 'JSON';

const answers = await inquirer.prompt<{ configType: string }>({
choices: [json, 'JavaScript'],
default: json,
message: 'What kind of config do you want?',
nicojs marked this conversation as resolved.
Show resolved Hide resolved
name: 'configType',
type: 'list'
});
return answers.configType === json;
}
}
42 changes: 20 additions & 22 deletions packages/core/src/initializer/presets/AngularPreset.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as os from 'os';

import { StrykerOptions } from '@stryker-mutator/api/core';

import Preset from './Preset';
import PresetConfiguration from './PresetConfiguration';

Expand All @@ -9,28 +11,24 @@ export class AngularPreset implements Preset {
public readonly name = 'angular-cli';
// Please keep config in sync with handbook
private readonly dependencies = ['@stryker-mutator/core', '@stryker-mutator/karma-runner', '@stryker-mutator/typescript'];
private readonly config = `{
mutate: [
'src/**/*.ts',
'!src/**/*.spec.ts',
'!src/test.ts',
'!src/environments/*.ts'
],
mutator: 'typescript',
testRunner: 'karma',
karma: {
configFile: 'src/karma.conf.js',
projectType: 'angular-cli',
config: {
browsers: ['ChromeHeadless']
}
},
reporters: ['progress', 'clear-text', 'html'],
maxConcurrentTestRunners: ${Math.floor(
os.cpus().length / 2
)}, // Recommended to use about half of your available cores when running stryker with angular.
coverageAnalysis: 'off'
}`;
private readonly config: Partial<StrykerOptions> = {
mutate: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/test.ts', '!src/environments/*.ts'],
mutator: 'typescript',
testRunner: 'karma',
karma: {
configFile: 'src/karma.conf.js',
projectType: 'angular-cli',
config: {
browsers: ['ChromeHeadless']
}
},
reporters: ['progress', 'clear-text', 'html'],
maxConcurrentTestRunners: Math.floor(os.cpus().length / 2),
// eslint-disable-next-line @typescript-eslint/camelcase
maxConcurrentTestRunners_comment: 'Recommended to use about half of your available cores when running stryker with angular',
coverageAnalysis: 'off'
};

public async createConfig(): Promise<PresetConfiguration> {
return { config: this.config, handbookUrl, dependencies: this.dependencies };
}
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/initializer/presets/PresetConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { StrykerOptions } from '@stryker-mutator/api/core';

export default interface PresetConfiguration {
config: string;
config: Partial<StrykerOptions>;
handbookUrl: string;
dependencies: string[];
}
Loading