Skip to content

Commit

Permalink
feat(Stryker CLI 'init'): Support for preset configuration during 'st…
Browse files Browse the repository at this point in the history
…ryker init' (#1248)

Add a question to the `init` command:

```
Are you using one of these frameworks? Then select a preset configuration. (Use arrow keys)
> angular-cli
  react
  vueJs
  ──────────────
  None/other
```

If you choose a preset, preset specific questions may be asked. If all is well, Stryker will work out-of-the box.
  • Loading branch information
Wmaarts authored and nicojs committed Nov 27, 2018
1 parent 50e9398 commit 5673e6b
Show file tree
Hide file tree
Showing 12 changed files with 605 additions and 95 deletions.
29 changes: 22 additions & 7 deletions packages/stryker/src/initializer/StrykerConfigWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getLogger } from 'stryker-api/logging';
import { StrykerOptions } from 'stryker-api/core';
import PromptOption from './PromptOption';
import { format } from 'prettier';
import PresetConfiguration from './presets/PresetConfiguration';

const STRYKER_CONFIG_FILE = 'stryker.conf.js';

Expand Down Expand Up @@ -47,6 +48,15 @@ export default class StrykerConfigWriter {
return this.writeStrykerConfig(configObject);
}

/**
* Create stryker.conf.js 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}`);
}

private configureTestFramework(configObject: Partial<StrykerOptions>, selectedTestFramework: null | PromptOption) {
if (selectedTestFramework) {
configObject.testFramework = selectedTestFramework.name;
Expand All @@ -56,17 +66,22 @@ export default class StrykerConfigWriter {
}
}

private writeStrykerConfig(configObject: Partial<StrykerOptions>) {
private writeStrykerConfigRaw(rawConfig: string, rawHeader = '') {
this.out('Writing stryker.conf.js...');
return fsAsPromised.writeFile(STRYKER_CONFIG_FILE, this.wrapInModule(configObject));
}

private wrapInModule(configObject: Partial<StrykerOptions>) {
return format(`
const formattedConf = format(`${rawHeader}
module.exports = function(config){
config.set(
${JSON.stringify(configObject, null, 2)}
${rawConfig}
);
}`, { parser: 'babylon' });
return fsAsPromised.writeFile(STRYKER_CONFIG_FILE, formattedConf);
}

private writeStrykerConfig(configObject: Partial<StrykerOptions>) {
return this.writeStrykerConfigRaw(this.wrapInModule(configObject));
}

private wrapInModule(configObject: Partial<StrykerOptions>): string {
return JSON.stringify(configObject, null, 2);
}
}
64 changes: 47 additions & 17 deletions packages/stryker/src/initializer/StrykerInitializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { getLogger } from 'stryker-api/logging';
import { filterEmpty } from '../utils/objectUtils';
import StrykerConfigWriter from './StrykerConfigWriter';
import CommandTestRunner from '../test-runner/CommandTestRunner';
import StrykerPresets from './StrykerPresets';
import Preset from './presets/Preset';

const enum PackageManager {
Npm = 'npm',
Expand All @@ -17,7 +19,7 @@ export default class StrykerInitializer {
private readonly log = getLogger(StrykerInitializer.name);
private readonly inquirer = new StrykerInquirer();

constructor(private readonly out = console.log, private readonly client: NpmClient = new NpmClient()) { }
constructor(private readonly out = console.log, private readonly client: NpmClient = new NpmClient(), private readonly strykerPresets: Preset[] = StrykerPresets) { }

/**
* Runs the initializer will prompt the user for questions about his setup. After that, install plugins and configure Stryker.
Expand All @@ -27,6 +29,50 @@ export default class StrykerInitializer {
const configWriter = new StrykerConfigWriter(this.out);
configWriter.guardForExistingConfig();
this.patchProxies();
const selectedPreset = await this.selectPreset();
if (selectedPreset) {
await this.initiatePreset(configWriter, selectedPreset);
}
else {
await this.initiateCustom(configWriter);
}
this.out('Done configuring stryker. Please review `stryker.conf.js`, you might need to configure transpilers or your test runner correctly.');
this.out('Let\'s kill some mutants with this command: `stryker run`');
}

/**
* The typed rest client works only with the specific HTTP_PROXY and HTTPS_PROXY env settings.
* Let's make sure they are available.
*/
private patchProxies() {
const copyEnvVariable = (from: string, to: string) => {
if (process.env[from] && !process.env[to]) {
process.env[to] = process.env[from];
}
};
copyEnvVariable('http_proxy', 'HTTP_PROXY');
copyEnvVariable('https_proxy', 'HTTPS_PROXY');
}

private async selectPreset(): Promise<Preset | undefined> {
const presetOptions: Preset[] = this.strykerPresets;
if (presetOptions.length) {
this.log.debug(`Found presets: ${JSON.stringify(presetOptions)}`);
return this.inquirer.promptPresets(presetOptions);
} else {
this.log.debug('No presets have been configured, reverting to custom configuration');
return undefined;
}
}

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

private async initiateCustom(configWriter: StrykerConfigWriter) {
const selectedTestRunner = await this.selectTestRunner();
const selectedTestFramework = selectedTestRunner && !CommandTestRunner.is(selectedTestRunner.name)
? await this.selectTestFramework(selectedTestRunner) : null;
Expand All @@ -47,22 +93,6 @@ export default class StrykerInitializer {
selectedPackageManager,
await this.fetchAdditionalConfig(npmDependencies));
this.installNpmDependencies(npmDependencies, selectedPackageManager);
this.out('Done configuring stryker. Please review `stryker.conf.js`, you might need to configure transpilers or your test runner correctly.');
this.out('Let\'s kill some mutants with this command: `stryker run`');
}

