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(validation): add validation on plugin options #2158

Merged
merged 16 commits into from
Apr 24, 2020
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
3 changes: 2 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[{*.ts,*.js,*jsx,*tsx,*.json,*.code-workspace}]
insert_final_newline = true
indent_style = space
indent_size = 2
indent_size = 2
end_of_line = lf
4 changes: 4 additions & 0 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type WritableMetricsResult = {
-readonly [K in keyof MetricsResult]: MetricsResult[K];
};

export function readLogFile(fileName = path.resolve('stryker.log')): Promise<string> {
return fs.readFile(fileName, 'utf8');
}

export async function expectMetricsResult(expectedMetricsResult: Partial<MetricsResult>) {
const actualMetricsResult = await readMutationTestResult();
const actualSnippet: Partial<WritableMetricsResult> = {};
Expand Down
7 changes: 7 additions & 0 deletions e2e/test/plugin-options-validation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"scripts": {
"pretest": "rimraf stryker.log",
"test": "stryker run --fileLogLevel info stryker-error-in-plugin-options.conf.json || exit 0",
"posttest": "mocha --require \"ts-node/register\" verify/verify.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker/master/packages/api/schema/stryker-core.json",
"mochaOptions": {
"spec": "this should have been an array"
},
"tsconfigFile": 42,
"babel": {
"extensions": "should be an array"
},
"jasmineConfigFile": {
"should": "be a string"
},
"karma": {
"projectType": "Project type not supported"
},
"webpack": {
"configFile": [
"should be a string"
]
}
}
16 changes: 16 additions & 0 deletions e2e/test/plugin-options-validation/verify/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect } from 'chai';
import { readLogFile } from '../../../helpers';

