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(Stryker CLI 'init'): Support for preset configuration during 'stryker init' #1248

Merged
merged 15 commits into from
Nov 27, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
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) {
Copy link
Member

Choose a reason for hiding this comment

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

Could we introduce a function here in the then branch?

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