/**
* The typed rest client works only with the specific HTTP_PROXY and HTTPS_PROXY env settings.
* Let's make sure they are available.
*/
private patchProxies() {
const copyEnvVariable = (from: string, to: string) => {
if (process.env[from] && !process.env[to]) {
process.env[to] = process.env[from];
}
};
copyEnvVariable('http_proxy', 'HTTP_PROXY');
copyEnvVariable('https_proxy', 'HTTPS_PROXY');
}

private async selectTestRunner(): Promise<PromptOption | null> {
Expand Down
14 changes: 14 additions & 0 deletions packages/stryker/src/initializer/StrykerInquirer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as inquirer from 'inquirer';
import PromptOption from './PromptOption';
import CommandTestRunner from '../test-runner/CommandTestRunner';
import Preset from './presets/Preset';

export interface PromptResult {
additionalNpmDependencies: string[];
Expand All @@ -9,6 +10,19 @@ export interface PromptResult {

export class StrykerInquirer {

public async promptPresets(options: Preset[]): Promise<Preset | undefined> {
const choices: inquirer.ChoiceType[] = options.map(_ => _.name);
choices.push(new inquirer.Separator());
choices.push('None/other');
const answers = await inquirer.prompt<{ preset: string }>({
choices,
message: 'Are you using one of these frameworks? Then select a preset configuration.',
name: 'preset',
type: 'list'
});
return options.find(_ => _.name === answers.preset);
}

public async promptTestRunners(options: PromptOption[]): Promise<PromptOption> {
const choices: inquirer.ChoiceType[] = options.map(_ => _.name);
choices.push(new inquirer.Separator());
Expand Down
8 changes: 8 additions & 0 deletions packages/stryker/src/initializer/StrykerPresets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AngularPreset } from './presets/AngularPreset';
import { ReactPreset } from './presets/ReactPreset';
import Preset from './presets/Preset';
import { VueJsPreset } from './presets/VueJsPreset';

// Add new presets here
const strykerPresets: Preset[] = [ new AngularPreset(), new ReactPreset(), new VueJsPreset() ];
export default strykerPresets;
39 changes: 39 additions & 0 deletions packages/stryker/src/initializer/presets/AngularPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Preset from './Preset';
import PresetConfiguration from './PresetConfiguration';
import * as os from 'os';

const handbookUrl = 'https://github.com/stryker-mutator/stryker-handbook/blob/master/stryker/guides/angular.md#angular';

export class AngularPreset implements Preset {
public readonly name = 'angular-cli';
// Please keep config in sync with handbook
private readonly dependencies = [
'stryker',
'stryker-karma-runner',
'stryker-typescript',
'stryker-html-reporter'
];
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'
}`;
public async createConfig(): Promise<PresetConfiguration> {
return { config: this.config, handbookUrl, dependencies: this.dependencies };
}
}
7 changes: 7 additions & 0 deletions packages/stryker/src/initializer/presets/Preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import PresetConfig from './PresetConfiguration';

interface Preset {
readonly name: string;
createConfig(): Promise<PresetConfig>;
}
export default Preset;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default interface PresetConfiguration {
config: string;
handbookUrl: string;
dependencies: string[];
}
60 changes: 60 additions & 0 deletions packages/stryker/src/initializer/presets/ReactPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Preset from './Preset';
import inquirer = require('inquirer');
import PresetConfiguration from './PresetConfiguration';

const handbookUrl = 'https://github.com/stryker-mutator/stryker-handbook/blob/master/stryker/guides/react.md#react';

/**
* More information can be found in the Stryker handbook:
* https://github.com/stryker-mutator/stryker-handbook/blob/master/stryker/guides/react.md#react
*/
export class ReactPreset implements Preset {
public readonly name = 'react';
private readonly generalDependencies = [
'stryker',
'stryker-jest-runner',
'stryker-html-reporter'
];

private readonly sharedConfig = `testRunner: 'jest',
reporters: ['progress', 'clear-text', 'html'],
coverageAnalysis: 'off',
jest: {
projectType: 'react'
}
`;

private readonly tsxDependencies = ['stryker-typescript', ...this.generalDependencies];
private readonly tsxConf = `{
mutate: ['src/**/*.ts?(x)', '!src/**/*@(.test|.spec|Spec).ts?(x)'],
mutator: 'typescript',
${this.sharedConfig}
}`;

private readonly jsxDependencies = ['stryker-javascript-mutator', ...this.generalDependencies];
private readonly jsxConf = `{
mutate: ['src/**/*.js?(x)', '!src/**/*@(.test|.spec|Spec).js?(x)'],
mutator: 'javascript',
${this.sharedConfig}
}`;

public async createConfig(): Promise<PresetConfiguration> {
const choices: inquirer.ChoiceType[] = ['JSX', 'TSX'];
const answers = await inquirer.prompt<{ choice: string }>({
choices,
message: 'Is your project a JSX project or a TSX project?',
name: 'choice',
type: 'list'
});
return this.load(answers.choice);
}
private load(choice: string): PresetConfiguration {
if (choice === 'JSX') {
return { config: this.jsxConf, handbookUrl, dependencies: this.jsxDependencies };
} else if (choice === 'TSX') {
return { config: this.tsxConf, handbookUrl, dependencies: this.tsxDependencies };
} else {
throw new Error(`Invalid project type ${choice}`);
}
}
}
Loading

0 comments on commit 5673e6b

Please sign in to comment.