describe('Verify errors', () => {

it('should report the expected errors', async () => {
const logFile = await readLogFile();
expect(logFile).includes('Config option "mochaOptions.spec" has the wrong type');
expect(logFile).includes('Config option "tsconfigFile" has the wrong type');
expect(logFile).includes('Config option "babel.extensions" has the wrong type');
expect(logFile).includes('Config option "jasmineConfigFile" has the wrong type');
expect(logFile).not.includes('Config option "karma.projectType" has the wrong type');
expect(logFile).includes('Config option "karma.projectType" should be one of the allowed values');
expect(logFile).includes('Config option "webpack.configFile" has the wrong type');
});
});
46 changes: 45 additions & 1 deletion packages/api/schema/stryker-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@
"MutationScore"
]
},
"clearTextReporterOptions": {
"title": "ClearTextReporterOptions",
"type": "object",
"properties": {
"allowColor": {
"description": "Indicates whether or not to use color coding in output.",
"type": "boolean",
"default": true
},
"logTests": {
"description": "Indicates whether or not to log which tests were executed for a given mutant.",
"type": "boolean",
"default": true
},
"maxTestsToLog": {
"description": "Indicates the maximum amount of test to log when `logTests` is enabled",
"type": "integer",
"minimum": 0,
"default": 3
}
}
},
"dashboardOptions": {
"title": "DashboardOptions",
"additionalProperties": false,
Expand Down Expand Up @@ -67,6 +89,18 @@
}
}
},
"eventRecorderOptions": {
"title": "EventRecorderOptions",
"additionalProperties": false,
"type": "object",
"properties": {
"baseDir": {
"description": "The base dir to write the events to",
"type": "string",
"default": "reports/mutation/events"
}
}
},
"htmlReporterOptions": {
"title": "HtmlReporterOptions",
"additionalProperties": false,
Expand Down Expand Up @@ -164,11 +198,21 @@
],
"default": "off"
},
"clearTextReporter": {
"description": "The options for the clear text reporter.",
"$ref": "#/definitions/clearTextReporterOptions",
"default": {}
},
"dashboard": {
"description": "The options for the dashboard reporter",
"description": "The options for the dashboard reporter.",
"$ref": "#/definitions/dashboardOptions",
"default": {}
},
"eventReporter": {
"description": "The options for the event recorder reporter.",
"$ref": "#/definitions/eventRecorderOptions",
"default": {}
},
"fileLogLevel": {
"description": "Set the log level that Stryker uses to write to the \"stryker.log\" file",
"$ref": "#/definitions/logLevel",
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/core/OptionsEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { StrykerOptions } from '../../core';
* editing of the configuration object is done by reference.
*
*/
export interface OptionsEditor {
export interface OptionsEditor<T extends StrykerOptions = StrykerOptions> {
/**
* Extending classes only need to implement the edit method, this method
* receives a writable config object that can be edited in any way.
Expand All @@ -18,5 +18,5 @@ export interface OptionsEditor {
*
* @param options: The stryker configuration object
*/
edit(options: StrykerOptions): void;
edit(options: T): void;
}
1 change: 1 addition & 0 deletions packages/api/src/plugin/Plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,5 @@ export type Plugins = {
export interface PluginResolver {
resolve<T extends keyof Plugins>(kind: T, name: string): Plugins[T];
resolveAll<T extends keyof Plugins>(kind: T): Array<Plugins[T]>;
resolveValidationSchemaContributions(): object[];
}
8 changes: 8 additions & 0 deletions packages/api/testResources/module/useCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ const optionsAllArgs: StrykerOptions = {
thresholds: { high: 80, low: 20, break: null},
timeoutFactor: 1.5,
timeoutMS: 5000,
clearTextReporter: {
allowColor: true,
logTests: true,
maxTestsToLog: 3,
},
eventReporter: {
baseDir: 'reports/mutation/events'
},
transpilers: [],
dashboard: {
baseUrl: 'baseUrl',
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-transpiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "A plugin for babel projects using Stryker",
"main": "src/index.js",
"scripts": {
"test": "nyc --exclude-after-remap=false --check-coverage --reporter=html --report-dir=reports/coverage --lines 90 --functions 90 --branches 80 npm run mocha",
"test": "nyc --exclude-after-remap=false --check-coverage --reporter=html --report-dir=reports/coverage --lines 90 --functions 80 --branches 80 npm run mocha",
"mocha": "mocha \"test/helpers/**/*.js\" \"test/unit/**/*.js\" && mocha --timeout 100000 \"test/helpers/**/*.js\" \"test/integration/**/*.js\"",
"stryker": "node ../core/bin/stryker run"
},
Expand Down
57 changes: 30 additions & 27 deletions packages/babel-transpiler/src/BabelConfigReader.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,30 @@
import * as fs from 'fs';
import * as path from 'path';

import { StrykerOptions } from '@stryker-mutator/api/core';
import { Logger } from '@stryker-mutator/api/logging';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';

import * as babel from './helpers/babelWrapper';

export interface StrykerBabelConfig {
extensions: readonly string[];
options: babel.TransformOptions;
optionsFile: string | null;
optionsApi?: Partial<babel.ConfigAPI>;
}

export const CONFIG_KEY = 'babel';
export const FILE_KEY: keyof StrykerBabelConfig = 'optionsFile';
export const OPTIONS_KEY: keyof StrykerBabelConfig = 'options';
export const EXTENSIONS_KEY: keyof StrykerBabelConfig = 'extensions';
import { StrykerBabelConfig } from '../src-generated/babel-transpiler-options';

const DEFAULT_BABEL_CONFIG: Readonly<StrykerBabelConfig> = Object.freeze({
extensions: Object.freeze([]),
options: Object.freeze({}),
optionsFile: '.babelrc',
});
import * as babel from './helpers/babelWrapper';
import { BabelTranspilerWithStrykerOptions } from './BabelTranspilerWithStrykerOptions';
import { ConfigAPI } from './helpers/babelWrapper';

export class BabelConfigReader {
public static inject = tokens(commonTokens.logger);
constructor(private readonly log: Logger) {}

public readConfig(strykerOptions: StrykerOptions): StrykerBabelConfig {
const babelConfig: StrykerBabelConfig = {
...DEFAULT_BABEL_CONFIG,
...strykerOptions[CONFIG_KEY],
};
public readConfig(strykerOptions: BabelTranspilerWithStrykerOptions): StrykerBabelConfig {
const babelConfig = { ...strykerOptions.babel };
babelConfig.options = {
...this.readBabelOptionsFromFile(babelConfig.optionsFile, babelConfig.optionsApi),
...this.readBabelOptionsFromFile(babelConfig.optionsFile),
...babelConfig.options,
};
this.log.debug(`Babel config is: ${JSON.stringify(babelConfig, null, 2)}`);
return babelConfig;
}

private readBabelOptionsFromFile(relativeFileName: string | null, optionsApi?: Partial<babel.ConfigAPI>): babel.TransformOptions {
private readBabelOptionsFromFile(relativeFileName: string | null): babel.TransformOptions {
if (relativeFileName) {
const babelrcPath = path.resolve(relativeFileName);
this.log.debug(`Reading .babelrc file from path "${babelrcPath}"`);
Expand All @@ -55,7 +37,7 @@ export class BabelConfigReader {
const config = require(babelrcPath);
if (typeof config === 'function') {
const configFunction = config as babel.ConfigFunction;
return configFunction(optionsApi as babel.ConfigAPI);
return configFunction(noopBabelConfigApi);
} else {
return config as babel.TransformOptions;
}
Expand All @@ -71,3 +53,24 @@ export class BabelConfigReader {
return {};
}
}

function noop() {}

const noopBabelConfigApi: ConfigAPI = {
assertVersion() {
return true;
},
cache: {
forever: noop,
invalidate() {
return noop as any;
},
never: noop,
using() {
return noop as any;
},
},
env: noop as any,
caller: noop as any,
version: noop as any,
};
7 changes: 5 additions & 2 deletions packages/babel-transpiler/src/BabelTranspiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { commonTokens, Injector, tokens, TranspilerPluginContext } from '@stryke
import { Transpiler } from '@stryker-mutator/api/transpile';
import { StrykerError } from '@stryker-mutator/util';

import { BabelConfigReader, StrykerBabelConfig } from './BabelConfigReader';
import { StrykerBabelConfig } from '../src-generated/babel-transpiler-options';

import { BabelConfigReader } from './BabelConfigReader';
import * as babel from './helpers/babelWrapper';
import { toJSFileName } from './helpers/helpers';
import { BabelTranspilerWithStrykerOptions } from './BabelTranspilerWithStrykerOptions';

const DEFAULT_EXTENSIONS: readonly string[] = babel.DEFAULT_EXTENSIONS;

Expand All @@ -28,7 +31,7 @@ export class BabelTranspiler implements Transpiler {
`Invalid \`coverageAnalysis\` "${options.coverageAnalysis}" is not supported by the stryker-babel-transpiler. Not able to produce source maps yet. Please set it to "off".`
);
}
this.babelConfig = babelConfigReader.readConfig(options);
this.babelConfig = babelConfigReader.readConfig(options as BabelTranspilerWithStrykerOptions);
this.projectRoot = this.determineProjectRoot();
this.extensions = [...DEFAULT_EXTENSIONS, ...this.babelConfig.extensions];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { StrykerOptions } from '@stryker-mutator/api/core';

import { BabelTranspilerOptions } from '../src-generated/babel-transpiler-options';

export interface BabelTranspilerWithStrykerOptions extends BabelTranspilerOptions, StrykerOptions {}
2 changes: 2 additions & 0 deletions packages/babel-transpiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ import { declareFactoryPlugin, PluginKind } from '@stryker-mutator/api/plugin';

import { babelTranspilerFactory } from './BabelTranspiler';

export * as strykerValidationSchema from '../schema/babel-transpiler-options.json';

export const strykerPlugins = [declareFactoryPlugin(PluginKind.Transpiler, 'babel', babelTranspilerFactory)];
10 changes: 10 additions & 0 deletions packages/babel-transpiler/test/helpers/factories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StrykerBabelConfig } from '../../src-generated/babel-transpiler-options';

export function createStrykerBabelConfig(overrides?: Partial<StrykerBabelConfig>): StrykerBabelConfig {
return {
extensions: ['.js', '.jsx', '.es6', '.es', '.mjs'],
optionsFile: '.babelrc',
options: {},
...overrides,
};
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import * as path from 'path';

import { ConfigAPI } from '@babel/core';
import { File } from '@stryker-mutator/api/core';
import { commonTokens } from '@stryker-mutator/api/plugin';
import { testInjector } from '@stryker-mutator/test-helpers';
import { expect } from 'chai';

import { CONFIG_KEY, StrykerBabelConfig } from '../../src/BabelConfigReader';
import { BabelTranspiler, babelTranspilerFactory } from '../../src/BabelTranspiler';
import { ProjectLoader } from '../helpers/projectLoader';
import { StrykerBabelConfig } from '../../src-generated/babel-transpiler-options';
import { BabelTranspilerWithStrykerOptions } from '../../src/BabelTranspilerWithStrykerOptions';
import { createStrykerBabelConfig } from '../helpers/factories';

function describeIntegrationTest(projectName: string, babelConfig: Partial<StrykerBabelConfig> = {}) {
const projectDir = path.resolve(__dirname, '..', '..', 'testResources', projectName);
Expand All @@ -20,7 +21,7 @@ function describeIntegrationTest(projectName: string, babelConfig: Partial<Stryk
beforeEach(async () => {
projectFiles = await ProjectLoader.getFiles(path.join(projectDir, 'source'));
resultFiles = await ProjectLoader.getFiles(path.join(projectDir, 'expectedResult'));
testInjector.options[CONFIG_KEY] = babelConfig;
((testInjector.options as unknown) as BabelTranspilerWithStrykerOptions).babel = createStrykerBabelConfig(babelConfig);
babelTranspiler = testInjector.injector.provideValue(commonTokens.produceSourceMaps, false).injectFunction(babelTranspilerFactory);
});

Expand Down Expand Up @@ -66,10 +67,8 @@ describe('Different extensions', () => {
describeIntegrationTest('differentExtensions');
});
describe('A Babel project with babel.config.js config file that exports function', () => {
const noop = () => {};
describeIntegrationTest('babelProjectWithBabelConfigJs', {
extensions: ['.ts'],
optionsApi: { cache: { forever: noop } } as ConfigAPI,
optionsFile: 'babel.config.js',
});
});
Expand Down
Loading