diff --git a/.editorconfig b/.editorconfig index ae54398377..64ab1ec096 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,5 @@ [{*.ts,*.js,*jsx,*tsx,*.json,*.code-workspace}] insert_final_newline = true indent_style = space -indent_size = 2 \ No newline at end of file +indent_size = 2 +end_of_line = lf \ No newline at end of file diff --git a/e2e/helpers.ts b/e2e/helpers.ts index f6c525721c..7923a7cb69 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -19,6 +19,10 @@ type WritableMetricsResult = { -readonly [K in keyof MetricsResult]: MetricsResult[K]; }; +export function readLogFile(fileName = path.resolve('stryker.log')): Promise { + return fs.readFile(fileName, 'utf8'); +} + export async function expectMetricsResult(expectedMetricsResult: Partial) { const actualMetricsResult = await readMutationTestResult(); const actualSnippet: Partial = {}; diff --git a/e2e/test/plugin-options-validation/package.json b/e2e/test/plugin-options-validation/package.json new file mode 100644 index 0000000000..fe203e7e5b --- /dev/null +++ b/e2e/test/plugin-options-validation/package.json @@ -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" + } +} diff --git a/e2e/test/plugin-options-validation/stryker-error-in-plugin-options.conf.json b/e2e/test/plugin-options-validation/stryker-error-in-plugin-options.conf.json new file mode 100644 index 0000000000..26f3aa9e92 --- /dev/null +++ b/e2e/test/plugin-options-validation/stryker-error-in-plugin-options.conf.json @@ -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" + ] + } +} diff --git a/e2e/test/plugin-options-validation/verify/verify.ts b/e2e/test/plugin-options-validation/verify/verify.ts new file mode 100644 index 0000000000..bbe8a2c848 --- /dev/null +++ b/e2e/test/plugin-options-validation/verify/verify.ts @@ -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'); + }); +}); diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index bd57b4f765..caffe47094 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -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, @@ -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, @@ -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", diff --git a/packages/api/src/core/OptionsEditor.ts b/packages/api/src/core/OptionsEditor.ts index fe0855d9e7..df61720203 100644 --- a/packages/api/src/core/OptionsEditor.ts +++ b/packages/api/src/core/OptionsEditor.ts @@ -9,7 +9,7 @@ import { StrykerOptions } from '../../core'; * editing of the configuration object is done by reference. * */ -export interface OptionsEditor { +export interface OptionsEditor { /** * Extending classes only need to implement the edit method, this method * receives a writable config object that can be edited in any way. @@ -18,5 +18,5 @@ export interface OptionsEditor { * * @param options: The stryker configuration object */ - edit(options: StrykerOptions): void; + edit(options: T): void; } diff --git a/packages/api/src/plugin/Plugins.ts b/packages/api/src/plugin/Plugins.ts index 600889091e..9b5e8f0dd7 100644 --- a/packages/api/src/plugin/Plugins.ts +++ b/packages/api/src/plugin/Plugins.ts @@ -105,4 +105,5 @@ export type Plugins = { export interface PluginResolver { resolve(kind: T, name: string): Plugins[T]; resolveAll(kind: T): Array; + resolveValidationSchemaContributions(): object[]; } diff --git a/packages/api/testResources/module/useCore.ts b/packages/api/testResources/module/useCore.ts index 395be35cf6..30b15da9b6 100644 --- a/packages/api/testResources/module/useCore.ts +++ b/packages/api/testResources/module/useCore.ts @@ -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', diff --git a/packages/babel-transpiler/package.json b/packages/babel-transpiler/package.json index 40715729ae..6c7451f2ec 100644 --- a/packages/babel-transpiler/package.json +++ b/packages/babel-transpiler/package.json @@ -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" }, diff --git a/packages/babel-transpiler/src/BabelConfigReader.ts b/packages/babel-transpiler/src/BabelConfigReader.ts index 51aa92d97d..d3f4c424d7 100644 --- a/packages/babel-transpiler/src/BabelConfigReader.ts +++ b/packages/babel-transpiler/src/BabelConfigReader.ts @@ -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; -} - -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 = 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.TransformOptions { + private readBabelOptionsFromFile(relativeFileName: string | null): babel.TransformOptions { if (relativeFileName) { const babelrcPath = path.resolve(relativeFileName); this.log.debug(`Reading .babelrc file from path "${babelrcPath}"`); @@ -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; } @@ -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, +}; diff --git a/packages/babel-transpiler/src/BabelTranspiler.ts b/packages/babel-transpiler/src/BabelTranspiler.ts index 8d3728eed8..d18893679c 100644 --- a/packages/babel-transpiler/src/BabelTranspiler.ts +++ b/packages/babel-transpiler/src/BabelTranspiler.ts @@ -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; @@ -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]; } diff --git a/packages/babel-transpiler/src/BabelTranspilerWithStrykerOptions.ts b/packages/babel-transpiler/src/BabelTranspilerWithStrykerOptions.ts new file mode 100644 index 0000000000..fe4b9aba1a --- /dev/null +++ b/packages/babel-transpiler/src/BabelTranspilerWithStrykerOptions.ts @@ -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 {} diff --git a/packages/babel-transpiler/src/index.ts b/packages/babel-transpiler/src/index.ts index 24e46edc38..9c2f2e45d7 100644 --- a/packages/babel-transpiler/src/index.ts +++ b/packages/babel-transpiler/src/index.ts @@ -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)]; diff --git a/packages/babel-transpiler/test/helpers/factories.ts b/packages/babel-transpiler/test/helpers/factories.ts new file mode 100644 index 0000000000..79321441a8 --- /dev/null +++ b/packages/babel-transpiler/test/helpers/factories.ts @@ -0,0 +1,10 @@ +import { StrykerBabelConfig } from '../../src-generated/babel-transpiler-options'; + +export function createStrykerBabelConfig(overrides?: Partial): StrykerBabelConfig { + return { + extensions: ['.js', '.jsx', '.es6', '.es', '.mjs'], + optionsFile: '.babelrc', + options: {}, + ...overrides, + }; +} diff --git a/packages/babel-transpiler/test/integration/BabelProjects.it.spec.ts b/packages/babel-transpiler/test/integration/BabelProjects.it.spec.ts index 9adefb3b4b..9e8ca36ad7 100644 --- a/packages/babel-transpiler/test/integration/BabelProjects.it.spec.ts +++ b/packages/babel-transpiler/test/integration/BabelProjects.it.spec.ts @@ -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 = {}) { const projectDir = path.resolve(__dirname, '..', '..', 'testResources', projectName); @@ -20,7 +21,7 @@ function describeIntegrationTest(projectName: string, babelConfig: Partial { 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); }); @@ -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', }); }); diff --git a/packages/babel-transpiler/test/unit/BabelConfigReader.spec.ts b/packages/babel-transpiler/test/unit/BabelConfigReader.spec.ts index a2eee285f6..11946e413c 100644 --- a/packages/babel-transpiler/test/unit/BabelConfigReader.spec.ts +++ b/packages/babel-transpiler/test/unit/BabelConfigReader.spec.ts @@ -5,7 +5,9 @@ import { factory, testInjector } from '@stryker-mutator/test-helpers'; import { expect } from 'chai'; import * as sinon from 'sinon'; -import { BabelConfigReader, StrykerBabelConfig } from '../../src/BabelConfigReader'; +import { BabelConfigReader } from '../../src/BabelConfigReader'; +import { BabelTranspilerOptions, StrykerBabelConfig } from '../../src-generated/babel-transpiler-options'; +import { createStrykerBabelConfig } from '../helpers/factories'; describe(BabelConfigReader.name, () => { let sut: BabelConfigReader; @@ -15,14 +17,14 @@ describe(BabelConfigReader.name, () => { }); it('should read babel configuration from StrykerOptions', () => { - const babelConfig: Partial = { + const babelConfig: StrykerBabelConfig = { extensions: ['.ts'], options: { presets: ['env'], }, optionsFile: null, }; - const options = factory.strykerOptions({ babel: babelConfig }); + const options = factory.strykerWithPluginOptions({ babel: babelConfig }); const result = sut.readConfig(options); expect(result).deep.eq(babelConfig); }); @@ -31,14 +33,14 @@ describe(BabelConfigReader.name, () => { // Arrange const babelOptions = { presets: ['env'] }; arrangeBabelOptionsFile(babelOptions, '.babelrc'); - const result = sut.readConfig(factory.strykerOptions()); + const result = sut.readConfig(factory.strykerWithPluginOptions({ babel: createStrykerBabelConfig() })); expect(result.options).deep.eq(babelOptions); expect(result.optionsFile).deep.eq('.babelrc'); }); it('should log the path to the babelrc file', () => { arrangeBabelOptionsFile({}); - sut.readConfig(factory.strykerOptions()); + sut.readConfig(factory.strykerWithPluginOptions({ babel: createStrykerBabelConfig() })); expect(testInjector.logger.debug).calledWith(`Reading .babelrc file from path "${path.resolve('.babelrc')}"`); }); @@ -48,7 +50,7 @@ describe(BabelConfigReader.name, () => { options: { presets: ['env'] }, optionsFile: null, }; - sut.readConfig(factory.strykerOptions({ babel: expectedConfig })); + sut.readConfig(factory.strykerWithPluginOptions({ babel: expectedConfig })); expect(testInjector.logger.debug).calledWith(`Babel config is: ${JSON.stringify(expectedConfig, null, 2)}`); }); @@ -56,14 +58,16 @@ describe(BabelConfigReader.name, () => { const babelConfig = { optionsFile: '.nonExistingBabelrc', }; - sut.readConfig(factory.strykerOptions({ babel: babelConfig })); + sut.readConfig(factory.strykerWithPluginOptions({ babel: createStrykerBabelConfig(babelConfig) })); expect(testInjector.logger.error).calledWith(`babelrc file does not exist at: ${path.resolve(babelConfig.optionsFile)}`); }); it('should log a warning if the babelrc file cannot be read', () => { sinon.stub(fs, 'existsSync').returns(true); sinon.stub(fs, 'readFileSync').withArgs(path.resolve('.babelrc'), 'utf8').returns('something, not json'); - sut.readConfig(factory.strykerOptions()); + sut.readConfig( + factory.strykerWithPluginOptions({ babel: createStrykerBabelConfig() }) + ); expect(testInjector.logger.error).calledWith( `Error while reading "${path.resolve('.babelrc')}" file: SyntaxError: Unexpected token s in JSON at position 0` ); @@ -75,7 +79,7 @@ describe(BabelConfigReader.name, () => { options: {}, optionsFile: '.babelrc', }; - const result = sut.readConfig(factory.strykerOptions()); + const result = sut.readConfig(factory.strykerWithPluginOptions({ babel: expected })); expect(result).deep.equal(expected); }); diff --git a/packages/babel-transpiler/test/unit/BabelTranspiler.spec.ts b/packages/babel-transpiler/test/unit/BabelTranspiler.spec.ts index 29f5d6f209..bac953c67e 100644 --- a/packages/babel-transpiler/test/unit/BabelTranspiler.spec.ts +++ b/packages/babel-transpiler/test/unit/BabelTranspiler.spec.ts @@ -6,10 +6,11 @@ import { factory, testInjector } from '@stryker-mutator/test-helpers'; import { expect } from 'chai'; import * as sinon from 'sinon'; -import { BabelConfigReader, StrykerBabelConfig } from '../../src/BabelConfigReader'; +import { BabelConfigReader } from '../../src/BabelConfigReader'; import { BabelTranspiler } from '../../src/BabelTranspiler'; import * as babel from '../../src/helpers/babelWrapper'; import { Mock, mock } from '../helpers/mock'; +import { StrykerBabelConfig } from '../../src-generated/babel-transpiler-options'; describe(BabelTranspiler.name, () => { let sut: BabelTranspiler; @@ -85,7 +86,7 @@ describe(BabelTranspiler.name, () => { it('should allow users to define babel options', async () => { const plugins = ['fooPlugin', 'barPlugin']; - babelConfig.options.plugins = plugins.slice(); + babelConfig.options!.plugins = plugins.slice(); arrangeHappyFlow(); await sut.transpile(files); files.forEach((file) => { @@ -99,8 +100,8 @@ describe(BabelTranspiler.name, () => { }); it('should not allow a user to override the file name', async () => { - babelConfig.options.filename = 'override'; - babelConfig.options.filenameRelative = 'override'; + babelConfig.options!.filename = 'override'; + babelConfig.options!.filenameRelative = 'override'; arrangeHappyFlow(); sut = new BabelTranspiler(options, /*produceSourceMaps:*/ false, (babelConfigReaderMock as unknown) as BabelConfigReader); await sut.transpile([files[0]]); diff --git a/packages/babel-transpiler/testResources/babelProjectWithBabelConfigJs/babel.config.js b/packages/babel-transpiler/testResources/babelProjectWithBabelConfigJs/babel.config.js index 45d98b91c1..a34a6a5a4a 100644 --- a/packages/babel-transpiler/testResources/babelProjectWithBabelConfigJs/babel.config.js +++ b/packages/babel-transpiler/testResources/babelProjectWithBabelConfigJs/babel.config.js @@ -1,4 +1,4 @@ -module.exports = api => { +module.exports = (api) => { api.cache.forever(); return { diff --git a/packages/babel-transpiler/tsconfig.src.json b/packages/babel-transpiler/tsconfig.src.json index b0303a444b..4861baccc2 100644 --- a/packages/babel-transpiler/tsconfig.src.json +++ b/packages/babel-transpiler/tsconfig.src.json @@ -5,7 +5,8 @@ }, "include": [ "src", - "src-generated" + "src-generated", + "schema/*.json" ], "references": [ { diff --git a/packages/core/src/TestableMutant.ts b/packages/core/src/TestableMutant.ts index ae89d1a51a..ed3f102c5a 100644 --- a/packages/core/src/TestableMutant.ts +++ b/packages/core/src/TestableMutant.ts @@ -3,9 +3,9 @@ import { Mutant } from '@stryker-mutator/api/mutant'; import { MutantResult, MutantStatus } from '@stryker-mutator/api/report'; import { TestSelection } from '@stryker-mutator/api/test_framework'; import { RunResult, TestResult } from '@stryker-mutator/api/test_runner'; +import { deepFreeze } from '@stryker-mutator/util'; import SourceFile, { isLineBreak } from './SourceFile'; -import { freezeRecursively } from './utils/objectUtils'; export enum TestSelectionResult { Failed, @@ -117,7 +117,7 @@ export default class TestableMutant { } public createResult(status: MutantStatus, testsRan: string[]): MutantResult { - return freezeRecursively({ + return deepFreeze({ id: this.id, location: this.location, mutatedLines: this.mutatedLines, @@ -128,7 +128,7 @@ export default class TestableMutant { sourceFilePath: this.fileName, status, testsRan, - }); + }) as MutantResult; } public toString() { diff --git a/packages/core/src/config/ConfigReader.ts b/packages/core/src/config/ConfigReader.ts index 345619ee52..4ad737b0ee 100644 --- a/packages/core/src/config/ConfigReader.ts +++ b/packages/core/src/config/ConfigReader.ts @@ -59,7 +59,7 @@ export default class ConfigReader { } } - if (this.cliOptions.configFile) { + if (typeof this.cliOptions.configFile === 'string') { this.log.debug(`Loading config ${this.cliOptions.configFile}`); const configFile = this.resolveConfigFile(this.cliOptions.configFile); try { diff --git a/packages/core/src/config/OptionsValidator.ts b/packages/core/src/config/OptionsValidator.ts index da359de830..f77dbf71e6 100644 --- a/packages/core/src/config/OptionsValidator.ts +++ b/packages/core/src/config/OptionsValidator.ts @@ -9,7 +9,7 @@ import { ConfigError } from '../errors'; import { describeErrors } from './validationErrors'; -const ajv = new Ajv({ useDefaults: true, allErrors: true, jsonPointers: false, verbose: true }); +const ajv = new Ajv({ useDefaults: true, allErrors: true, jsonPointers: false, verbose: true, missingRefs: 'ignore', logger: false }); export class OptionsValidator { private readonly validateFn: Ajv.ValidateFunction; @@ -67,3 +67,9 @@ export function defaultOptions(): StrykerOptions { validator.validate(options); return options; } + +export function validateOptions(options: unknown, optionsValidator: OptionsValidator): StrykerOptions { + optionsValidator.validate(options); + return options; +} +validateOptions.inject = tokens(commonTokens.options, coreTokens.optionsValidator); diff --git a/packages/core/src/config/buildSchemaWithPluginContributions.ts b/packages/core/src/config/buildSchemaWithPluginContributions.ts new file mode 100644 index 0000000000..1be72f8314 --- /dev/null +++ b/packages/core/src/config/buildSchemaWithPluginContributions.ts @@ -0,0 +1,27 @@ +import { commonTokens, PluginResolver, tokens } from '@stryker-mutator/api/plugin'; +import { Logger } from '@stryker-mutator/api/logging'; + +import { coreTokens } from '../di'; + +function mergedSchema(mainSchema: any, additionalSchemas: any[]): object { + const schema = { + ...mainSchema, + properties: { + ...mainSchema.properties, + }, + definitions: { + ...mainSchema.definitions, + }, + }; + + Object.assign(schema.properties, ...additionalSchemas.map((s) => s.properties)); + Object.assign(schema.definitions, ...additionalSchemas.map((s) => s.definitions)); + return schema; +} + +export function buildSchemaWithPluginContributions(schema: object, pluginResolver: PluginResolver, logger: Logger): object { + const additionalSchemas = pluginResolver.resolveValidationSchemaContributions(); + logger.debug('Contributing %s schemas from plugins to options validation.', additionalSchemas.length); + return mergedSchema(schema, additionalSchemas); +} +buildSchemaWithPluginContributions.inject = tokens(coreTokens.validationSchema, commonTokens.pluginResolver, commonTokens.logger); diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index eb8e6ce004..c99985f8b5 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -1,3 +1,4 @@ export { OptionsEditorApplier } from './OptionsEditorApplier'; export * from './readConfig'; export * from './OptionsValidator'; +export * from './buildSchemaWithPluginContributions'; diff --git a/packages/core/src/di/PluginLoader.ts b/packages/core/src/di/PluginLoader.ts index 72e27b2e12..264da952fc 100644 --- a/packages/core/src/di/PluginLoader.ts +++ b/packages/core/src/di/PluginLoader.ts @@ -15,8 +15,13 @@ interface PluginModule { strykerPlugins: Array>; } +interface SchemaValidationContribution { + strykerValidationSchema: object; +} + export class PluginLoader implements PluginResolver { private readonly pluginsByKind: Map>> = new Map(); + private readonly contributedValidationSchemas: object[] = []; public static inject = tokens(commonTokens.logger, coreTokens.pluginDescriptors); constructor(private readonly log: Logger, private readonly pluginDescriptors: readonly string[]) {} @@ -27,6 +32,10 @@ export class PluginLoader implements PluginResolver { }); } + public resolveValidationSchemaContributions(): object[] { + return this.contributedValidationSchemas; + } + public resolve(kind: T, name: string): Plugins[T] { const plugins = this.pluginsByKind.get(kind); if (plugins) { @@ -89,6 +98,9 @@ export class PluginLoader implements PluginResolver { if (this.isPluginModule(module)) { module.strykerPlugins.forEach((plugin) => this.loadPlugin(plugin)); } + if (this.hasValidationSchemaContribution(module)) { + this.contributedValidationSchemas.push(module.strykerValidationSchema); + } } catch (e) { if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(name) !== -1) { this.log.warn('Cannot find plugin "%s".\n Did you forget to install it ?\n' + ' npm install %s --save-dev', name, name); @@ -111,4 +123,9 @@ export class PluginLoader implements PluginResolver { const pluginModule = module as PluginModule; return pluginModule && pluginModule.strykerPlugins && Array.isArray(pluginModule.strykerPlugins); } + + private hasValidationSchemaContribution(module: unknown): module is SchemaValidationContribution { + const pluginModule = module as SchemaValidationContribution; + return pluginModule && pluginModule.strykerValidationSchema && typeof pluginModule.strykerValidationSchema === 'object'; + } } diff --git a/packages/core/src/di/buildMainInjector.ts b/packages/core/src/di/buildMainInjector.ts index 29dfd2efcd..0b547742da 100644 --- a/packages/core/src/di/buildMainInjector.ts +++ b/packages/core/src/di/buildMainInjector.ts @@ -5,14 +5,13 @@ import { TestFramework } from '@stryker-mutator/api/test_framework'; import { getLogger } from 'log4js'; import { rootInjector } from 'typed-inject'; -import { OptionsEditorApplier, readConfig } from '../config'; +import { OptionsEditorApplier, readConfig, buildSchemaWithPluginContributions, OptionsValidator, validateOptions } from '../config'; import ConfigReader from '../config/ConfigReader'; import BroadcastReporter from '../reporters/BroadcastReporter'; import { TemporaryDirectory } from '../utils/TemporaryDirectory'; import Timer from '../utils/Timer'; -import { OptionsValidator } from '../config/OptionsValidator'; -import { loggerFactory, mutatorDescriptorFactory, optionsFactory, pluginResolverFactory, testFrameworkFactory } from './factoryMethods'; +import { loggerFactory, mutatorDescriptorFactory, applyOptionsEditors, pluginResolverFactory, testFrameworkFactory } from './factoryMethods'; import { coreTokens, PluginCreator } from '.'; @@ -38,10 +37,13 @@ export function buildMainInjector(cliOptions: Partial): Injector .provideFactory(commonTokens.options, readConfig) .provideFactory(coreTokens.pluginDescriptors, pluginDescriptorsFactory) .provideFactory(commonTokens.pluginResolver, pluginResolverFactory) + .provideFactory(coreTokens.validationSchema, buildSchemaWithPluginContributions) + .provideClass(coreTokens.optionsValidator, OptionsValidator) + .provideFactory(commonTokens.options, validateOptions) .provideFactory(coreTokens.pluginCreatorConfigEditor, PluginCreator.createFactory(PluginKind.ConfigEditor)) .provideFactory(coreTokens.pluginCreatorOptionsEditor, PluginCreator.createFactory(PluginKind.OptionsEditor)) .provideClass(coreTokens.configOptionsApplier, OptionsEditorApplier) - .provideFactory(commonTokens.options, optionsFactory) + .provideFactory(commonTokens.options, applyOptionsEditors) .provideFactory(commonTokens.mutatorDescriptor, mutatorDescriptorFactory) .provideFactory(coreTokens.pluginCreatorReporter, PluginCreator.createFactory(PluginKind.Reporter)) .provideFactory(coreTokens.pluginCreatorTestFramework, PluginCreator.createFactory(PluginKind.TestFramework)) diff --git a/packages/core/src/di/factoryMethods.ts b/packages/core/src/di/factoryMethods.ts index 8df8529a08..eb41029a65 100644 --- a/packages/core/src/di/factoryMethods.ts +++ b/packages/core/src/di/factoryMethods.ts @@ -1,10 +1,10 @@ import { MutatorDescriptor, StrykerOptions } from '@stryker-mutator/api/core'; import { Logger, LoggerFactoryMethod } from '@stryker-mutator/api/logging'; import { commonTokens, Injector, OptionsContext, PluginKind, PluginResolver, tokens } from '@stryker-mutator/api/plugin'; +import { deepFreeze } from '@stryker-mutator/util'; import { OptionsEditorApplier } from '../config'; import TestFrameworkOrchestrator from '../TestFrameworkOrchestrator'; -import { freezeRecursively } from '../utils/objectUtils'; import { coreTokens, PluginCreator, PluginLoader } from '.'; @@ -29,11 +29,11 @@ export function loggerFactory(getLogger: LoggerFactoryMethod, target: Function | } loggerFactory.inject = tokens(commonTokens.getLogger, commonTokens.target); -export function optionsFactory(options: StrykerOptions, optionsEditorApplier: OptionsEditorApplier): StrykerOptions { +export function applyOptionsEditors(options: StrykerOptions, optionsEditorApplier: OptionsEditorApplier): StrykerOptions { optionsEditorApplier.edit(options); - return freezeRecursively(options); + return deepFreeze(options) as StrykerOptions; } -optionsFactory.inject = tokens(commonTokens.options, coreTokens.configOptionsApplier); +applyOptionsEditors.inject = tokens(commonTokens.options, coreTokens.configOptionsApplier); export function mutatorDescriptorFactory(options: StrykerOptions): MutatorDescriptor { const defaults: MutatorDescriptor = { diff --git a/packages/core/src/di/index.ts b/packages/core/src/di/index.ts index 756f6e9a6d..08690f2326 100644 --- a/packages/core/src/di/index.ts +++ b/packages/core/src/di/index.ts @@ -4,4 +4,5 @@ export * from './buildMainInjector'; export * from './buildChildProcessInjector'; export * from './PluginCreator'; export * from './PluginLoader'; +export * from './factoryMethods'; export { coreTokens }; diff --git a/packages/core/src/reporters/ClearTextReporter.ts b/packages/core/src/reporters/ClearTextReporter.ts index a5d8a53dd4..c821bdd0ea 100644 --- a/packages/core/src/reporters/ClearTextReporter.ts +++ b/packages/core/src/reporters/ClearTextReporter.ts @@ -98,17 +98,12 @@ export default class ClearTextReporter implements Reporter { } private logExecutedTests(result: MutantResult, logImplementation: (input: string) => void) { - const clearTextReporterConfig = this.options.clearTextReporter || {}; - - if (!clearTextReporterConfig.logTests) { + if (!this.options.clearTextReporter.logTests) { return; } if (result.testsRan && result.testsRan.length > 0) { - let testsToLog = 3; - if (typeof clearTextReporterConfig.maxTestsToLog === 'number') { - testsToLog = clearTextReporterConfig.maxTestsToLog; - } + let testsToLog = this.options.clearTextReporter.maxTestsToLog; if (testsToLog > 0) { logImplementation('Tests ran: '); diff --git a/packages/core/src/reporters/EventRecorderReporter.ts b/packages/core/src/reporters/EventRecorderReporter.ts index 4b0248f809..7199bb465f 100644 --- a/packages/core/src/reporters/EventRecorderReporter.ts +++ b/packages/core/src/reporters/EventRecorderReporter.ts @@ -10,37 +10,19 @@ import { cleanFolder } from '../utils/fileUtils'; import StrictReporter from './StrictReporter'; -const DEFAULT_BASE_FOLDER = 'reports/mutation/events'; - export default class EventRecorderReporter implements StrictReporter { public static readonly inject = tokens(commonTokens.logger, commonTokens.options); private readonly allWork: Array> = []; private readonly createBaseFolderTask: Promise; - private _baseFolder: string; private index = 0; constructor(private readonly log: Logger, private readonly options: StrykerOptions) { - this.createBaseFolderTask = cleanFolder(this.baseFolder); - } - - private get baseFolder() { - if (!this._baseFolder) { - if (this.options.eventReporter && this.options.eventReporter.baseDir) { - this._baseFolder = this.options.eventReporter.baseDir; - this.log.debug(`Using configured output folder ${this._baseFolder}`); - } else { - this.log.debug( - `No base folder configuration found (using configuration: eventReporter: { baseDir: 'output/folder' }), using default ${DEFAULT_BASE_FOLDER}` - ); - this._baseFolder = DEFAULT_BASE_FOLDER; - } - } - return this._baseFolder; + this.createBaseFolderTask = cleanFolder(this.options.eventReporter.baseDir); } private writeToFile(methodName: keyof Reporter, data: any) { - const filename = path.join(this.baseFolder, `${this.format(this.index++)}-${methodName}.json`); + const filename = path.join(this.options.eventReporter.baseDir, `${this.format(this.index++)}-${methodName}.json`); this.log.debug(`Writing event ${methodName} to file ${filename}`); return fs.writeFile(filename, JSON.stringify(data), { encoding: 'utf8' }); } diff --git a/packages/core/src/utils/objectUtils.ts b/packages/core/src/utils/objectUtils.ts index cc40f84a09..05415a9426 100644 --- a/packages/core/src/utils/objectUtils.ts +++ b/packages/core/src/utils/objectUtils.ts @@ -3,17 +3,6 @@ import { StrykerError } from '@stryker-mutator/util'; export { serialize, deserialize } from 'surrial'; -export function freezeRecursively(target: T): T { - Object.freeze(target); - Object.keys(target).forEach((key) => { - const value = target[key]; - if (typeof value === 'object' && value !== null) { - freezeRecursively(value); - } - }); - return target; -} - export function wrapInClosure(codeFragment: string) { return ` (function (window) { diff --git a/packages/core/test/helpers/producers.ts b/packages/core/test/helpers/producers.ts index 9456f85def..fed9a5dda0 100644 --- a/packages/core/test/helpers/producers.ts +++ b/packages/core/test/helpers/producers.ts @@ -1,4 +1,4 @@ -import { File } from '@stryker-mutator/api/core'; +import { File, ClearTextReporterOptions } from '@stryker-mutator/api/core'; import { factory } from '@stryker-mutator/test-helpers'; import { FileCoverageData } from 'istanbul-lib-coverage'; import { Logger } from 'log4js'; @@ -29,6 +29,12 @@ function factoryMethod(defaultsFactory: () => T) { return (overrides?: Partial) => Object.assign({}, defaultsFactory(), overrides); } +export const createClearTextReporterOptions = factoryMethod(() => ({ + allowColor: true, + logTests: true, + maxTestsToLog: 3, +})); + export const logger = (): Mock => { return { _log: sinon.stub(), diff --git a/packages/core/test/unit/config/OptionsValidator.spec.ts b/packages/core/test/unit/config/OptionsValidator.spec.ts index e2796c6a3b..cff15bbf79 100644 --- a/packages/core/test/unit/config/OptionsValidator.spec.ts +++ b/packages/core/test/unit/config/OptionsValidator.spec.ts @@ -213,8 +213,9 @@ describe(OptionsValidator.name, () => { } function breakConfig(key: keyof StrykerOptions, value: any): void { - if (typeof testInjector.options[key] === 'object' && !Array.isArray(testInjector.options[key])) { - testInjector.options[key] = { ...testInjector.options[key], ...value }; + const original = testInjector.options[key]; + if (typeof original === 'object' && !Array.isArray(original)) { + testInjector.options[key] = { ...original, ...value }; } else { testInjector.options[key] = value; } diff --git a/packages/core/test/unit/config/buildSchemaWithPluginContributions.spec.ts b/packages/core/test/unit/config/buildSchemaWithPluginContributions.spec.ts new file mode 100644 index 0000000000..37b1520a0a --- /dev/null +++ b/packages/core/test/unit/config/buildSchemaWithPluginContributions.spec.ts @@ -0,0 +1,39 @@ +import type { JSONSchema7 } from 'json-schema'; +import { expect } from 'chai'; +import { deepFreeze } from '@stryker-mutator/util'; +import { factory, testInjector } from '@stryker-mutator/test-helpers'; +import { PluginResolver } from '@stryker-mutator/api/plugin'; + +import { buildSchemaWithPluginContributions } from '../../../src/config'; + +describe(buildSchemaWithPluginContributions.name, () => { + let pluginResolverStub: sinon.SinonStubbedInstance; + let pluginContributions: object[]; + + beforeEach(() => { + pluginResolverStub = factory.pluginResolver(); + pluginContributions = []; + pluginResolverStub.resolveValidationSchemaContributions.returns(pluginContributions); + }); + + it('should merge `properties`', () => { + const input: JSONSchema7 = deepFreeze({ properties: { foo: { type: 'string' } } }); + const additionalSchema: JSONSchema7 = deepFreeze({ properties: { bar: { type: 'string' } } }); + const additionalSchema2: JSONSchema7 = deepFreeze({ properties: { baz: { type: 'number' } } }); + pluginContributions.push(additionalSchema, additionalSchema2); + const actual = buildSchemaWithPluginContributions(input, pluginResolverStub, testInjector.logger); + expect(actual).deep.eq({ definitions: {}, properties: { ...input.properties, ...additionalSchema.properties, ...additionalSchema2.properties } }); + }); + + it('should merge `definitions`', () => { + const input: JSONSchema7 = deepFreeze({ definitions: { foo: { type: 'string' } } }); + const additionalSchema: JSONSchema7 = deepFreeze({ definitions: { bar: { type: 'string' } } }); + const additionalSchema2: JSONSchema7 = deepFreeze({ definitions: { baz: { type: 'number' } } }); + pluginContributions.push(additionalSchema, additionalSchema2); + const actual = buildSchemaWithPluginContributions(input, pluginResolverStub, testInjector.logger); + expect(actual).deep.eq({ + properties: {}, + definitions: { ...input.definitions, ...additionalSchema.definitions, ...additionalSchema2.definitions }, + }); + }); +}); diff --git a/packages/core/test/unit/di/buildMainInjector.spec.ts b/packages/core/test/unit/di/buildMainInjector.spec.ts index 47b02e512a..9bb18a7aaf 100644 --- a/packages/core/test/unit/di/buildMainInjector.spec.ts +++ b/packages/core/test/unit/di/buildMainInjector.spec.ts @@ -21,23 +21,30 @@ describe(buildMainInjector.name, () => { let testFrameworkMock: TestFramework; let configReaderMock: sinon.SinonStubbedInstance; let pluginCreatorMock: sinon.SinonStubbedInstance>; + let buildSchemaWithPluginContributionsStub: sinon.SinonStub; let optionsEditorApplierMock: sinon.SinonStubbedInstance; let broadcastReporterMock: sinon.SinonStubbedInstance; + let optionsValidatorStub: sinon.SinonStubbedInstance; let expectedConfig: StrykerOptions; beforeEach(() => { configReaderMock = sinon.createStubInstance(ConfigReader); pluginCreatorMock = sinon.createStubInstance(PluginCreator); + pluginCreatorMock = sinon.createStubInstance(PluginCreator); optionsEditorApplierMock = sinon.createStubInstance(configModule.OptionsEditorApplier); testFrameworkMock = factory.testFramework(); testFrameworkOrchestratorMock = sinon.createStubInstance(TestFrameworkOrchestrator); testFrameworkOrchestratorMock.determineTestFramework.returns(testFrameworkMock); pluginLoaderMock = sinon.createStubInstance(di.PluginLoader); + optionsValidatorStub = sinon.createStubInstance(configModule.OptionsValidator); + buildSchemaWithPluginContributionsStub = sinon.stub(); expectedConfig = factory.strykerOptions(); broadcastReporterMock = factory.reporter('broadcast'); configReaderMock.readConfig.returns(expectedConfig); stubInjectable(PluginCreator, 'createFactory').returns(() => pluginCreatorMock); stubInjectable(configModule, 'OptionsEditorApplier').returns(optionsEditorApplierMock); + stubInjectable(configModule, 'buildSchemaWithPluginContributions').returns(buildSchemaWithPluginContributionsStub); + stubInjectable(configModule, 'OptionsValidator').returns(optionsValidatorStub); stubInjectable(di, 'PluginLoader').returns(pluginLoaderMock); stubInjectable(configReaderModule, 'default').returns(configReaderMock); stubInjectable(broadcastReporterModule, 'default').returns(broadcastReporterMock); @@ -85,6 +92,11 @@ describe(buildMainInjector.name, () => { buildMainInjector(expectedCliOptions).resolve(commonTokens.options); expect(configReaderModule.default).calledWith(expectedCliOptions); }); + + it('should validate the options', () => { + buildMainInjector({}).resolve(commonTokens.options); + expect(optionsValidatorStub.validate).calledWith(expectedConfig); + }); }); it('should supply mutatorDescriptor', () => { diff --git a/packages/core/test/unit/initializer/Presets.spec.ts b/packages/core/test/unit/initializer/Presets.spec.ts index 69efe3dcc5..5a8088d6cc 100644 --- a/packages/core/test/unit/initializer/Presets.spec.ts +++ b/packages/core/test/unit/initializer/Presets.spec.ts @@ -30,7 +30,7 @@ describe('Presets', () => { it('should use the angular-cli', async () => { const config = await angularPreset.createConfig(); - expect(config.config.karma.projectType).to.eq('angular-cli'); + expect((config.config.karma as any).projectType).to.eq('angular-cli'); }); }); diff --git a/packages/core/test/unit/reporters/ClearTextReporter.spec.ts b/packages/core/test/unit/reporters/ClearTextReporter.spec.ts index cecdef26ff..4c52ff8721 100644 --- a/packages/core/test/unit/reporters/ClearTextReporter.spec.ts +++ b/packages/core/test/unit/reporters/ClearTextReporter.spec.ts @@ -9,6 +9,7 @@ import * as sinon from 'sinon'; import chalk = require('chalk'); import ClearTextReporter from '../../../src/reporters/ClearTextReporter'; +import { createClearTextReporterOptions } from '../../helpers/producers'; const colorizeFileAndPosition = (sourceFilePath: string, line: number, column: number) => { return [chalk.cyan(sourceFilePath), chalk.yellow(`${line}`), chalk.yellow(`${column}`)].join(':'); @@ -79,7 +80,7 @@ describe(ClearTextReporter.name, () => { describe('when coverageAnalysis is "all"', () => { beforeEach(() => { testInjector.options.coverageAnalysis = 'all'; - testInjector.options.clearTextReporter = { logTests: true }; + testInjector.options.clearTextReporter = createClearTextReporterOptions({ logTests: true }); }); describe('onAllMutantsTested() all mutants except error', () => { @@ -149,6 +150,7 @@ describe(ClearTextReporter.name, () => { it('should not log individual ran tests when logTests is not true', () => { testInjector.options.coverageAnalysis = 'perTest'; + testInjector.options.clearTextReporter.logTests = false; sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage)); @@ -161,7 +163,7 @@ describe(ClearTextReporter.name, () => { it('should log individual ran tests when logTests is true', () => { testInjector.options.coverageAnalysis = 'perTest'; - testInjector.options.clearTextReporter = { logTests: true }; + testInjector.options.clearTextReporter = createClearTextReporterOptions({ logTests: true }); sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage)); @@ -175,7 +177,7 @@ describe(ClearTextReporter.name, () => { describe('with fewer tests that may be logged', () => { it('should log fewer tests', () => { testInjector.options.coverageAnalysis = 'perTest'; - testInjector.options.clearTextReporter = { logTests: true, maxTestsToLog: 1 }; + testInjector.options.clearTextReporter = createClearTextReporterOptions({ logTests: true, maxTestsToLog: 1 }); sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage)); @@ -189,7 +191,7 @@ describe(ClearTextReporter.name, () => { describe('with more tests that may be logged', () => { it('should log all tests', () => { testInjector.options.coverageAnalysis = 'perTest'; - testInjector.options.clearTextReporter = { logTests: true, maxTestsToLog: 10 }; + testInjector.options.clearTextReporter = createClearTextReporterOptions({ logTests: true, maxTestsToLog: 10 }); sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage)); @@ -204,7 +206,7 @@ describe(ClearTextReporter.name, () => { describe('with the default amount of tests that may be logged', () => { it('should log all tests', () => { testInjector.options.coverageAnalysis = 'perTest'; - testInjector.options.clearTextReporter = { logTests: true, maxTestsToLog: 3 }; + testInjector.options.clearTextReporter = createClearTextReporterOptions({ logTests: true, maxTestsToLog: 3 }); sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage)); @@ -219,7 +221,7 @@ describe(ClearTextReporter.name, () => { describe('with no tests that may be logged', () => { it('should not log a test', () => { testInjector.options.coverageAnalysis = 'perTest'; - testInjector.options.clearTextReporter = { logTests: true, maxTestsToLog: 0 }; + testInjector.options.clearTextReporter = createClearTextReporterOptions({ logTests: true, maxTestsToLog: 0 }); sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage)); diff --git a/packages/core/test/unit/reporters/EventRecorderReporter.spec.ts b/packages/core/test/unit/reporters/EventRecorderReporter.spec.ts index bf8a8138aa..48ccb5adf3 100644 --- a/packages/core/test/unit/reporters/EventRecorderReporter.spec.ts +++ b/packages/core/test/unit/reporters/EventRecorderReporter.spec.ts @@ -10,7 +10,7 @@ import EventRecorderReporter from '../../../src/reporters/EventRecorderReporter' import StrictReporter from '../../../src/reporters/StrictReporter'; import * as fileUtils from '../../../src/utils/fileUtils'; -describe('EventRecorderReporter', () => { +describe(EventRecorderReporter.name, () => { let sut: StrictReporter; let cleanFolderStub: sinon.SinonStub; let writeFileStub: sinon.SinonStub; @@ -27,12 +27,6 @@ describe('EventRecorderReporter', () => { sut = testInjector.injector.injectClass(EventRecorderReporter); }); - it('should log about the default baseFolder', () => { - expect(testInjector.logger.debug).to.have.been.calledWith( - "No base folder configuration found (using configuration: eventReporter: { baseDir: 'output/folder' }), using default reports/mutation/events" - ); - }); - it('should clean the baseFolder', () => { expect(fileUtils.cleanFolder).to.have.been.calledWith('reports/mutation/events'); }); diff --git a/packages/html-reporter/.vscode/launch.json b/packages/html-reporter/.vscode/launch.json index 4bcde6fd7b..3b0900ee7c 100644 --- a/packages/html-reporter/.vscode/launch.json +++ b/packages/html-reporter/.vscode/launch.json @@ -54,4 +54,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/packages/jasmine-runner/.npmignore b/packages/jasmine-runner/.npmignore index d5f6bc8cab..6d23c7107f 100644 --- a/packages/jasmine-runner/.npmignore +++ b/packages/jasmine-runner/.npmignore @@ -4,7 +4,8 @@ !src/** src/**/*.map src/**/*.ts -!src/**/*.d.ts +!{src,src-generated}/**/*.d.ts +!schema/*.json !readme.md !LICENSE !CHANGELOG.md \ No newline at end of file diff --git a/packages/jasmine-runner/src/index.ts b/packages/jasmine-runner/src/index.ts index b4f2c91d8e..bb4e59ad56 100644 --- a/packages/jasmine-runner/src/index.ts +++ b/packages/jasmine-runner/src/index.ts @@ -3,3 +3,5 @@ import { declareClassPlugin, PluginKind } from '@stryker-mutator/api/plugin'; import JasmineTestRunner from './JasmineTestRunner'; export const strykerPlugins = [declareClassPlugin(PluginKind.TestRunner, 'jasmine', JasmineTestRunner)]; + +export * as strykerValidationSchema from '../schema/jasmine-runner-options.json'; diff --git a/packages/jasmine-runner/tsconfig.src.json b/packages/jasmine-runner/tsconfig.src.json index 6fee93e409..4ac04cbc87 100644 --- a/packages/jasmine-runner/tsconfig.src.json +++ b/packages/jasmine-runner/tsconfig.src.json @@ -10,7 +10,8 @@ "include": [ "src", "src-generated", - "typings" + "typings", + "schema/*.json" ], "references": [ { diff --git a/packages/jest-runner/schema/jest-runner-options.json b/packages/jest-runner/schema/jest-runner-options.json index 030c365e6c..897e8b3152 100644 --- a/packages/jest-runner/schema/jest-runner-options.json +++ b/packages/jest-runner/schema/jest-runner-options.json @@ -16,8 +16,7 @@ }, "config": { "description": "A custom Jest configuration object. You could also use `require` to load it here. Please leave it empty if you want jest configuration to be loaded from package.json or a standard jest configuration file.", - "type": "object", - "default": {} + "type": "object" }, "enableFindRelatedTests": { "description": "Whether to run jest with the `--findRelatedTests` flag. When `true`, Jest will only run tests related to the mutated file per test. (See [_--findRelatedTests_](https://jestjs.io/docs/en/cli.html#findrelatedtests-spaceseparatedlistofsourcefiles)", diff --git a/packages/jest-runner/src/JestOptionsEditor.ts b/packages/jest-runner/src/JestOptionsEditor.ts index 611300172d..a6596d29f0 100644 --- a/packages/jest-runner/src/JestOptionsEditor.ts +++ b/packages/jest-runner/src/JestOptionsEditor.ts @@ -1,32 +1,27 @@ import { Logger } from '@stryker-mutator/api/logging'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; -import { OptionsEditor, StrykerOptions } from '@stryker-mutator/api/core'; +import { OptionsEditor } from '@stryker-mutator/api/core'; import CustomJestConfigLoader from './configLoaders/CustomJestConfigLoader'; import JestConfigLoader from './configLoaders/JestConfigLoader'; import ReactScriptsJestConfigLoader from './configLoaders/ReactScriptsJestConfigLoader'; import ReactScriptsTSJestConfigLoader from './configLoaders/ReactScriptsTSJestConfigLoader'; import JEST_OVERRIDE_OPTIONS from './jestOverrideOptions'; +import { JestRunnerOptionsWithStrykerOptions } from './JestRunnerOptionsWithStrykerOptions'; const DEFAULT_PROJECT_NAME = 'custom'; -export default class JestOptionsEditor implements OptionsEditor { +export default class JestOptionsEditor implements OptionsEditor { public static inject = tokens(commonTokens.logger); constructor(private readonly log: Logger) {} - public edit(options: StrykerOptions): void { - // If there is no Jest property on the Stryker config create it - options.jest = options.jest || {}; - - // When no projectType is set, set it to the default - options.jest.projectType = options.jest.projectType || options.jest.project || DEFAULT_PROJECT_NAME; - + public edit(options: JestRunnerOptionsWithStrykerOptions): void { // When no config property is set, load the configuration with the project type - options.jest.config = options.jest.config || this.getConfigLoader(options.jest.projectType).loadConfig(); + options.jest.config = options.jest.config || (this.getConfigLoader(options.jest.projectType).loadConfig() as any); // Override some of the config properties to optimise Jest for Stryker - options.jest.config = this.overrideProperties(options.jest.config); + options.jest.config = this.overrideProperties((options.jest.config as unknown) as Jest.Configuration); } private getConfigLoader(projectType: string): JestConfigLoader { @@ -53,6 +48,6 @@ export default class JestOptionsEditor implements OptionsEditor { } private overrideProperties(config: Jest.Configuration) { - return Object.assign(config, JEST_OVERRIDE_OPTIONS); + return { ...config, ...JEST_OVERRIDE_OPTIONS }; } } diff --git a/packages/jest-runner/src/JestTestRunner.ts b/packages/jest-runner/src/JestTestRunner.ts index dc3c7a1cea..d7a236d633 100644 --- a/packages/jest-runner/src/JestTestRunner.ts +++ b/packages/jest-runner/src/JestTestRunner.ts @@ -31,11 +31,12 @@ export default class JestTestRunner implements TestRunner { private readonly processEnvRef: NodeJS.ProcessEnv, private readonly jestTestAdapter: JestTestAdapter ) { + const jestOptions = options as JestRunnerOptionsWithStrykerOptions; // Get jest configuration from stryker options and assign it to jestConfig - this.jestConfig = (options as JestRunnerOptionsWithStrykerOptions).jest.config as Jest.Configuration; + this.jestConfig = (jestOptions.jest.config as unknown) as Jest.Configuration; // Get enableFindRelatedTests from stryker jest options or default to true - this.enableFindRelatedTests = options.jest.enableFindRelatedTests; + this.enableFindRelatedTests = jestOptions.jest.enableFindRelatedTests; if (this.enableFindRelatedTests === undefined) { this.enableFindRelatedTests = true; } @@ -51,7 +52,7 @@ export default class JestTestRunner implements TestRunner { // basePath will be used in future releases of Stryker as a way to define the project root // Default to process.cwd when basePath is not set for now, should be removed when issue is solved // https://github.com/stryker-mutator/stryker/issues/650 - this.jestConfig.rootDir = options.basePath || process.cwd(); + this.jestConfig.rootDir = (options.basePath as string) || process.cwd(); this.log.debug(`Project root is ${this.jestConfig.rootDir}`); } diff --git a/packages/jest-runner/src/index.ts b/packages/jest-runner/src/index.ts index 7954bae8d1..cdb05a48bb 100644 --- a/packages/jest-runner/src/index.ts +++ b/packages/jest-runner/src/index.ts @@ -9,3 +9,4 @@ export const strykerPlugins = [ declareClassPlugin(PluginKind.OptionsEditor, 'jest', JestOptionsEditor), declareFactoryPlugin(PluginKind.TestRunner, 'jest', jestTestRunnerFactory), ]; +export * as strykerValidationSchema from '../schema/jest-runner-options.json'; diff --git a/packages/jest-runner/test/helpers/testResultProducer.ts b/packages/jest-runner/test/helpers/producers.ts similarity index 95% rename from packages/jest-runner/test/helpers/testResultProducer.ts rename to packages/jest-runner/test/helpers/producers.ts index a31c81cfcd..826cf3512d 100644 --- a/packages/jest-runner/test/helpers/testResultProducer.ts +++ b/packages/jest-runner/test/helpers/producers.ts @@ -1,3 +1,13 @@ +import { JestOptions } from '../../src-generated/jest-runner-options'; + +export const createJestOptions = (overrides?: Partial): JestOptions => { + return { + enableFindRelatedTests: true, + projectType: 'custom', + ...overrides, + }; +}; + export const createFailResult = () => ({ numFailedTests: 2, numFailedTestSuites: 1, diff --git a/packages/jest-runner/test/integration/JestOptionsEditor.it.spec.ts b/packages/jest-runner/test/integration/JestOptionsEditor.it.spec.ts index d01057403a..6595dd4184 100644 --- a/packages/jest-runner/test/integration/JestOptionsEditor.it.spec.ts +++ b/packages/jest-runner/test/integration/JestOptionsEditor.it.spec.ts @@ -3,16 +3,16 @@ import * as path from 'path'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { expect } from 'chai'; import * as sinon from 'sinon'; -import { StrykerOptions } from '@stryker-mutator/api/core'; import JestOptionsEditor from '../../src/JestOptionsEditor'; +import { JestRunnerOptionsWithStrykerOptions } from '../../src/JestRunnerOptionsWithStrykerOptions'; describe('Integration test for Jest OptionsEditor', () => { let jestConfigEditor: JestOptionsEditor; let getProjectRootStub: sinon.SinonStub; const projectRoot: string = process.cwd(); - let options: StrykerOptions; + let options: JestRunnerOptionsWithStrykerOptions; beforeEach(() => { getProjectRootStub = sinon.stub(process, 'cwd'); @@ -20,11 +20,15 @@ describe('Integration test for Jest OptionsEditor', () => { jestConfigEditor = testInjector.injector.injectClass(JestOptionsEditor); - options = factory.strykerOptions(); + options = factory.strykerOptions() as JestRunnerOptionsWithStrykerOptions; + options.jest = { + enableFindRelatedTests: true, + projectType: 'custom', + }; }); it('should create a Jest configuration for a create-react-app project', () => { - options.jest = { projectType: 'create-react-app' }; + options.jest.projectType = 'create-react-app'; jestConfigEditor.edit(options); @@ -57,7 +61,7 @@ describe('Integration test for Jest OptionsEditor', () => { }); it('should create a Jest configuration for a React project', () => { - options.jest = { projectType: 'react' }; + options.jest.projectType = 'react'; jestConfigEditor.edit(options); @@ -90,7 +94,7 @@ describe('Integration test for Jest OptionsEditor', () => { }); it('should log a deprecation warning when projectType is "react"', () => { - options.jest = { projectType: 'react' }; + options.jest.projectType = 'react'; jestConfigEditor.edit(options); @@ -100,7 +104,7 @@ describe('Integration test for Jest OptionsEditor', () => { }); it('should create a Jest configuration for a create-react-app + TypeScript project', () => { - options.jest = { projectType: 'create-react-app-ts' }; + options.jest.projectType = 'create-react-app-ts'; jestConfigEditor.edit(options); @@ -139,7 +143,7 @@ describe('Integration test for Jest OptionsEditor', () => { }); it('should create a Jest configuration for a React + TypeScript project', () => { - options.jest = { projectType: 'react-ts' }; + options.jest.projectType = 'react-ts'; jestConfigEditor.edit(options); @@ -178,7 +182,7 @@ describe('Integration test for Jest OptionsEditor', () => { }); it('should log a deprecation warning when projectType is "react-ts"', () => { - options.jest = { projectType: 'react-ts' }; + options.jest.projectType = 'react-ts'; jestConfigEditor.edit(options); @@ -240,14 +244,6 @@ describe('Integration test for Jest OptionsEditor', () => { verbose: false, }); }); - - it('should return with an error when an invalid projectType is specified', () => { - const projectType = 'invalidProject'; - options.jest = { projectType }; - - expect(() => jestConfigEditor.edit(options)).to.throw(Error, `No configLoader available for ${projectType}`); - }); - function assertJestConfig(expected: any, actual: any) { Object.keys(expected).forEach((key) => { if (Array.isArray(expected[key])) { diff --git a/packages/jest-runner/test/integration/StrykerJestRunner.it.spec.ts b/packages/jest-runner/test/integration/StrykerJestRunner.it.spec.ts index 03bcb8a104..6b2d17914b 100644 --- a/packages/jest-runner/test/integration/StrykerJestRunner.it.spec.ts +++ b/packages/jest-runner/test/integration/StrykerJestRunner.it.spec.ts @@ -8,6 +8,9 @@ import { factory, testInjector } from '@stryker-mutator/test-helpers'; import JestOptionsEditor from '../../src/JestOptionsEditor'; import { jestTestRunnerFactory } from '../../src/JestTestRunner'; +import { JestRunnerOptionsWithStrykerOptions } from '../../src/JestRunnerOptionsWithStrykerOptions'; +import { JestOptions } from '../../src-generated/jest-runner-options'; +import { createJestOptions } from '../helpers/producers'; const paths = require('react-scripts-ts/config/paths'); // It's a bit hacky, but we need to tell create-react-app-ts to pick a different tsconfig.test.json @@ -39,12 +42,11 @@ describe('Integration test for Strykers Jest runner', () => { processCwdStub = sinon.stub(process, 'cwd'); }); - function createSut(jestConfig?: any) { + function createSut(overrides?: Partial) { const jestOptionsEditor = testInjector.injector.injectClass(JestOptionsEditor); - const options = factory.strykerOptions(); - if (jestConfig) { - options.jest = jestConfig; - } + const options: JestRunnerOptionsWithStrykerOptions = factory.strykerWithPluginOptions({ + jest: createJestOptions(overrides), + }); jestOptionsEditor.edit(options); return testInjector.injector.provideValue(commonTokens.options, options).injectFunction(jestTestRunnerFactory); } diff --git a/packages/jest-runner/test/unit/JestOptionsEditor.spec.ts b/packages/jest-runner/test/unit/JestOptionsEditor.spec.ts index 17ad2c5d9b..200267fcf8 100644 --- a/packages/jest-runner/test/unit/JestOptionsEditor.spec.ts +++ b/packages/jest-runner/test/unit/JestOptionsEditor.spec.ts @@ -1,19 +1,19 @@ import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { assert, expect } from 'chai'; import * as sinon from 'sinon'; -import { StrykerOptions } from '@stryker-mutator/api/core'; import CustomJestConfigLoader, * as defaultJestConfigLoader from '../../src/configLoaders/CustomJestConfigLoader'; import ReactScriptsJestConfigLoader, * as reactScriptsJestConfigLoader from '../../src/configLoaders/ReactScriptsJestConfigLoader'; import ReactScriptsTSJestConfigLoader, * as reactScriptsTSJestConfigLoader from '../../src/configLoaders/ReactScriptsTSJestConfigLoader'; import JestOptionsEditor from '../../src/JestOptionsEditor'; +import { JestRunnerOptionsWithStrykerOptions } from '../../src/JestRunnerOptionsWithStrykerOptions'; describe(JestOptionsEditor.name, () => { let sut: JestOptionsEditor; let customConfigLoaderStub: ConfigLoaderStub; let reactScriptsJestConfigLoaderStub: ConfigLoaderStub; let reactScriptsTSJestConfigLoaderStub: ConfigLoaderStub; - let options: StrykerOptions; + let options: JestRunnerOptionsWithStrykerOptions; beforeEach(() => { customConfigLoaderStub = sinon.createStubInstance(CustomJestConfigLoader); @@ -35,7 +35,12 @@ describe(JestOptionsEditor.name, () => { reactScriptsTSJestConfigLoaderStub.loadConfig.returns(defaultOptions); sut = testInjector.injector.injectClass(JestOptionsEditor); - options = factory.strykerOptions(); + options = factory.strykerWithPluginOptions({ + jest: { + enableFindRelatedTests: true, + projectType: 'custom', + }, + }); }); it('should call the defaultConfigLoader loadConfig method when no projectType is defined', () => { @@ -46,7 +51,7 @@ describe(JestOptionsEditor.name, () => { }); it("should call the ReactScriptsJestConfigLoader loadConfig method when 'react' is defined as projectType", () => { - options.jest = { projectType: 'react' }; + options.jest = { projectType: 'react', enableFindRelatedTests: true }; sut.edit(options); @@ -54,7 +59,7 @@ describe(JestOptionsEditor.name, () => { }); it("should call the ReactScriptsTSJestConfigLoader loadConfig method when 'react-ts' is defined as projectType", () => { - options.jest = { projectType: 'react-ts' }; + options.jest = { projectType: 'react-ts', enableFindRelatedTests: true }; sut.edit(options); @@ -72,13 +77,6 @@ describe(JestOptionsEditor.name, () => { verbose: false, }); }); - - it('should throw an error when an invalid projectType is defined', () => { - const projectType = 'invalidProject'; - options.jest = { projectType }; - - expect(() => sut.edit(options)).to.throw(Error, `No configLoader available for ${projectType}`); - }); }); interface ConfigLoaderStub { diff --git a/packages/jest-runner/test/unit/JestTestRunner.spec.ts b/packages/jest-runner/test/unit/JestTestRunner.spec.ts index add0be54f2..bb689bed55 100644 --- a/packages/jest-runner/test/unit/JestTestRunner.spec.ts +++ b/packages/jest-runner/test/unit/JestTestRunner.spec.ts @@ -5,7 +5,7 @@ import sinon from 'sinon'; import { JestTestAdapter } from '../../src/jestTestAdapters'; import JestTestRunner, { JEST_TEST_ADAPTER_TOKEN, PROCESS_ENV_TOKEN } from '../../src/JestTestRunner'; -import * as fakeResults from '../helpers/testResultProducer'; +import * as producers from '../helpers/producers'; describe('JestTestRunner', () => { const basePath = '/path/to/project/root'; @@ -43,7 +43,7 @@ describe('JestTestRunner', () => { }); it('should call the jestTestRunner run method and return a correct runResult', async () => { - jestTestAdapterMock.run.resolves({ results: fakeResults.createSuccessResult() }); + jestTestAdapterMock.run.resolves({ results: producers.createSuccessResult() }); const result = await jestTestRunner.run(runOptions); @@ -62,7 +62,7 @@ describe('JestTestRunner', () => { }); it('should call the jestTestRunner run method and return a skipped runResult', async () => { - jestTestAdapterMock.run.resolves({ results: fakeResults.createPendingResult() }); + jestTestAdapterMock.run.resolves({ results: producers.createPendingResult() }); const result = await jestTestRunner.run(runOptions); @@ -81,7 +81,7 @@ describe('JestTestRunner', () => { }); it('should call the jestTestRunner run method and return a todo runResult', async () => { - jestTestAdapterMock.run.resolves({ results: fakeResults.createTodoResult() }); + jestTestAdapterMock.run.resolves({ results: producers.createTodoResult() }); const result = await jestTestRunner.run(runOptions); @@ -106,7 +106,7 @@ describe('JestTestRunner', () => { }); it('should call the jestTestRunner run method and return a negative runResult', async () => { - jestTestAdapterMock.run.resolves({ results: fakeResults.createFailResult() }); + jestTestAdapterMock.run.resolves({ results: producers.createFailResult() }); const result = await jestTestRunner.run(runOptions); diff --git a/packages/jest-runner/tsconfig.src.json b/packages/jest-runner/tsconfig.src.json index 951d32f746..8273a9ffad 100644 --- a/packages/jest-runner/tsconfig.src.json +++ b/packages/jest-runner/tsconfig.src.json @@ -7,7 +7,8 @@ "include": [ "src", "src-generated", - "typings" + "typings", + "schema/*.json" ], "references": [ { diff --git a/packages/karma-runner/src/KarmaTestRunner.ts b/packages/karma-runner/src/KarmaTestRunner.ts index f2577215fe..56dbfe240f 100644 --- a/packages/karma-runner/src/KarmaTestRunner.ts +++ b/packages/karma-runner/src/KarmaTestRunner.ts @@ -97,7 +97,6 @@ export default class KarmaTestRunner implements TestRunner { StrykerReporter.instance.on('server_start', (port: number) => { this.port = port; }); - StrykerReporter.instance.on('server_start', () => {}); } private listenToCoverage() { diff --git a/packages/karma-runner/src/index.ts b/packages/karma-runner/src/index.ts index 73578b6c9a..505ba8a7cd 100644 --- a/packages/karma-runner/src/index.ts +++ b/packages/karma-runner/src/index.ts @@ -3,3 +3,5 @@ import { declareClassPlugin, PluginKind } from '@stryker-mutator/api/plugin'; import KarmaTestRunner from './KarmaTestRunner'; export const strykerPlugins = [declareClassPlugin(PluginKind.TestRunner, 'karma', KarmaTestRunner)]; + +export * as strykerValidationSchema from '../schema/karma-runner-options.json'; diff --git a/packages/karma-runner/test/integration/KarmaTestRunner.it.spec.ts b/packages/karma-runner/test/integration/KarmaTestRunner.it.spec.ts index 81703995ef..c8ac5add9d 100644 --- a/packages/karma-runner/test/integration/KarmaTestRunner.it.spec.ts +++ b/packages/karma-runner/test/integration/KarmaTestRunner.it.spec.ts @@ -11,6 +11,7 @@ import { TestSelection } from '@stryker-mutator/api/test_framework'; import KarmaTestRunner from '../../src/KarmaTestRunner'; import { expectTestResults } from '../helpers/assertions'; +import { KarmaRunnerOptionsWithStrykerOptions } from '../../src/KarmaRunnerOptionsWithStrykerOptions'; function wrapInClosure(codeFragment: string) { return ` @@ -71,7 +72,7 @@ describe(`${KarmaTestRunner.name} integration`, () => { before(() => { testFramework = new MochaTestFramework(); setOptions(['testResources/sampleProject/src/Add.js', 'testResources/sampleProject/test-mocha/AddSpec.js']); - testInjector.options.karma.config.frameworks = ['mocha', 'chai']; + (testInjector.options as KarmaRunnerOptionsWithStrykerOptions).karma.config!.frameworks = ['mocha', 'chai']; sut = createSut(); return sut.init(); }); diff --git a/packages/karma-runner/tsconfig.src.json b/packages/karma-runner/tsconfig.src.json index 5d6c443c17..1b0ffa229c 100644 --- a/packages/karma-runner/tsconfig.src.json +++ b/packages/karma-runner/tsconfig.src.json @@ -11,7 +11,8 @@ "include": [ "src", "src-generated", - "typings" + "typings", + "schema/*.json" ], "references": [ { diff --git a/packages/mocha-runner/package.json b/packages/mocha-runner/package.json index ad89c840a6..6d2e5e8ab6 100644 --- a/packages/mocha-runner/package.json +++ b/packages/mocha-runner/package.json @@ -37,6 +37,7 @@ "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/mocha-runner#readme", "dependencies": { "@stryker-mutator/api": "^3.1.0", + "@stryker-mutator/util": "^3.1.0", "multimatch": "~4.0.0", "tslib": "~1.11.1" }, diff --git a/packages/mocha-runner/schema/mocha-runner-options.json b/packages/mocha-runner/schema/mocha-runner-options.json index cd907d59a5..4d68ae17d7 100644 --- a/packages/mocha-runner/schema/mocha-runner-options.json +++ b/packages/mocha-runner/schema/mocha-runner-options.json @@ -16,29 +16,25 @@ "type": "array", "items": { "type": "string" - }, - "default": [] + } }, "ignore": { "description": "Set mocha's [`ignore option](https://github.com/mochajs/mocha/blob/master/example/config/.mocharc.yml#L26)", "type": "array", "items": { "type": "string" - }, - "default": [] + } }, "file": { "description": "Set mocha's [`file options`](https://mochajs.org/#-file-filedirectoryglob)", "type": "array", "items": { "type": "string" - }, - "default": [] + } }, "opts": { "description": "Specify a ['mocha.opts' file](https://mochajs.org/#mochaopts) to be loaded. Options specified directly in your stryker.conf.js file will overrule options from the 'mocha.opts' file. Disable loading of an additional mocha.opts file with `false`. The only supported mocha options are used: `--ui`, `--require`, `--async-only`, `--timeout`, `--grep` (or their short form counterparts). Others are ignored by the @stryker-mutator/mocha-runner.", - "type": "string", - "default": "./test/mocha.opts" + "type": "string" }, "config": { "description": "Explicit path to the [mocha config file](https://mochajs.org/#-config-path)", @@ -50,33 +46,27 @@ }, "no-package": { "type": "boolean", - "default": "false", "description": "Explicit turn off [mocha package file](https://mochajs.org/#-package-path)" }, "no-opts": { "type": "boolean", - "default": "false", "description": "Explicit turn off [mocha opts file](https://mochajs.org/#-opts-path)" }, "no-config": { "type": "boolean", - "default": "false", "description": "Explicit turn off [mocha config file](https://mochajs.org/#-config-path)" }, "timeout": { "description": "Set mocha's [`timeout` option](https://mochajs.org/#-t---timeout-ms)", - "type": "number", - "default": 2000 + "type": "number" }, "async-only": { "description": "Set mocha's [`async-only` option](https://mochajs.org/#-async-only-a)", - "type": "boolean", - "default": false + "type": "boolean" }, "ui": { "description": "Set the name of your [mocha ui](https://mochajs.org/#-u---ui-name)", - "type": "string", - "default": "bdd" + "type": "string" }, "files": { "description": "DEPRECATED, use `spec` instead.", @@ -97,10 +87,7 @@ "type": "array", "items": { "type": "string" - }, - "default": [ - "test/**/*.js" - ] + } }, "grep": { "description": "Specify a mocha [`grep`](https://mochajs.org/#grep) command, to single out individual tests.", diff --git a/packages/mocha-runner/src/MochaOptionsEditor.ts b/packages/mocha-runner/src/MochaOptionsEditor.ts index 1203d5fc49..fb281f76fb 100644 --- a/packages/mocha-runner/src/MochaOptionsEditor.ts +++ b/packages/mocha-runner/src/MochaOptionsEditor.ts @@ -2,13 +2,14 @@ import { tokens } from '@stryker-mutator/api/plugin'; import { StrykerOptions, OptionsEditor } from '@stryker-mutator/api/core'; import MochaOptionsLoader from './MochaOptionsLoader'; -import { mochaOptionsKey } from './utils'; +import { MochaRunnerWithStrykerOptions } from './MochaRunnerWithStrykerOptions'; export default class MochaOptionsEditor implements OptionsEditor { public static inject = tokens('loader'); constructor(private readonly loader: MochaOptionsLoader) {} public edit(options: StrykerOptions): void { - options[mochaOptionsKey] = this.loader.load(options); + const optionsWithMocha = options as MochaRunnerWithStrykerOptions; + optionsWithMocha.mochaOptions = this.loader.load(optionsWithMocha); } } diff --git a/packages/mocha-runner/src/MochaOptionsLoader.ts b/packages/mocha-runner/src/MochaOptionsLoader.ts index 4eef28ae26..90c8d331af 100644 --- a/packages/mocha-runner/src/MochaOptionsLoader.ts +++ b/packages/mocha-runner/src/MochaOptionsLoader.ts @@ -1,14 +1,15 @@ 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 { propertyPath } from '@stryker-mutator/util'; -import { MochaOptions } from '../src-generated/mocha-runner-options'; +import { MochaOptions, MochaRunnerOptions } from '../src-generated/mocha-runner-options'; import LibWrapper from './LibWrapper'; -import { filterConfig, mochaOptionsKey, serializeArguments } from './utils'; +import { filterConfig, serializeMochaLoadOptionsArguments } from './utils'; +import { MochaRunnerWithStrykerOptions } from './MochaRunnerWithStrykerOptions'; /** * Subset of defaults for mocha options @@ -33,8 +34,8 @@ export default class MochaOptionsLoader { public static inject = tokens(commonTokens.logger); constructor(private readonly log: Logger) {} - public load(strykerOptions: StrykerOptions): MochaOptions { - const mochaOptions = { ...strykerOptions[mochaOptionsKey] } as MochaOptions; + public load(strykerOptions: MochaRunnerWithStrykerOptions): MochaOptions { + const mochaOptions = { ...strykerOptions.mochaOptions } as MochaOptions; return { ...DEFAULT_MOCHA_OPTIONS, ...this.loadMochaOptions(mochaOptions), ...mochaOptions }; } @@ -45,12 +46,12 @@ export default class MochaOptionsLoader { } else { this.log.warn('DEPRECATED: Mocha < 6 detected. Please upgrade to at least Mocha version 6.'); this.log.debug('Mocha < 6 detected. Using custom logic to parse mocha options'); - return this.loadLegacyMochaOptsFile(overrides.opts); + return this.loadLegacyMochaOptsFile(overrides); } } private loadMocha6Options(overrides: MochaOptions) { - const args = serializeArguments(overrides); + const args = serializeMochaLoadOptionsArguments(overrides); const loadOptions = LibWrapper.loadOptions || (() => ({})); const rawConfig = loadOptions(args) || {}; if (this.log.isTraceEnabled()) { @@ -63,21 +64,25 @@ export default class MochaOptionsLoader { return options; } - private loadLegacyMochaOptsFile(opts: false | string | undefined): Partial { - switch (typeof opts) { - case 'boolean': - this.log.debug('Not reading additional mochaOpts from a file'); - return {}; + private loadLegacyMochaOptsFile(options: MochaOptions): Partial { + if (options['no-opts']) { + this.log.debug('Not reading additional mochaOpts from a file'); + return options; + } + switch (typeof options.opts) { case 'undefined': - const defaultMochaOptsFileName = path.resolve(DEFAULT_MOCHA_OPTIONS.opts); + const defaultMochaOptsFileName = path.resolve(DEFAULT_MOCHA_OPTIONS.opts!); if (fs.existsSync(defaultMochaOptsFileName)) { return this.readMochaOptsFile(defaultMochaOptsFileName); } else { - this.log.debug('No mocha opts file found, not loading additional mocha options (%s.opts was not defined).', mochaOptionsKey); + this.log.debug( + 'No mocha opts file found, not loading additional mocha options (%s was not defined).', + propertyPath('mochaOptions', 'opts') + ); return {}; } case 'string': - const optsFileName = path.resolve(opts); + const optsFileName = path.resolve(options.opts); if (fs.existsSync(optsFileName)) { return this.readMochaOptsFile(optsFileName); } else { @@ -109,7 +114,7 @@ export default class MochaOptionsLoader { break; case '--timeout': case '-t': - mochaRunnerOptions.timeout = this.parseNextInt(args, DEFAULT_MOCHA_OPTIONS.timeout); + mochaRunnerOptions.timeout = this.parseNextInt(args, DEFAULT_MOCHA_OPTIONS.timeout!); break; case '--async-only': case '-A': @@ -117,7 +122,7 @@ export default class MochaOptionsLoader { break; case '--ui': case '-u': - mochaRunnerOptions.ui = this.parseNextString(args) ?? DEFAULT_MOCHA_OPTIONS.ui; + mochaRunnerOptions.ui = this.parseNextString(args) ?? DEFAULT_MOCHA_OPTIONS.ui!; break; case '--grep': case '-g': diff --git a/packages/mocha-runner/src/MochaRunnerWithStrykerOptions.ts b/packages/mocha-runner/src/MochaRunnerWithStrykerOptions.ts new file mode 100644 index 0000000000..4ebf80d50b --- /dev/null +++ b/packages/mocha-runner/src/MochaRunnerWithStrykerOptions.ts @@ -0,0 +1,5 @@ +import { StrykerOptions } from '@stryker-mutator/api/core'; + +import { MochaRunnerOptions } from '../src-generated/mocha-runner-options'; + +export interface MochaRunnerWithStrykerOptions extends StrykerOptions, MochaRunnerOptions {} diff --git a/packages/mocha-runner/src/MochaTestRunner.ts b/packages/mocha-runner/src/MochaTestRunner.ts index f773ba4c60..5d6cf05f28 100644 --- a/packages/mocha-runner/src/MochaTestRunner.ts +++ b/packages/mocha-runner/src/MochaTestRunner.ts @@ -4,12 +4,14 @@ import { StrykerOptions } from '@stryker-mutator/api/core'; import { Logger } from '@stryker-mutator/api/logging'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { RunResult, RunStatus, TestRunner } from '@stryker-mutator/api/test_runner'; +import { propertyPath } from '@stryker-mutator/util'; -import { MochaOptions } from '../src-generated/mocha-runner-options'; +import { MochaOptions, MochaRunnerOptions } from '../src-generated/mocha-runner-options'; import LibWrapper from './LibWrapper'; import { StrykerMochaReporter } from './StrykerMochaReporter'; -import { evalGlobal, mochaOptionsKey } from './utils'; +import { evalGlobal } from './utils'; +import { MochaRunnerWithStrykerOptions } from './MochaRunnerWithStrykerOptions'; const DEFAULT_TEST_PATTERN = 'test/**/*.js'; @@ -19,7 +21,7 @@ export class MochaTestRunner implements TestRunner { public static inject = tokens(commonTokens.logger, commonTokens.sandboxFileNames, commonTokens.options); constructor(private readonly log: Logger, private readonly allFileNames: readonly string[], options: StrykerOptions) { - this.mochaOptions = options[mochaOptionsKey]; + this.mochaOptions = (options as MochaRunnerWithStrykerOptions).mochaOptions; this.additionalRequires(); StrykerMochaReporter.log = log; } @@ -59,7 +61,10 @@ export class MochaTestRunner implements TestRunner { globPatterns, null, 2 - )}). Please specify the files (glob patterns) containing your tests in ${mochaOptionsKey}.files in your config file.` + )}). Please specify the files (glob patterns) containing your tests in ${propertyPath( + 'mochaOptions', + 'spec' + )} in your config file.` ); } return fileNames; diff --git a/packages/mocha-runner/src/index.ts b/packages/mocha-runner/src/index.ts index d6eb7804cb..ad4f26c80c 100644 --- a/packages/mocha-runner/src/index.ts +++ b/packages/mocha-runner/src/index.ts @@ -13,3 +13,5 @@ mochaOptionsEditorFactory.inject = tokens(commonTokens.injector); function mochaOptionsEditorFactory(injector: Injector): MochaOptionsEditor { return injector.provideClass('loader', MochaOptionsLoader).injectClass(MochaOptionsEditor); } + +export * as strykerValidationSchema from '../schema/mocha-runner-options.json'; diff --git a/packages/mocha-runner/src/utils.ts b/packages/mocha-runner/src/utils.ts index d050221c84..5e55ea0caf 100644 --- a/packages/mocha-runner/src/utils.ts +++ b/packages/mocha-runner/src/utils.ts @@ -11,22 +11,32 @@ export function evalGlobal(body: string) { fn(require); } -export function serializeArguments(mochaOptions: MochaOptions) { +export function serializeMochaLoadOptionsArguments(mochaOptions: MochaOptions): string[] { const args: string[] = []; - Object.keys(mochaOptions).forEach((key) => { - args.push(`--${key}`); - const value: any = (mochaOptions as any)[key]; - if (typeof value === 'string') { - args.push(value); - } else if (Array.isArray(value)) { - args.push(value.join(',')); - } - }); + if (mochaOptions['no-config']) { + args.push('--no-config'); + } + if (mochaOptions['no-opts']) { + args.push('--no-opts'); + } + if (mochaOptions['no-package']) { + args.push('--no-package'); + } + if (mochaOptions.package) { + args.push('--package'); + args.push(mochaOptions.package); + } + if (mochaOptions.opts) { + args.push('--opts'); + args.push(mochaOptions.opts); + } + if (mochaOptions.config) { + args.push('--config'); + args.push(mochaOptions.config); + } return args; } -export const mochaOptionsKey = 'mochaOptions'; - const SUPPORTED_MOCHA_OPTIONS = Object.freeze(Object.keys(mochaSchema.properties.mochaOptions.properties)); /** diff --git a/packages/mocha-runner/stryker.conf.js b/packages/mocha-runner/stryker.conf.js index 8d6b05fbb1..7e1de65ad2 100644 --- a/packages/mocha-runner/stryker.conf.js +++ b/packages/mocha-runner/stryker.conf.js @@ -1,7 +1,11 @@ const path = require('path'); +/** + * @type {import('@stryker-mutator/api/src-generated/stryker-core').StrykerOptions & import('./src-generated/mocha-runner-options').MochaRunnerOptions & import('../typescript/src-generated/typescript-options').TypescriptOptions} + */ const settings = require('../../stryker.parent.conf'); const moduleName = __dirname.split(path.sep).pop(); settings.dashboard.module = moduleName; -module.exports = function (config) { - config.set(settings); -}; +module.exports = settings; +settings.mochaOptions['async-only'] = 'true'; +settings.tsconfig = 'tsconfig.json'; + diff --git a/packages/mocha-runner/test/integration/MochaFileResolving.spec.ts b/packages/mocha-runner/test/integration/MochaFileResolving.spec.ts index 03ee36d3de..7a2fed1bdb 100644 --- a/packages/mocha-runner/test/integration/MochaFileResolving.spec.ts +++ b/packages/mocha-runner/test/integration/MochaFileResolving.spec.ts @@ -6,10 +6,16 @@ import { testInjector } from '@stryker-mutator/test-helpers'; import MochaOptionsLoader from '../../src/MochaOptionsLoader'; import { MochaTestRunner } from '../../src/MochaTestRunner'; -import { mochaOptionsKey } from '../../src/utils'; +import { MochaRunnerWithStrykerOptions } from '../../src/MochaRunnerWithStrykerOptions'; describe('Mocha 6 file resolving integration', () => { const cwd = process.cwd(); + let options: MochaRunnerWithStrykerOptions; + + beforeEach(() => { + options = testInjector.options as MochaRunnerWithStrykerOptions; + options.mochaOptions = {}; + }); afterEach(() => { process.chdir(cwd); @@ -18,7 +24,7 @@ describe('Mocha 6 file resolving integration', () => { it('should resolve test files while respecting "files", "spec", "extension" and "exclude" properties', () => { const configLoader = createConfigLoader(); process.chdir(resolveTestDir()); - testInjector.options[mochaOptionsKey] = configLoader.load(testInjector.options); + options.mochaOptions = configLoader.load(options); const testRunner = createTestRunner(); testRunner.init(); expect((testRunner as any).testFileNames).deep.eq([ diff --git a/packages/mocha-runner/test/integration/MochaOptionsLoader.it.spec.ts b/packages/mocha-runner/test/integration/MochaOptionsLoader.it.spec.ts index 09bd355cd3..9935212640 100644 --- a/packages/mocha-runner/test/integration/MochaOptionsLoader.it.spec.ts +++ b/packages/mocha-runner/test/integration/MochaOptionsLoader.it.spec.ts @@ -5,7 +5,7 @@ import { expect } from 'chai'; import { MochaOptions } from '../../src-generated/mocha-runner-options'; import MochaOptionsLoader, { DEFAULT_MOCHA_OPTIONS } from '../../src/MochaOptionsLoader'; -import { mochaOptionsKey } from '../../src/utils'; +import { MochaRunnerWithStrykerOptions } from '../../src/MochaRunnerWithStrykerOptions'; describe(`${MochaOptionsLoader.name} integration`, () => { let sut: MochaOptionsLoader; @@ -142,9 +142,10 @@ describe(`${MochaOptionsLoader.name} integration`, () => { return path.resolve(__dirname, '..', '..', 'testResources', 'mocha-config', relativeName); } - function actLoad(mochaConfig: Partial): MochaOptions { - testInjector.options[mochaOptionsKey] = mochaConfig; - return sut.load(testInjector.options); + function actLoad(mochaConfig: MochaOptions): MochaOptions { + const mochaRunnerWithStrykerOptions = testInjector.options as MochaRunnerWithStrykerOptions; + mochaRunnerWithStrykerOptions.mochaOptions = mochaConfig; + return sut.load(mochaRunnerWithStrykerOptions); } function createSut() { diff --git a/packages/mocha-runner/test/integration/QUnitSample.it.spec.ts b/packages/mocha-runner/test/integration/QUnitSample.it.spec.ts index bbdf99a8c2..d24d5fc53b 100644 --- a/packages/mocha-runner/test/integration/QUnitSample.it.spec.ts +++ b/packages/mocha-runner/test/integration/QUnitSample.it.spec.ts @@ -26,7 +26,7 @@ describe('QUnit sample', () => { ui: 'qunit', }); testInjector.options.mochaOptions = mochaOptions; - files = mochaOptions.spec; + files = mochaOptions.spec!; const sut = createSut(); await sut.init(); const actualResult = await sut.run({}); diff --git a/packages/mocha-runner/test/unit/MochaOptionsLoader.spec.ts b/packages/mocha-runner/test/unit/MochaOptionsLoader.spec.ts index a2d09d78b9..060d5082e3 100644 --- a/packages/mocha-runner/test/unit/MochaOptionsLoader.spec.ts +++ b/packages/mocha-runner/test/unit/MochaOptionsLoader.spec.ts @@ -3,22 +3,23 @@ import * as path from 'path'; import sinon = require('sinon'); import { expect } from 'chai'; -import { testInjector, factory } from '@stryker-mutator/test-helpers'; -import { StrykerOptions } from '@stryker-mutator/api/core'; +import { testInjector } from '@stryker-mutator/test-helpers'; import LibWrapper from '../../src/LibWrapper'; import { MochaOptions } from '../../src-generated/mocha-runner-options'; import MochaOptionsLoader from '../../src/MochaOptionsLoader'; -import { mochaOptionsKey } from '../../src/utils'; +import { MochaRunnerWithStrykerOptions } from '../../src/MochaRunnerWithStrykerOptions'; describe(MochaOptionsLoader.name, () => { let readFileStub: sinon.SinonStub; let existsFileStub: sinon.SinonStub; - let strykerOptions: StrykerOptions; + let options: MochaRunnerWithStrykerOptions; let sut: MochaOptionsLoader; beforeEach(() => { sut = testInjector.injector.injectClass(MochaOptionsLoader); + options = testInjector.options as MochaRunnerWithStrykerOptions; + options.mochaOptions = {}; }); describe('with mocha >= 6', () => { @@ -30,7 +31,7 @@ describe(MochaOptionsLoader.name, () => { }); it('should log about mocha >= 6', () => { - sut.load(testInjector.options); + sut.load(options); expect(testInjector.logger.debug).calledWith( "Mocha >= 6 detected. Using mocha's `%s` to load mocha options", LibWrapper.loadOptions && LibWrapper.loadOptions.name @@ -38,20 +39,30 @@ describe(MochaOptionsLoader.name, () => { }); it('should call `loadOptions` with serialized arguments', () => { - testInjector.options[mochaOptionsKey] = { - ['no-baz']: true, - foo: 'bar', - spec: ['helpers/*.js', 'test/*.js'], + options.mochaOptions = { + 'no-config': true, + 'no-opts': true, + 'no-package': true, + package: 'alternative-package.json', + config: 'alternative-config.yaml', + opts: 'alternative.opts', + extension: ['.js'], // should not appear in serialized arguments }; - sut.load(testInjector.options); - expect(LibWrapper.loadOptions).calledWith(['--no-baz', '--foo', 'bar', '--spec', 'helpers/*.js,test/*.js']); + sut.load(options); + expect(LibWrapper.loadOptions).calledWith([ + '--no-config', + '--no-opts', + '--no-package', + '--package', + 'alternative-package.json', + '--opts', + 'alternative.opts', + '--config', + 'alternative-config.yaml', + ]); }); it('should filter out invalid options from the `loadOptions` result', () => { - testInjector.options[mochaOptionsKey] = { - override: true, - }; - // Following are valid options rawOptions.extension = ['foo']; rawOptions.require = ['bar']; @@ -64,7 +75,7 @@ describe(MochaOptionsLoader.name, () => { rawOptions.spec = ['test/**/*.js']; rawOptions.garply = 'waldo'; // this should be filtered out - const result = sut.load(testInjector.options); + const result = sut.load(options); const expected: MochaOptions = createMochaOptions({ extension: ['foo'], file: ['grault'], @@ -77,23 +88,23 @@ describe(MochaOptionsLoader.name, () => { 'async-only': true, ui: 'quux', }); - expect(result).deep.eq({ ...expected, override: true }); + expect(result).deep.eq({ ...expected }); }); it('should trace log the mocha call', () => { testInjector.logger.isTraceEnabled.returns(true); - testInjector.options[mochaOptionsKey] = { - foo: 'bar', + options.mochaOptions = { + 'no-config': true, }; rawOptions.baz = 'qux'; - sut.load(testInjector.options); + sut.load(options); const fnName = LibWrapper.loadOptions && LibWrapper.loadOptions.name; - expect(testInjector.logger.trace).calledWith(`Mocha: ${fnName}(['--foo','bar']) => {"baz":"qux"}`); + expect(testInjector.logger.trace).calledWith(`Mocha: ${fnName}(['--no-config']) => {"baz":"qux"}`); }); it("should respect mocha's defaults", () => { - const options = sut.load(testInjector.options); - expect(options).deep.eq(createMochaOptions()); + const actualOptions = sut.load(options); + expect(actualOptions).deep.eq(createMochaOptions()); }); }); @@ -102,27 +113,26 @@ describe(MochaOptionsLoader.name, () => { sinon.stub(LibWrapper, 'loadOptions').value(undefined); readFileStub = sinon.stub(fs, 'readFileSync'); existsFileStub = sinon.stub(fs, 'existsSync').returns(true); - strykerOptions = factory.strykerOptions(); }); it('should log about mocha < 6', () => { existsFileStub.returns(false); - sut.load(strykerOptions); + sut.load(options); expect(testInjector.logger.debug).calledWith('Mocha < 6 detected. Using custom logic to parse mocha options'); }); it('should log deprecated mocha version warning', async () => { existsFileStub.returns(false); - sut.load(strykerOptions); + sut.load(options); expect(testInjector.logger.warn).calledWith('DEPRECATED: Mocha < 6 detected. Please upgrade to at least Mocha version 6.'); }); it('should load a mocha.opts file if specified', () => { readFileStub.returns(''); - strykerOptions.mochaOptions = { + options.mochaOptions = { opts: 'some/mocha.opts/file', }; - sut.load(strykerOptions); + sut.load(options); expect(testInjector.logger.info).calledWith(`Loading mochaOpts from "${path.resolve('some/mocha.opts/file')}"`); expect(fs.readFileSync).calledWith(path.resolve('some/mocha.opts/file')); }); @@ -130,11 +140,11 @@ describe(MochaOptionsLoader.name, () => { it("should log an error if specified mocha.opts file doesn't exist", () => { readFileStub.returns(''); existsFileStub.returns(false); - strykerOptions.mochaOptions = { + options.mochaOptions = { opts: 'some/mocha.opts/file', }; - sut.load(strykerOptions); + sut.load(options); expect(testInjector.logger.error).calledWith( `Could not load opts from "${path.resolve('some/mocha.opts/file')}". Please make sure opts file exists.` ); @@ -142,27 +152,27 @@ describe(MochaOptionsLoader.name, () => { it('should load default mocha.opts file if not specified', () => { readFileStub.returns(''); - sut.load(strykerOptions); + sut.load(options); expect(testInjector.logger.info).calledWith(`Loading mochaOpts from "${path.resolve('test/mocha.opts')}"`); expect(fs.readFileSync).calledWith(path.resolve('./test/mocha.opts')); }); - it("shouldn't load anything if mocha.opts = false", () => { - strykerOptions.mochaOptions = { - opts: false, + it("shouldn't load anything if mocha['no-opts'] = true", () => { + options.mochaOptions = { + 'no-opts': true, }; - sut.load(strykerOptions); + sut.load(options); expect(fs.readFileSync).not.called; expect(testInjector.logger.debug).calledWith('Not reading additional mochaOpts from a file'); }); it('should not load default mocha.opts file if not found', () => { existsFileStub.returns(false); - const options = sut.load(strykerOptions); - expect(options).deep.eq(createMochaOptions()); + const mochaOptions = sut.load(options); + expect(mochaOptions).deep.eq(createMochaOptions()); expect(testInjector.logger.debug).calledWith( - 'No mocha opts file found, not loading additional mocha options (%s.opts was not defined).', - 'mochaOptions' + 'No mocha opts file found, not loading additional mocha options (%s was not defined).', + 'mochaOptions.opts' ); }); @@ -171,9 +181,9 @@ describe(MochaOptionsLoader.name, () => { --require src/test/support/setup -r babel-require `); - strykerOptions.mochaOptions = { opts: '.' }; - const options = sut.load(strykerOptions); - expect(options).deep.include({ + options.mochaOptions = { opts: '.' }; + const mochaOptions = sut.load(options); + expect(mochaOptions).deep.include({ require: ['src/test/support/setup', 'babel-require'], }); }); @@ -181,8 +191,8 @@ describe(MochaOptionsLoader.name, () => { function itShouldLoadProperty(property: string, value: string, expectedConfig: Partial) { it(`should load '${property} if specified`, () => { readFileStub.returns(`${property} ${value}`); - strykerOptions.mochaOptions = { opts: 'path/to/opts/file' }; - expect(sut.load(strykerOptions)).deep.include(expectedConfig); + options.mochaOptions = { opts: 'path/to/opts/file' }; + expect(sut.load(options)).deep.include(expectedConfig); }); } @@ -203,15 +213,15 @@ describe(MochaOptionsLoader.name, () => { -A -r babel-register `); - strykerOptions.mochaOptions = { + options.mochaOptions = { 'async-only': false, opts: 'path/to/opts/file', require: ['ts-node/register'], timeout: 4000, ui: 'exports', }; - const options = sut.load(strykerOptions); - expect(options).deep.equal( + const mochaOptions = sut.load(options); + expect(mochaOptions).deep.equal( createMochaOptions({ 'async-only': false, extension: ['js'], @@ -229,12 +239,12 @@ describe(MochaOptionsLoader.name, () => { --reporter dot --ignore-leaks `); - strykerOptions.mochaOptions = { + options.mochaOptions = { opts: 'some/mocha.opts/file', }; - const options = sut.load(strykerOptions); - expect(options).not.have.property('reporter'); - expect(options).not.have.property('ignore-leaks'); + const mochaOptions = sut.load(options); + expect(mochaOptions).not.have.property('reporter'); + expect(mochaOptions).not.have.property('ignore-leaks'); expect(testInjector.logger.debug).calledWith('Ignoring option "--reporter" as it is not supported.'); expect(testInjector.logger.debug).calledWith('Ignoring option "--ignore-leaks" as it is not supported.'); }); @@ -244,11 +254,11 @@ describe(MochaOptionsLoader.name, () => { --timeout --ui `); - strykerOptions.mochaOptions = { + options.mochaOptions = { opts: 'some/mocha.opts/file', }; - const options = sut.load(strykerOptions); - expect(options).deep.eq( + const mochaOptions = sut.load(options); + expect(mochaOptions).deep.eq( createMochaOptions({ extension: ['js'], opts: 'some/mocha.opts/file', diff --git a/packages/mocha-runner/test/unit/MochaTestRunner.spec.ts b/packages/mocha-runner/test/unit/MochaTestRunner.spec.ts index ee69dbbd14..8c1d6b74e6 100644 --- a/packages/mocha-runner/test/unit/MochaTestRunner.spec.ts +++ b/packages/mocha-runner/test/unit/MochaTestRunner.spec.ts @@ -122,7 +122,7 @@ describe(MochaTestRunner.name, () => { // Assert expect(actFn).throws( - `[MochaTestRunner] No files discovered (tried pattern(s) ${relativeGlobbing}). Please specify the files (glob patterns) containing your tests in mochaOptions.files in your config file.` + `[MochaTestRunner] No files discovered (tried pattern(s) ${relativeGlobbing}). Please specify the files (glob patterns) containing your tests in mochaOptions.spec in your config file.` ); expect(testInjector.logger.debug).calledWith(`Tried ${absoluteGlobbing} on files: ${filesStringified}.`); }); diff --git a/packages/mocha-runner/tsconfig.src.json b/packages/mocha-runner/tsconfig.src.json index a68eab47ec..394edac4c1 100644 --- a/packages/mocha-runner/tsconfig.src.json +++ b/packages/mocha-runner/tsconfig.src.json @@ -4,7 +4,7 @@ "rootDir": "." }, "include": [ - "schema/**/*.json", + "schema/*.json", "src", "src-generated", "typings" diff --git a/packages/mocha-runner/tsconfig.stryker.json b/packages/mocha-runner/tsconfig.stryker.json index 3ffe9e8c61..ff126f747a 100644 --- a/packages/mocha-runner/tsconfig.stryker.json +++ b/packages/mocha-runner/tsconfig.stryker.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.test.json", "include": [ - "schema/**/*.json", "src-generated", "src", "test" diff --git a/packages/test-helpers/src/TestInjector.ts b/packages/test-helpers/src/TestInjector.ts index 0f37ed136a..16e536be61 100644 --- a/packages/test-helpers/src/TestInjector.ts +++ b/packages/test-helpers/src/TestInjector.ts @@ -35,10 +35,7 @@ class TestInjector { this.mutatorDescriptor = factory.mutatorDescriptor(); this.options = factory.strykerOptions(); this.logger = factory.logger(); - this.pluginResolver = { - resolve: sinon.stub(), - resolveAll: sinon.stub(), - }; + this.pluginResolver = factory.pluginResolver(); } } diff --git a/packages/test-helpers/src/factory.ts b/packages/test-helpers/src/factory.ts index 84cf68ff65..95a564c0b4 100644 --- a/packages/test-helpers/src/factory.ts +++ b/packages/test-helpers/src/factory.ts @@ -11,6 +11,7 @@ import { Metrics, MetricsResult } from 'mutation-testing-metrics'; import * as sinon from 'sinon'; import { Injector } from 'typed-inject'; import { OptionsEditor } from '@stryker-mutator/api/src/core/OptionsEditor'; +import { PluginResolver } from '@stryker-mutator/api/plugin'; const ajv = new Ajv({ useDefaults: true }); @@ -40,6 +41,14 @@ function factoryMethod(defaultsFactory: () => T) { export const location = factoryMethod(() => ({ start: { line: 0, column: 0 }, end: { line: 0, column: 0 } })); +export function pluginResolver(): sinon.SinonStubbedInstance { + return { + resolve: sinon.stub(), + resolveAll: sinon.stub(), + resolveValidationSchemaContributions: sinon.stub(), + }; +} + export const mutantResult = factoryMethod(() => ({ id: '256', location: location(), @@ -168,6 +177,10 @@ export const strykerOptions = factoryMethod(() => { return options; }); +export const strykerWithPluginOptions = (pluginOptions: T): T & StrykerOptions => { + return { ...strykerOptions(), ...pluginOptions }; +}; + export const mutatorDescriptor = factoryMethod(() => ({ excludedMutations: [], name: 'fooMutator', diff --git a/packages/typescript/schema/typescript-options.json b/packages/typescript/schema/typescript-options.json index 7847b8650f..a4c1de2230 100644 --- a/packages/typescript/schema/typescript-options.json +++ b/packages/typescript/schema/typescript-options.json @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema", "type": "object", + "title": "TypescriptOptions", "additionalProperties": false, "properties": { "tsconfigFile": { diff --git a/packages/typescript/src/TypescriptOptionsEditor.ts b/packages/typescript/src/TypescriptOptionsEditor.ts index 036ff0ce52..2579315d7e 100644 --- a/packages/typescript/src/TypescriptOptionsEditor.ts +++ b/packages/typescript/src/TypescriptOptionsEditor.ts @@ -5,10 +5,13 @@ import * as path from 'path'; import { Logger } from '@stryker-mutator/api/logging'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import * as ts from 'typescript'; -import { StrykerOptions, OptionsEditor } from '@stryker-mutator/api/core'; +import { OptionsEditor } from '@stryker-mutator/api/core'; +import { propertyPath } from '@stryker-mutator/util'; + +import { TypescriptOptions } from '../src-generated/typescript-options'; -import { CONFIG_KEY, CONFIG_KEY_FILE } from './helpers/keys'; import { normalizeFileForTypescript, normalizeFileFromTypescript } from './helpers/tsHelpers'; +import { TypescriptWithStrykerOptions } from './TypescriptWithStrykerOptions'; // Override some compiler options that have to do with code quality. When mutating, we're not interested in the resulting code quality // See https://github.com/stryker-mutator/stryker/issues/391 for more info @@ -18,24 +21,24 @@ const COMPILER_OPTIONS_OVERRIDES: Readonly> = Object noUnusedParameters: false, }); -export default class TypescriptOptionsEditor implements OptionsEditor { +export default class TypescriptOptionsEditor implements OptionsEditor { public static inject = tokens(commonTokens.logger); constructor(private readonly log: Logger) {} - public edit(strykerConfig: StrykerOptions, host: ts.ParseConfigHost = ts.sys) { + public edit(strykerConfig: TypescriptWithStrykerOptions, host: ts.ParseConfigHost = ts.sys) { this.loadTSConfig(strykerConfig, host); } - private loadTSConfig(strykerConfig: StrykerOptions, host: ts.ParseConfigHost) { - if (typeof strykerConfig[CONFIG_KEY_FILE] === 'string') { - const tsconfigFileName = path.resolve(strykerConfig[CONFIG_KEY_FILE]); + private loadTSConfig(strykerConfig: TypescriptWithStrykerOptions, host: ts.ParseConfigHost) { + if (strykerConfig.tsconfigFile) { + const tsconfigFileName = path.resolve(strykerConfig.tsconfigFile); this.log.info(`Loading tsconfig file ${tsconfigFileName}`); const tsconfig = this.readTypescriptConfig(tsconfigFileName, host); if (tsconfig) { - strykerConfig[CONFIG_KEY] = this.overrideOptions(tsconfig); + strykerConfig.tsconfig = this.overrideOptions(tsconfig) as any; } } else { - this.log.debug("No '%s' specified, not loading any config", CONFIG_KEY_FILE); + this.log.debug("No '%s' specified, not loading any config", propertyPath('tsconfigFile')); } } diff --git a/packages/typescript/src/TypescriptTranspiler.ts b/packages/typescript/src/TypescriptTranspiler.ts index d79351fd18..4c97ff523b 100644 --- a/packages/typescript/src/TypescriptTranspiler.ts +++ b/packages/typescript/src/TypescriptTranspiler.ts @@ -4,22 +4,21 @@ import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { Transpiler } from '@stryker-mutator/api/transpile'; import * as ts from 'typescript'; -import { getProjectDirectory, getTSConfig, guardTypescriptVersion, isHeaderFile } from './helpers/tsHelpers'; +import { getProjectDirectory, getTSConfig, guardTypescriptVersion, isHeaderFile as isDeclarationFile } from './helpers/tsHelpers'; import TranspileFilter from './transpiler/TranspileFilter'; import TranspilingLanguageService from './transpiler/TranspilingLanguageService'; +import { TypescriptWithStrykerOptions } from './TypescriptWithStrykerOptions'; export default class TypescriptTranspiler implements Transpiler { private languageService: TranspilingLanguageService; private readonly filter: TranspileFilter; + private readonly options: TypescriptWithStrykerOptions; public static inject = tokens(commonTokens.options, commonTokens.produceSourceMaps, commonTokens.getLogger); - constructor( - private readonly options: StrykerOptions, - private readonly produceSourceMaps: boolean, - private readonly getLogger: LoggerFactoryMethod - ) { + constructor(options: StrykerOptions, private readonly produceSourceMaps: boolean, private readonly getLogger: LoggerFactoryMethod) { guardTypescriptVersion(); - this.filter = TranspileFilter.create(this.options); + this.options = options; + this.filter = TranspileFilter.create(options); } public transpile(files: readonly File[]): Promise { @@ -59,7 +58,7 @@ export default class TypescriptTranspiler implements Transpiler { const fileDictionary: { [name: string]: File } = {}; files.forEach((file) => (fileDictionary[file.name] = file)); files.forEach((file) => { - if (!isHeaderFile(file.name)) { + if (!isDeclarationFile(file.name) && !this.hasJsonOutput(file.name)) { if (this.filter.isIncluded(file.name)) { // File is to be transpiled. Only emit if more output is expected. if (!isSingleOutput) { @@ -76,4 +75,7 @@ export default class TypescriptTranspiler implements Transpiler { return Object.keys(fileDictionary).map((name) => fileDictionary[name]); } + private hasJsonOutput(fileName: string): boolean { + return fileName.endsWith('.json') && !getTSConfig(this.options).options.outDir; + } } diff --git a/packages/typescript/src/TypescriptWithStrykerOptions.ts b/packages/typescript/src/TypescriptWithStrykerOptions.ts new file mode 100644 index 0000000000..bbe8d8f22d --- /dev/null +++ b/packages/typescript/src/TypescriptWithStrykerOptions.ts @@ -0,0 +1,5 @@ +import { StrykerOptions } from '@stryker-mutator/api/core'; + +import { TypescriptOptions } from '../src-generated/typescript-options'; + +export interface TypescriptWithStrykerOptions extends TypescriptOptions, StrykerOptions {} diff --git a/packages/typescript/src/helpers/keys.ts b/packages/typescript/src/helpers/keys.ts deleted file mode 100644 index 9d7b2d1191..0000000000 --- a/packages/typescript/src/helpers/keys.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CONFIG_KEY_FILE = 'tsconfigFile'; -export const CONFIG_KEY = 'tsconfig'; diff --git a/packages/typescript/src/helpers/tsHelpers.ts b/packages/typescript/src/helpers/tsHelpers.ts index c371e8abec..6c59447ccd 100644 --- a/packages/typescript/src/helpers/tsHelpers.ts +++ b/packages/typescript/src/helpers/tsHelpers.ts @@ -1,11 +1,11 @@ import * as os from 'os'; import * as path from 'path'; -import { File, StrykerOptions } from '@stryker-mutator/api/core'; +import { File } from '@stryker-mutator/api/core'; import * as semver from 'semver'; import * as ts from 'typescript'; -import { CONFIG_KEY, CONFIG_KEY_FILE } from './keys'; +import { TypescriptWithStrykerOptions } from '../TypescriptWithStrykerOptions'; export function parseFile(file: File, target: ts.ScriptTarget | undefined) { return ts.createSourceFile(file.name, file.textContent, target || ts.ScriptTarget.ES5, /*setParentNodes*/ true); @@ -27,12 +27,12 @@ export function normalizeFileFromTypescript(fileName: string) { return path.normalize(fileName); } -export function getTSConfig(options: StrykerOptions): ts.ParsedCommandLine | undefined { - return options[CONFIG_KEY]; +export function getTSConfig(options: TypescriptWithStrykerOptions): ts.ParsedCommandLine { + return (options.tsconfig as unknown) as ts.ParsedCommandLine; } -export function getProjectDirectory(options: StrykerOptions) { - return path.dirname(options[CONFIG_KEY_FILE] || '.'); +export function getProjectDirectory(options: TypescriptWithStrykerOptions) { + return path.dirname(options.tsconfigFile || '.'); } /** @@ -70,7 +70,7 @@ export function isTypescriptFile(fileName: string) { } export function isJavaScriptFile(file: ts.OutputFile) { - return file.name.endsWith('.js') || file.name.endsWith('.jsx'); + return file.name.endsWith('.js') || file.name.endsWith('.jsx') || file.name.endsWith('.json'); } export function isMapFile(file: ts.OutputFile) { diff --git a/packages/typescript/src/index.ts b/packages/typescript/src/index.ts index 0480508116..a64129d4f0 100644 --- a/packages/typescript/src/index.ts +++ b/packages/typescript/src/index.ts @@ -9,3 +9,5 @@ export const strykerPlugins = [ declareClassPlugin(PluginKind.Transpiler, 'typescript', TypescriptTranspiler), declareFactoryPlugin(PluginKind.Mutator, 'typescript', typescriptMutatorFactory), ]; + +export * as strykerValidationSchema from '../schema/typescript-options.json'; diff --git a/packages/typescript/test/integration/allowJS.it.spec.ts b/packages/typescript/test/integration/allowJS.it.spec.ts index 79c88e0df4..299eb9d74f 100644 --- a/packages/typescript/test/integration/allowJS.it.spec.ts +++ b/packages/typescript/test/integration/allowJS.it.spec.ts @@ -1,24 +1,24 @@ import * as fs from 'fs'; import * as path from 'path'; -import { File, StrykerOptions } from '@stryker-mutator/api/core'; +import { File } from '@stryker-mutator/api/core'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { expect } from 'chai'; -import { CONFIG_KEY } from '../../src/helpers/keys'; import TypescriptOptionsEditor from '../../src/TypescriptOptionsEditor'; import TypescriptTranspiler from '../../src/TypescriptTranspiler'; +import { TypescriptWithStrykerOptions } from '../../src/TypescriptWithStrykerOptions'; describe('AllowJS integration', () => { - let options: StrykerOptions; + let options: TypescriptWithStrykerOptions; let inputFiles: File[]; beforeEach(() => { const optionsEditor = testInjector.injector.injectClass(TypescriptOptionsEditor); - options = factory.strykerOptions(); + options = factory.strykerOptions() as TypescriptWithStrykerOptions; options.tsconfigFile = path.resolve(__dirname, '..', '..', 'testResources', 'allowJS', 'tsconfig.json'); optionsEditor.edit(options); - inputFiles = options[CONFIG_KEY].fileNames.map((fileName: string) => new File(fileName, fs.readFileSync(fileName, 'utf8'))); + inputFiles = (options.tsconfig!.fileNames as string[]).map((fileName) => new File(fileName, fs.readFileSync(fileName, 'utf8'))); }); it('should be able to transpile source code', async () => { diff --git a/packages/typescript/test/integration/allowJsonFile.it.spec.ts b/packages/typescript/test/integration/allowJsonFile.it.spec.ts new file mode 100644 index 0000000000..14fa8f6037 --- /dev/null +++ b/packages/typescript/test/integration/allowJsonFile.it.spec.ts @@ -0,0 +1,42 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { File } from '@stryker-mutator/api/core'; +import { testInjector, factory } from '@stryker-mutator/test-helpers'; +import { expect } from 'chai'; +import { ParsedCommandLine } from 'typescript'; + +import TypescriptOptionsEditor from '../../src/TypescriptOptionsEditor'; +import TypescriptTranspiler from '../../src/TypescriptTranspiler'; +import { TypescriptWithStrykerOptions } from '../../src/TypescriptWithStrykerOptions'; + +describe('AllowJsonFiles integration', () => { + let options: TypescriptWithStrykerOptions; + let inputFiles: File[]; + + beforeEach(() => { + const optionsEditor = testInjector.injector.injectClass(TypescriptOptionsEditor); + options = factory.strykerOptions(); + options.tsconfigFile = path.resolve(__dirname, '..', '..', 'testResources', 'allowJsonFiles', 'tsconfig.json'); + optionsEditor.edit(options); + inputFiles = (options.tsconfig!.fileNames as string[]).map((fileName) => new File(fileName, fs.readFileSync(fileName, 'utf8'))); + }); + + it('should be able to transpile source code', async () => { + const transpiler = new TypescriptTranspiler(options, /*produceSourceMaps: */ false, () => testInjector.logger); + const outputFiles = await transpiler.transpile(inputFiles); + expect(outputFiles.length).to.eq(2); + expect(outputFiles.map((f) => f.name)).deep.eq([ + path.resolve(__dirname, '..', '..', 'testResources', 'allowJsonFiles', 'json.json'), + path.resolve(__dirname, '..', '..', 'testResources', 'allowJsonFiles', 'index.js'), + ]); + }); + + it('should be able to transpile source code to outDir', async () => { + ((options.tsconfig as unknown) as ParsedCommandLine).options.outDir = 'dist'; + const transpiler = new TypescriptTranspiler(options, /*produceSourceMaps: */ false, () => testInjector.logger); + const outputFiles = await transpiler.transpile(inputFiles); + expect(outputFiles.length).to.eq(2); + expect(outputFiles.map((f) => f.name)).deep.eq([path.join('dist', 'index.js'), path.join('dist', 'json.json')]); + }); +}); diff --git a/packages/typescript/test/integration/configEditor.it.spec.ts b/packages/typescript/test/integration/configEditor.it.spec.ts index 0105f0d478..76311b9e0f 100644 --- a/packages/typescript/test/integration/configEditor.it.spec.ts +++ b/packages/typescript/test/integration/configEditor.it.spec.ts @@ -4,6 +4,7 @@ import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { expect } from 'chai'; import TypescriptOptionsEditor from '../../src/TypescriptOptionsEditor'; +import { TypescriptWithStrykerOptions } from '../../src/TypescriptWithStrykerOptions'; function resolveSampleProject(relativeFileName: string) { return path.resolve(__dirname, '..', '..', 'testResources', 'sampleProject', relativeFileName); @@ -11,10 +12,12 @@ function resolveSampleProject(relativeFileName: string) { describe('Read TS Config file integration', () => { it('should discover files like TS does', () => { - const config = factory.strykerOptions(); + const config = factory.strykerOptions() as TypescriptWithStrykerOptions; config.tsconfigFile = resolveSampleProject('tsconfig.json'); testInjector.injector.injectClass(TypescriptOptionsEditor).edit(config); - const actual = config.tsconfig; - expect(actual.fileNames.map(path.normalize)).deep.eq([resolveSampleProject('math.ts'), resolveSampleProject('useMath.ts')]); + expect((config.tsconfig!.fileNames as string[]).map(path.normalize)).deep.eq([ + resolveSampleProject('math.ts'), + resolveSampleProject('useMath.ts'), + ]); }); }); diff --git a/packages/typescript/test/integration/ownDogFood.it.spec.ts b/packages/typescript/test/integration/ownDogFood.it.spec.ts index 58027507f8..a6ad6bd6ee 100644 --- a/packages/typescript/test/integration/ownDogFood.it.spec.ts +++ b/packages/typescript/test/integration/ownDogFood.it.spec.ts @@ -1,16 +1,16 @@ import * as fs from 'fs'; import * as path from 'path'; -import { File, StrykerOptions } from '@stryker-mutator/api/core'; +import { File } from '@stryker-mutator/api/core'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { expect } from 'chai'; -import { CONFIG_KEY } from '../../src/helpers/keys'; import TypescriptOptionsEditor from '../../src/TypescriptOptionsEditor'; import TypescriptTranspiler from '../../src/TypescriptTranspiler'; +import { TypescriptWithStrykerOptions } from '../../src/TypescriptWithStrykerOptions'; describe('@stryker-mutator/typescript', () => { - let options: StrykerOptions; + let options: TypescriptWithStrykerOptions; let inputFiles: File[]; beforeEach(() => { @@ -18,7 +18,8 @@ describe('@stryker-mutator/typescript', () => { options = factory.strykerOptions(); options.tsconfigFile = path.resolve(__dirname, '..', '..', 'tsconfig.src.json'); optionsEditor.edit(options); - inputFiles = options[CONFIG_KEY].fileNames.map((fileName: string) => new File(fileName, fs.readFileSync(fileName, 'utf8'))); + (options.tsconfig!.options as any)['outDir'] = 'blaat'; + inputFiles = (options.tsconfig!.fileNames as string[]).map((fileName) => new File(fileName, fs.readFileSync(fileName, 'utf8'))); }); it('should be able to transpile itself', async () => { @@ -34,7 +35,7 @@ describe('@stryker-mutator/typescript', () => { }); it('should not result in an error if a variable is declared as any and noImplicitAny = false', async () => { - options.tsconfig.noImplicitAny = false; + options.tsconfig!.noImplicitAny = false; inputFiles[0] = new File(inputFiles[0].name, inputFiles[0].textContent + 'const shouldResultInError = 3'); const transpiler = new TypescriptTranspiler(options, /*produceSourceMaps: */ true, () => testInjector.logger); const outputFiles = await transpiler.transpile(inputFiles); diff --git a/packages/typescript/test/integration/sample.it.spec.ts b/packages/typescript/test/integration/sample.it.spec.ts index 10008a1ee4..3f3e0c99c5 100644 --- a/packages/typescript/test/integration/sample.it.spec.ts +++ b/packages/typescript/test/integration/sample.it.spec.ts @@ -1,18 +1,18 @@ import * as fs from 'fs'; import * as path from 'path'; -import { File, StrykerOptions } from '@stryker-mutator/api/core'; +import { File } from '@stryker-mutator/api/core'; import { Mutant } from '@stryker-mutator/api/mutant'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { expect } from 'chai'; -import { CONFIG_KEY } from '../../src/helpers/keys'; import TypescriptOptionsEditor from '../../src/TypescriptOptionsEditor'; import { typescriptMutatorFactory } from '../../src/TypescriptMutator'; import TypescriptTranspiler from '../../src/TypescriptTranspiler'; +import { TypescriptWithStrykerOptions } from '../../src/TypescriptWithStrykerOptions'; describe('Sample integration', () => { - let options: StrykerOptions; + let options: TypescriptWithStrykerOptions; let inputFiles: File[]; beforeEach(() => { @@ -20,7 +20,7 @@ describe('Sample integration', () => { options = factory.strykerOptions(); options.tsconfigFile = path.resolve(__dirname, '..', '..', 'testResources', 'sampleProject', 'tsconfig.json'); optionsEditor.edit(options); - inputFiles = options[CONFIG_KEY].fileNames.map((fileName: string) => new File(fileName, fs.readFileSync(fileName, 'utf8'))); + inputFiles = (options.tsconfig!.fileNames as string[]).map((fileName) => new File(fileName, fs.readFileSync(fileName, 'utf8'))); testInjector.options = options; }); diff --git a/packages/typescript/test/integration/useHeaderFile.ts b/packages/typescript/test/integration/useHeaderFile.it.spec.ts similarity index 78% rename from packages/typescript/test/integration/useHeaderFile.ts rename to packages/typescript/test/integration/useHeaderFile.it.spec.ts index 49b7e00bc5..74342cc682 100644 --- a/packages/typescript/test/integration/useHeaderFile.ts +++ b/packages/typescript/test/integration/useHeaderFile.it.spec.ts @@ -1,17 +1,17 @@ import * as fs from 'fs'; import * as path from 'path'; -import { File, StrykerOptions } from '@stryker-mutator/api/core'; +import { File } from '@stryker-mutator/api/core'; import { commonTokens } from '@stryker-mutator/api/plugin'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { expect } from 'chai'; -import { CONFIG_KEY } from '../../src/helpers/keys'; import TypescriptOptionsEditor from '../../src/TypescriptOptionsEditor'; import TypescriptTranspiler from '../../src/TypescriptTranspiler'; +import { TypescriptWithStrykerOptions } from '../../src/TypescriptWithStrykerOptions'; describe('Use header file integration', () => { - let options: StrykerOptions; + let options: TypescriptWithStrykerOptions; let inputFiles: File[]; let transpiler: TypescriptTranspiler; @@ -20,7 +20,7 @@ describe('Use header file integration', () => { options = factory.strykerOptions(); options.tsconfigFile = path.resolve(__dirname, '..', '..', 'testResources', 'useHeaderFile', 'tsconfig.json'); optionsEditor.edit(options); - inputFiles = options[CONFIG_KEY].fileNames.map((fileName: string) => new File(fileName, fs.readFileSync(fileName, 'utf8'))); + inputFiles = (options.tsconfig!.fileNames as string[]).map((fileName) => new File(fileName, fs.readFileSync(fileName, 'utf8'))); transpiler = testInjector.injector .provideValue(commonTokens.produceSourceMaps, false) .provideValue(commonTokens.options, options) diff --git a/packages/typescript/test/unit/TypescriptOptionsEditor.spec.ts b/packages/typescript/test/unit/TypescriptOptionsEditor.spec.ts index 4c9501b5d0..511aabf052 100644 --- a/packages/typescript/test/unit/TypescriptOptionsEditor.spec.ts +++ b/packages/typescript/test/unit/TypescriptOptionsEditor.spec.ts @@ -6,15 +6,14 @@ import { expect } from 'chai'; import * as ts from 'typescript'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { match, SinonStub } from 'sinon'; -import { StrykerOptions } from '@stryker-mutator/api/core'; -import TypescriptOptionsEditor from './../../src/TypescriptOptionsEditor'; +import { TypescriptWithStrykerOptions } from '../../src/TypescriptWithStrykerOptions'; -const CONFIG_KEY = 'tsconfigFile'; +import TypescriptOptionsEditor from './../../src/TypescriptOptionsEditor'; describe(TypescriptOptionsEditor.name, () => { let readFileSyncStub: SinonStub; - let options: StrykerOptions; + let options: TypescriptWithStrykerOptions; let sut: TypescriptOptionsEditor; beforeEach(() => { @@ -25,12 +24,12 @@ describe(TypescriptOptionsEditor.name, () => { it('should not load any config if "tsconfigFile" is not specified', () => { sut.edit(options); - expect(options[CONFIG_KEY]).undefined; - expect(testInjector.logger.debug).calledWith("No '%s' specified, not loading any config", CONFIG_KEY); + expect(options.tsconfigFile).undefined; + expect(testInjector.logger.debug).calledWith("No '%s' specified, not loading any config", 'tsconfigFile'); }); it('should load the given tsconfig file', () => { - options[CONFIG_KEY] = 'tsconfig.json'; + options.tsconfigFile = 'tsconfig.json'; readFileSyncStub.returns(`{ "compilerOptions": { "module": "commonjs", @@ -48,7 +47,7 @@ describe(TypescriptOptionsEditor.name, () => { }`); sut.edit(options, parseConfigHost()); expect(fs.readFileSync).calledWith(path.resolve('tsconfig.json')); - expect(options.tsconfig.options).include({ + expect(options.tsconfig!.options).include({ configFilePath: path.resolve('tsconfig.json').replace(/\\/g, '/'), module: ts.ModuleKind.CommonJS, noImplicitAny: true, @@ -57,11 +56,11 @@ describe(TypescriptOptionsEditor.name, () => { removeComments: true, sourceMap: true, }); - expect(options.tsconfig.fileNames).deep.eq(['file1.ts', 'file2.ts']); + expect(options.tsconfig!.fileNames).deep.eq(['file1.ts', 'file2.ts']); }); it('should override quality options', () => { - options[CONFIG_KEY] = 'tsconfig.json'; + options.tsconfigFile = 'tsconfig.json'; readFileSyncStub.returns(`{ "compilerOptions": { "allowUnreachableCode": false, @@ -70,7 +69,7 @@ describe(TypescriptOptionsEditor.name, () => { } }`); sut.edit(options, parseConfigHost()); - expect(options.tsconfig.options).include({ + expect(options.tsconfig!.options).include({ allowUnreachableCode: true, noUnusedLocals: false, noUnusedParameters: false, @@ -79,13 +78,13 @@ describe(TypescriptOptionsEditor.name, () => { it('should log errors on failure during load', () => { readFileSyncStub.returns('invalid json'); - options[CONFIG_KEY] = 'tsconfig.json'; + options.tsconfigFile = 'tsconfig.json'; expect(() => sut.edit(options)).throws("error TS1005: '{' expected."); }); it('should log errors on failure during load of extending file', () => { readFileSyncStub.returns('{ "extends": "./parent.tsconfig.json" }'); - options[CONFIG_KEY] = 'tsconfig.json'; + options.tsconfigFile = 'tsconfig.json'; sut.edit(options, parseConfigHost({ readFile: () => 'invalid json' })); expect(testInjector.logger.error).calledWithMatch(match("error TS1005: '{' expected.")); }); diff --git a/packages/typescript/test/unit/TypescriptTranspiler.spec.ts b/packages/typescript/test/unit/TypescriptTranspiler.spec.ts index 72b58f7100..2f0db27f80 100644 --- a/packages/typescript/test/unit/TypescriptTranspiler.spec.ts +++ b/packages/typescript/test/unit/TypescriptTranspiler.spec.ts @@ -10,6 +10,7 @@ import TranspileFilter from '../../src/transpiler/TranspileFilter'; import TranspilingLanguageService, * as transpilingLanguageService from '../../src/transpiler/TranspilingLanguageService'; import { EmitOutput } from '../../src/transpiler/TranspilingLanguageService'; import TypescriptTranspiler from '../../src/TypescriptTranspiler'; +import { TypescriptWithStrykerOptions } from '../../src/TypescriptWithStrykerOptions'; describe(TypescriptTranspiler.name, () => { let languageService: sinon.SinonStubbedInstance; @@ -62,7 +63,7 @@ describe(TypescriptTranspiler.name, () => { expectFilesEqual(outputFiles, [input[0], new File('file2.js', 'file2'), new File('file4.js', 'file4')]); }); - it('should not transpile header files', async () => { + it('should not transform declaration files', async () => { // Arrange const input = [new File('file1.ts', ''), new File('file2.d.ts', '')]; arrangeIncludedFiles(); @@ -77,6 +78,24 @@ describe(TypescriptTranspiler.name, () => { expect(languageService.emit).calledWith('file1.ts'); }); + it("should not transform json files that don't have output", async () => { + // Arrange + (testInjector.options as TypescriptWithStrykerOptions).tsconfig = { + options: { + // no outDir + }, + }; + const input = [new File('file1.json', '')]; + arrangeIncludedFiles(); + + // Act + const outputFiles = await sut.transpile(input); + + // Assert + expectFilesEqual(outputFiles, [new File('file1.json', ''), input[0]]); + expect(languageService.emit).not.called; + }); + it('should remove duplicate files (issue 1318)', async () => { // Arrange const inputFile = new File('file1.ts', 'const foo: number = 42;'); diff --git a/packages/typescript/testResources/allowJsonFiles/index.ts b/packages/typescript/testResources/allowJsonFiles/index.ts new file mode 100644 index 0000000000..940639e41b --- /dev/null +++ b/packages/typescript/testResources/allowJsonFiles/index.ts @@ -0,0 +1,2 @@ +import json from './json.json'; +console.log(json); diff --git a/packages/typescript/testResources/allowJsonFiles/json.json b/packages/typescript/testResources/allowJsonFiles/json.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/typescript/testResources/allowJsonFiles/tsconfig.json b/packages/typescript/testResources/allowJsonFiles/tsconfig.json new file mode 100644 index 0000000000..ac78c58209 --- /dev/null +++ b/packages/typescript/testResources/allowJsonFiles/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "resolveJsonModule": true, + "esModuleInterop": true + }, + "files": [ + "index.ts", + "json.json" + ] +} diff --git a/packages/typescript/tsconfig.src.json b/packages/typescript/tsconfig.src.json index c7fcda103e..35cdab44c7 100644 --- a/packages/typescript/tsconfig.src.json +++ b/packages/typescript/tsconfig.src.json @@ -4,7 +4,9 @@ "rootDir": "." }, "include": [ - "src" + "src", + "src-generated", + "schema/*.json" ], "references": [ { @@ -14,4 +16,4 @@ "path": "../mutator-specification/tsconfig.src.json" } ] -} \ No newline at end of file +} diff --git a/packages/util/src/Immutable.ts b/packages/util/src/Immutable.ts index 4eeec7df9b..dbe7938a8d 100644 --- a/packages/util/src/Immutable.ts +++ b/packages/util/src/Immutable.ts @@ -14,3 +14,29 @@ export type ImmutableArray = ReadonlyArray>; export type ImmutableMap = ReadonlyMap, Immutable>; export type ImmutableSet = ReadonlySet>; export type ImmutableObject = { readonly [K in keyof T]: Immutable }; + +export function deepFreeze(target: T): Immutable { + switch (typeof target) { + case 'object': + if (Array.isArray(target)) { + return Object.freeze(target.map(deepFreeze)) as Immutable; + } + if (target instanceof Map) { + return (Object.freeze(new Map([...target.entries()].map(([k, v]) => [deepFreeze(k), deepFreeze(v)]))) as unknown) as Immutable; + } + if (target === null) { + return null as Immutable; + } + if (target instanceof Set) { + return (Object.freeze(new Set([...target.values()].map(deepFreeze))) as unknown) as Immutable; + } + return Object.freeze({ + ...Object.entries(target).reduce((result, [prop, val]) => { + result[prop] = deepFreeze(val); + return result; + }, {} as any), + }); + default: + return target as Immutable; + } +} diff --git a/packages/util/src/stringUtils.ts b/packages/util/src/stringUtils.ts index 7d30563eb8..02ae727d00 100644 --- a/packages/util/src/stringUtils.ts +++ b/packages/util/src/stringUtils.ts @@ -1,3 +1,5 @@ +import { notEmpty } from './notEmpty'; + /** * Consolidates multiple consecutive white spaces into a single space. * @param str The string to be normalized @@ -5,3 +7,7 @@ export function normalizeWhitespaces(str: string) { return str.replace(/\s+/g, ' ').trim(); } + +export function propertyPath(prop: keyof T, prop2?: keyof T[typeof prop]): string { + return [prop, prop2].filter(notEmpty).join('.'); +} diff --git a/packages/util/test/unit/Immutable.spec.ts b/packages/util/test/unit/Immutable.spec.ts new file mode 100644 index 0000000000..7ab066d625 --- /dev/null +++ b/packages/util/test/unit/Immutable.spec.ts @@ -0,0 +1,105 @@ +import { expect } from 'chai'; + +import { deepFreeze } from '../../src/Immutable'; + +describe(deepFreeze.name, () => { + it('should not change the input object', () => { + const input = {}; + deepFreeze(input); + expect(input).not.frozen; + }); + + it('should freeze objects', () => { + const input = { foo: 'bar', baz: 42 }; + const output = deepFreeze(input); + expect(output).frozen; + expect(output).deep.eq(input); + expect(output).not.eq(input); + }); + + it('should work for `null` and `undefined`', () => { + expect(deepFreeze(null)).eq(null); + expect(deepFreeze(undefined)).eq(undefined); + }); + + it('should work for primitives', () => { + const s = Symbol(); + expect(deepFreeze(42)).eq(42); + expect(deepFreeze('foo')).eq('foo'); + expect(deepFreeze(s)).eq(s); + expect(deepFreeze(true)).eq(true); + }); + + it('should deeply freeze objects', () => { + const input = { + foo: { + bar: { + baz: 'qux', + }, + }, + }; + const output = deepFreeze(input); + expect(output).deep.eq(input); + expect(output).frozen; + expect(output.foo).frozen; + expect(output.foo.bar).frozen; + }); + + it('should work for Arrays', () => { + const one = { + foo: 'bar', + }; + const two = { + baz: 42, + }; + const input = [one, two]; + const output = deepFreeze(input); + expect(output).frozen; + expect(output).instanceOf(Array); + expect(output).lengthOf(2); + expect(output).deep.eq(input); + expect(input).not.frozen; + expect(one).not.frozen; + expect(two).not.frozen; + for (const v of output) { + expect(v).frozen; + } + }); + + it('should work for Maps', () => { + const key = { + foo: 'bar', + }; + const value = { + baz: 42, + }; + const input = new Map([[key, value]]); + const output = deepFreeze(input); + expect(output).frozen; + expect(output).lengthOf(1); + expect(output).deep.eq(input); + expect(input).not.frozen; + expect(key).not.frozen; + expect(value).not.frozen; + for (const [k, v] of output.entries()) { + expect(k).frozen; + expect(v).frozen; + } + }); + + it('should work for Sets', () => { + const value = { + foo: 'bar', + }; + const input = new Set([value]); + const output = deepFreeze(input); + expect(output).frozen; + expect(output).lengthOf(1); + expect(output).deep.eq(input); + expect(input).not.frozen; + expect(value).not.frozen; + for (const v of output.values()) { + expect(v).frozen; + } + }); +}); diff --git a/packages/util/test/unit/stringUtils.spec.ts b/packages/util/test/unit/stringUtils.spec.ts index fe6937e861..cf8082102f 100644 --- a/packages/util/test/unit/stringUtils.spec.ts +++ b/packages/util/test/unit/stringUtils.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { normalizeWhitespaces } from '../../src'; +import { normalizeWhitespaces, propertyPath } from '../../src'; describe('stringUtils', () => { describe(normalizeWhitespaces.name, () => { @@ -16,4 +16,16 @@ describe('stringUtils', () => { expect(normalizeWhitespaces('foo \t \n bar\n\tbaz')).eq('foo bar baz'); }); }); + + describe(propertyPath.name, () => { + interface Foo { + bar: { + baz: string; + }; + } + + it('should be able to point to a path', () => { + expect(propertyPath('bar', 'baz')).eq('bar.baz'); + }); + }); }); diff --git a/packages/wct-runner/src/index.ts b/packages/wct-runner/src/index.ts index 96833d24b5..16b54b400e 100644 --- a/packages/wct-runner/src/index.ts +++ b/packages/wct-runner/src/index.ts @@ -3,3 +3,5 @@ import { declareClassPlugin, PluginKind } from '@stryker-mutator/api/plugin'; import WctTestRunner from './WctTestRunner'; export const strykerPlugins = [declareClassPlugin(PluginKind.TestRunner, 'wct', WctTestRunner)]; + +export * as strykerValidationSchema from '../schema/wct-runner-options.json'; diff --git a/packages/wct-runner/tsconfig.src.json b/packages/wct-runner/tsconfig.src.json index 56ab0fec66..74b0dc5e9f 100644 --- a/packages/wct-runner/tsconfig.src.json +++ b/packages/wct-runner/tsconfig.src.json @@ -11,7 +11,8 @@ }, "include": [ "src", - "typings" + "typings", + "schema/*.json" ], "references": [ { @@ -21,4 +22,4 @@ "path": "../util/tsconfig.src.json" } ] -} \ No newline at end of file +} diff --git a/packages/webpack-transpiler/schema/webpack-transpiler-options.json b/packages/webpack-transpiler/schema/webpack-transpiler-options.json index 09ff629831..c1561e26eb 100644 --- a/packages/webpack-transpiler/schema/webpack-transpiler-options.json +++ b/packages/webpack-transpiler/schema/webpack-transpiler-options.json @@ -13,8 +13,11 @@ "properties": { "configFile": { "description": "Location of your webpack config file", - "type": "string", - "default": "webpack.config.js" + "type": "string" + }, + "context": { + "description": "The [webpack context](https://webpack.js.org/configuration/entry-context/#context)", + "type": "string" }, "silent": { "description": "Specify to remove the \"ProgressPlugin\" from your webpack config file (making the process silent)", diff --git a/packages/webpack-transpiler/src/WebpackTranspiler.ts b/packages/webpack-transpiler/src/WebpackTranspiler.ts index 771c550bef..ce15d4e7b8 100644 --- a/packages/webpack-transpiler/src/WebpackTranspiler.ts +++ b/packages/webpack-transpiler/src/WebpackTranspiler.ts @@ -2,18 +2,15 @@ import { File, StrykerOptions } from '@stryker-mutator/api/core'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { Transpiler } from '@stryker-mutator/api/transpile'; -import { WebpackTranspilerOptions } from '../src-generated/webpack-transpiler-options'; +import { WebpackOptions } from '../src-generated/webpack-transpiler-options'; import ConfigLoader from './compiler/ConfigLoader'; import WebpackCompiler from './compiler/WebpackCompiler'; import { pluginTokens } from './pluginTokens'; - -const DEFAULT_STRYKER_WEBPACK_CONFIG = Object.freeze({ configFile: undefined, silent: true, context: process.cwd() }); - -interface WebpackTranspilerOptionsWithStrykerOptions extends StrykerOptions, WebpackTranspilerOptions {} +import { WebpackTranspilerWithStrykerOptions } from './WebpackTranspilerWithStrykerOptions'; export default class WebpackTranspiler implements Transpiler { - private readonly config: StrykerWebpackConfig; + private readonly config: WebpackOptions; private webpackCompiler: WebpackCompiler; public static inject = tokens(commonTokens.options, commonTokens.produceSourceMaps, pluginTokens.configLoader); @@ -23,7 +20,10 @@ export default class WebpackTranspiler implements Transpiler { `Invalid \`coverageAnalysis\` "${options.coverageAnalysis}" is not supported by the stryker-webpack-transpiler (yet). It is not able to produce source maps yet. Please set it "coverageAnalysis" to "off".` ); } - this.config = this.getStrykerWebpackConfig((options as WebpackTranspilerOptionsWithStrykerOptions).webpack); + this.config = { + context: process.cwd(), + ...((options as unknown) as WebpackTranspilerWithStrykerOptions).webpack, + }; } public async transpile(files: readonly File[]): Promise { @@ -37,16 +37,4 @@ export default class WebpackTranspiler implements Transpiler { const outputFiles = await this.webpackCompiler.emit(); return [...files, ...outputFiles]; } - - private getStrykerWebpackConfig(strykerWebpackConfig?: Partial): StrykerWebpackConfig { - return Object.assign({}, DEFAULT_STRYKER_WEBPACK_CONFIG, strykerWebpackConfig); - } -} - -export interface StrykerWebpackConfig { - configFile?: string; - silent: boolean; - configFileArgs?: any[]; - // TODO: Remove this when stryker implements projectRoot, see https://github.com/stryker-mutator/stryker/issues/650 */ - context?: string; } diff --git a/packages/webpack-transpiler/src/WebpackTranspilerWithStrykerOptions.ts b/packages/webpack-transpiler/src/WebpackTranspilerWithStrykerOptions.ts new file mode 100644 index 0000000000..64e868ee70 --- /dev/null +++ b/packages/webpack-transpiler/src/WebpackTranspilerWithStrykerOptions.ts @@ -0,0 +1,5 @@ +import { StrykerOptions } from '@stryker-mutator/api/core'; + +import { WebpackTranspilerOptions } from '../src-generated/webpack-transpiler-options'; + +export interface WebpackTranspilerWithStrykerOptions extends WebpackTranspilerOptions, StrykerOptions {} diff --git a/packages/webpack-transpiler/src/compiler/ConfigLoader.ts b/packages/webpack-transpiler/src/compiler/ConfigLoader.ts index f1e5115102..e3ae27a692 100644 --- a/packages/webpack-transpiler/src/compiler/ConfigLoader.ts +++ b/packages/webpack-transpiler/src/compiler/ConfigLoader.ts @@ -6,7 +6,7 @@ import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { Configuration } from 'webpack'; import { pluginTokens } from '../pluginTokens'; -import { StrykerWebpackConfig } from '../WebpackTranspiler'; +import { WebpackOptions } from '../../src-generated/webpack-transpiler-options'; import { NodeRequireFunction } from './NodeRequireFunction'; @@ -16,13 +16,13 @@ export default class ConfigLoader { public static inject = tokens(commonTokens.logger, pluginTokens.require); constructor(private readonly log: Logger, private readonly requireFn: NodeRequireFunction) {} - public async load(config: StrykerWebpackConfig): Promise { + public async load(config: WebpackOptions): Promise { let webpackConfig: Configuration; if (config.configFile) { webpackConfig = await this.loadWebpackConfigFromProjectRoot(config.configFile); if (webpackConfig instanceof Function) { - webpackConfig = webpackConfig.apply(null, config.configFileArgs); + webpackConfig = webpackConfig(); } if (config.silent) { this.configureSilent(webpackConfig); diff --git a/packages/webpack-transpiler/src/index.ts b/packages/webpack-transpiler/src/index.ts index be81ed652c..060c8588e6 100644 --- a/packages/webpack-transpiler/src/index.ts +++ b/packages/webpack-transpiler/src/index.ts @@ -10,3 +10,5 @@ function webpackTranspilerFactory(injector: Injector) { return injector.provideValue(pluginTokens.require, require).provideClass(pluginTokens.configLoader, ConfigLoader).injectClass(WebpackTranspiler); } webpackTranspilerFactory.inject = tokens(commonTokens.injector); + +export * as strykerValidationSchema from '../schema/webpack-transpiler-options.json'; diff --git a/packages/webpack-transpiler/test/helpers/producers.ts b/packages/webpack-transpiler/test/helpers/producers.ts index 7b5cf92b1d..d505f538af 100644 --- a/packages/webpack-transpiler/test/helpers/producers.ts +++ b/packages/webpack-transpiler/test/helpers/producers.ts @@ -2,7 +2,7 @@ import { File } from '@stryker-mutator/api/core'; import * as sinon from 'sinon'; import { Configuration } from 'webpack'; -import { StrykerWebpackConfig } from '../../src/WebpackTranspiler'; +import { WebpackOptions } from '../../src-generated/webpack-transpiler-options'; import { WebpackCompilerMock } from './mockInterfaces'; @@ -32,8 +32,7 @@ function createFactory(defaultFn: () => T): (overrides?: Partial) => T { return (overrides) => Object.assign(defaultFn(), overrides); } -export const createStrykerWebpackConfig = createFactory(() => ({ - configFile: undefined, +export const createWebpackOptions = createFactory(() => ({ context: '/path/to/project/root', silent: true, })); diff --git a/packages/webpack-transpiler/test/integration/transpiler.it.spec.ts b/packages/webpack-transpiler/test/integration/transpiler.it.spec.ts index 82b3d7997c..429a575668 100644 --- a/packages/webpack-transpiler/test/integration/transpiler.it.spec.ts +++ b/packages/webpack-transpiler/test/integration/transpiler.it.spec.ts @@ -9,14 +9,21 @@ import { expect } from 'chai'; import ConfigLoader from '../../src/compiler/ConfigLoader'; import { pluginTokens } from '../../src/pluginTokens'; import WebpackTranspiler from '../../src/WebpackTranspiler'; +import { WebpackTranspilerWithStrykerOptions } from '../../src/WebpackTranspilerWithStrykerOptions'; describe('Webpack transpiler', () => { + let options: WebpackTranspilerWithStrykerOptions; + beforeEach(() => { - testInjector.options.webpack = {}; + options = (testInjector.options as unknown) as WebpackTranspilerWithStrykerOptions; + options.webpack = { + configFile: 'webpack.conf.js', + silent: true, + }; }); it('should be able to transpile the "gettingStarted" sample', async () => { - testInjector.options.webpack.configFile = path.join(getProjectRoot('gettingStarted'), 'webpack.config.js'); + options.webpack.configFile = path.join(getProjectRoot('gettingStarted'), 'webpack.config.js'); const sut = createSut(); const files = readFiles(); @@ -27,7 +34,8 @@ describe('Webpack transpiler', () => { }); it('should be able to transpile "zeroConfig" sample without a Webpack config file', async () => { - testInjector.options.webpack.context = getProjectRoot('zeroConfig'); + options.webpack.context = getProjectRoot('zeroConfig'); + delete options.webpack.configFile; const sut = createSut(); const files = readFiles(); @@ -45,7 +53,7 @@ function createSut() { } function getProjectRoot(testResourceProjectName: string) { - return path.join(process.cwd(), 'testResources', testResourceProjectName); + return path.resolve(__dirname, '..', '..', 'testResources', testResourceProjectName); } function readFiles(): File[] { diff --git a/packages/webpack-transpiler/test/unit/WebpackTranspiler.spec.ts b/packages/webpack-transpiler/test/unit/WebpackTranspiler.spec.ts index 99ad2cc4f9..fbf8ae455f 100644 --- a/packages/webpack-transpiler/test/unit/WebpackTranspiler.spec.ts +++ b/packages/webpack-transpiler/test/unit/WebpackTranspiler.spec.ts @@ -8,7 +8,7 @@ import { Configuration } from 'webpack'; import ConfigLoader from '../../src/compiler/ConfigLoader'; import WebpackCompiler, * as webpackCompilerModule from '../../src/compiler/WebpackCompiler'; import WebpackTranspiler from '../../src/WebpackTranspiler'; -import { createMockInstance, createStrykerWebpackConfig, createTextFile, Mock } from '../helpers/producers'; +import { createMockInstance, createTextFile, Mock, createWebpackOptions } from '../helpers/producers'; describe('WebpackTranspiler', () => { let webpackTranspiler: WebpackTranspiler; @@ -30,7 +30,7 @@ describe('WebpackTranspiler', () => { sinon.stub(webpackCompilerModule, 'default').returns(webpackCompilerStub); - testInjector.options.webpack = { context: '/path/to/project/root' }; + testInjector.options.webpack = createWebpackOptions({ context: '/path/to/project/root' }); }); it('should only create the compiler once', async () => { @@ -41,7 +41,7 @@ describe('WebpackTranspiler', () => { expect(webpackCompilerModule.default).calledOnce; expect(webpackCompilerModule.default).calledWithNew; expect(configLoaderStub.load).calledOnce; - expect(configLoaderStub.load).calledWith(createStrykerWebpackConfig()); + expect(configLoaderStub.load).calledWith(createWebpackOptions()); }); it('should throw an error if `produceSourceMaps` is `true`', () => { diff --git a/packages/webpack-transpiler/test/unit/compiler/ConfigLoader.spec.ts b/packages/webpack-transpiler/test/unit/compiler/ConfigLoader.spec.ts index 5f8d9d022e..506f665d55 100644 --- a/packages/webpack-transpiler/test/unit/compiler/ConfigLoader.spec.ts +++ b/packages/webpack-transpiler/test/unit/compiler/ConfigLoader.spec.ts @@ -8,7 +8,7 @@ import { Configuration, Plugin } from 'webpack'; import ConfigLoader from '../../../src/compiler/ConfigLoader'; import { pluginTokens } from '../../../src/pluginTokens'; -import { createStrykerWebpackConfig } from '../../helpers/producers'; +import { createWebpackOptions } from '../../helpers/producers'; class FooPlugin implements Plugin { public foo = true; @@ -38,21 +38,21 @@ describe('ConfigLoader', () => { requireStub.returns('resolved'); existsSyncStub.returns(true); - const result = await sut.load(createStrykerWebpackConfig({ configFile: 'webpack.foo.config.js' })); + const result = await sut.load(createWebpackOptions({ configFile: 'webpack.foo.config.js' })); expect(result).eq('resolved'); expect(requireStub).calledWith(path.resolve('webpack.foo.config.js')); }); - it('should call function with configFileArgs if webpack config file exports a function', async () => { + it('should call function if webpack config file exports a function', async () => { const configFunctionStub = sinon.stub(); configFunctionStub.returns('webpackconfig'); requireStub.returns(configFunctionStub); existsSyncStub.returns(true); - const result = await sut.load(createStrykerWebpackConfig({ configFile: 'webpack.foo.config.js', configFileArgs: [1, 2] })); + const result = await sut.load(createWebpackOptions({ configFile: 'webpack.foo.config.js' })); expect(result).eq('webpackconfig'); expect(requireStub).calledWith(path.resolve('webpack.foo.config.js')); - expect(configFunctionStub).calledWith(1, 2); + expect(configFunctionStub).called; }); it('should remove "ProgressPlugin" if silent is `true`', async () => { @@ -66,7 +66,7 @@ describe('ConfigLoader', () => { existsSyncStub.returns(true); // Act - const result = await sut.load(createStrykerWebpackConfig({ configFile: 'webpack.config.js', silent: true })); + const result = await sut.load(createWebpackOptions({ configFile: 'webpack.config.js', silent: true })); // Assert expect(result.plugins).to.be.an('array').that.does.not.deep.include(new ProgressPlugin()); @@ -85,7 +85,7 @@ describe('ConfigLoader', () => { requireStub.returns(webpackConfig); existsSyncStub.returns(true); - const result = await sut.load(createStrykerWebpackConfig({ configFile: 'webpack.config.js', silent: false })); + const result = await sut.load(createWebpackOptions({ configFile: 'webpack.config.js', silent: false })); expect(result.plugins).to.be.an('array').that.does.deep.include(new ProgressPlugin()); }); @@ -94,7 +94,7 @@ describe('ConfigLoader', () => { existsSyncStub.returns(false); - const result = await sut.load(createStrykerWebpackConfig({ context: contextPath })); + const result = await sut.load(createWebpackOptions({ context: contextPath })); expect(result).to.deep.equal({ context: contextPath }); }); @@ -104,7 +104,7 @@ describe('ConfigLoader', () => { existsSyncStub.returns(false); - return expect(sut.load(createStrykerWebpackConfig({ configFile }))).rejectedWith( + return expect(sut.load(createWebpackOptions({ configFile }))).rejectedWith( `Could not load webpack config at "${path.resolve(configFile)}", file not found.` ); }); @@ -114,7 +114,7 @@ describe('ConfigLoader', () => { existsSyncStub.returns(false); - await sut.load(createStrykerWebpackConfig({ context: contextPath })); + await sut.load(createWebpackOptions({ context: contextPath })); expect(testInjector.logger.debug).calledWith('Webpack config "%s" not found, trying Webpack 4 zero config'); }); @@ -123,7 +123,7 @@ describe('ConfigLoader', () => { requireStub.returns(Promise.resolve('resolved')); existsSyncStub.returns(true); - const result = await sut.load(createStrykerWebpackConfig({ configFile: 'webpack.foo.config.js' })); + const result = await sut.load(createWebpackOptions({ configFile: 'webpack.foo.config.js' })); expect(result).eq('resolved'); expect(requireStub).calledWith(path.resolve('webpack.foo.config.js')); diff --git a/packages/webpack-transpiler/tsconfig.src.json b/packages/webpack-transpiler/tsconfig.src.json index c6914ce5df..edc314e5f6 100644 --- a/packages/webpack-transpiler/tsconfig.src.json +++ b/packages/webpack-transpiler/tsconfig.src.json @@ -13,7 +13,8 @@ "include": [ "src", "src-generated", - "typings" + "typings", + "schema/*.json" ], "references": [ { diff --git a/stryker.parent.conf.js b/stryker.parent.conf.js index 8b2a94a06b..32e1445019 100644 --- a/stryker.parent.conf.js +++ b/stryker.parent.conf.js @@ -20,6 +20,11 @@ module.exports = { ], dashboard: { reportType: 'full' - } + }, + files: [ + '{src,test,src-generated}/**/*.ts', + '!{src,test,src-generated}/**/*.d.ts', + 'schema/**/*.json' + ] }; diff --git a/tasks/generate-json-schema-to-ts.js b/tasks/generate-json-schema-to-ts.js index 5d9fe0f7db..379b341ea0 100644 --- a/tasks/generate-json-schema-to-ts.js +++ b/tasks/generate-json-schema-to-ts.js @@ -9,6 +9,15 @@ const mkdir = promisify(fs.mkdir); const resolveFromParent = path.resolve.bind(path, __dirname, '..'); const globAsPromised = promisify(glob); +const noNetworkHttpResolver = { + order: 1, + canRead: /^http:/i, + + read(_file, callback) { + callback(undefined, {}); + } +} + /** * * @param {string} schemaFile @@ -19,16 +28,23 @@ async function generate(schemaFile) { const resolveOutputDir = path.resolve.bind(path, parsedFile.dir, '..', 'src-generated'); const outFile = resolveOutputDir(`${parsedFile.name}.ts`); await mkdir(resolveOutputDir(), { recursive: true }); - let ts = await compile(schema, parsedFile.base, { + let ts = (await compile(schema, parsedFile.base, { style: { singleQuote: true, }, + $refOptions: { + resolve: { + http: false, + // @ts-ignore + noNetworkHttpResolver + } + }, bannerComment: `/** * This file was automatically generated by ${path.basename(__filename)}. * DO NOT MODIFY IT BY HAND. Instead, modify the source file JSON file: ${path.basename(schemaFile)}, * and run 'npm run generate' from monorepo base directory. */` - }); + })).replace(/\[k: string\]: any;/g, '[k: string]: unknown;'); await writeFile(outFile, ts, 'utf8'); console.info(`✅ ${path.relative(path.resolve(__dirname, '..'), path.resolve(__dirname, schemaFile))} -> ${path.relative(path.resolve(__dirname, '..'), resolveFromParent(outFile))}`); } @@ -42,7 +58,6 @@ generateAllSchemas().catch(err => { process.exitCode = 1; }); - function preprocessSchema(inputSchema) { try { switch (inputSchema.type) {