UNPKG

@stryker-mutator/core

Version:

The extendable JavaScript mutation testing framework

213 lines 11.7 kB
import os from 'os'; import path from 'path'; import { Minimatch } from 'minimatch'; import ajvModule from 'ajv'; import { strykerCoreSchema } from '@stryker-mutator/api/core'; import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; import { noopLogger, findUnserializables, deepFreeze, } from '@stryker-mutator/util'; import { coreTokens } from '../di/index.js'; import { ConfigError } from '../errors.js'; import { objectUtils, optionsPath } from '../utils/index.js'; import { CommandTestRunner } from '../test-runner/command-test-runner.js'; import { IGNORE_PATTERN_CHARACTER, MUTATION_RANGE_REGEX } from '../fs/index.js'; import { describeErrors } from './validation-errors.js'; const Ajv = ajvModule.default; const ajv = new Ajv({ useDefaults: true, allErrors: true, jsPropertySyntax: true, verbose: true, logger: false, strict: false, }); export class OptionsValidator { schema; log; validateFn; static inject = tokens(coreTokens.validationSchema, commonTokens.logger); constructor(schema, log) { this.schema = schema; this.log = log; this.validateFn = ajv.compile(schema); } /** * Validates the provided options, throwing an error if something is wrong. * Optionally also warns about excess or unserializable options. * @param options The stryker options to validate * @param mark Wether or not to log warnings on unknown properties or unserializable properties */ validate(options, mark = false) { this.removeDeprecatedOptions(options); this.schemaValidate(options); this.customValidation(options); if (mark) { this.markOptions(options); } } removeDeprecatedOptions(rawOptions) { if (typeof rawOptions.mutator === 'string') { this.log.warn('DEPRECATED. Use of "mutator" as string is no longer needed. You can remove it from your configuration. Stryker now supports mutating of JavaScript and friend files out of the box.'); delete rawOptions.mutator; } // @ts-expect-error mutator.name if (typeof rawOptions.mutator === 'object' && rawOptions.mutator.name) { this.log.warn('DEPRECATED. Use of "mutator.name" is no longer needed. You can remove "mutator.name" from your configuration. Stryker now supports mutating of JavaScript and friend files out of the box.'); // @ts-expect-error mutator.name delete rawOptions.mutator.name; } if (Object.keys(rawOptions).includes('testFramework')) { this.log.warn('DEPRECATED. Use of "testFramework" is no longer needed. You can remove it from your configuration. Your test runner plugin now handles its own test framework integration.'); delete rawOptions.testFramework; } if (Array.isArray(rawOptions.transpilers)) { const example = rawOptions.transpilers.includes('babel') ? 'babel src --out-dir lib' : rawOptions.transpilers.includes('typescript') ? 'tsc -b' : rawOptions.transpilers.includes('webpack') ? 'webpack --config webpack.config.js' : 'npm run build'; this.log.warn(`DEPRECATED. Support for "transpilers" is removed. You can now configure your own "${optionsPath('buildCommand')}". For example, ${example}.`); delete rawOptions.transpilers; } if (Array.isArray(rawOptions.files)) { const ignorePatternsName = optionsPath('ignorePatterns'); const isString = (uncertain) => typeof uncertain === 'string'; const files = rawOptions.files.filter(isString); const newIgnorePatterns = [ '**', ...files.map((filePattern) => filePattern.startsWith(IGNORE_PATTERN_CHARACTER) ? filePattern.substr(1) : `${IGNORE_PATTERN_CHARACTER}${filePattern}`), ]; delete rawOptions.files; this.log.warn(`DEPRECATED. Use of "files" is deprecated, please use "${ignorePatternsName}" instead (or remove "files" altogether will probably work as well). For now, rewriting them as ${JSON.stringify(newIgnorePatterns)}. See https://stryker-mutator.io/docs/stryker-js/configuration/#ignorepatterns-string`); const existingIgnorePatterns = Array.isArray(rawOptions[ignorePatternsName]) ? rawOptions[ignorePatternsName] : []; rawOptions[ignorePatternsName] = [ ...newIgnorePatterns, ...existingIgnorePatterns, ]; } // @ts-expect-error jest.enableBail if (rawOptions.jest?.enableBail !== undefined) { this.log.warn('DEPRECATED. Use of "jest.enableBail" is deprecated, please use "disableBail" instead. See https://stryker-mutator.io/docs/stryker-js/configuration#disablebail-boolean'); // @ts-expect-error jest.enableBail rawOptions.disableBail = !rawOptions.jest?.enableBail; // @ts-expect-error jest.enableBail delete rawOptions.jest.enableBail; } // @ts-expect-error htmlReporter.baseDir if (rawOptions.htmlReporter?.baseDir) { this.log.warn(`DEPRECATED. Use of "htmlReporter.baseDir" is deprecated, please use "${optionsPath('htmlReporter', 'fileName')}" instead. See https://stryker-mutator.io/docs/stryker-js/configuration/#reporters-string`); // @ts-expect-error htmlReporter.baseDir if (!rawOptions.htmlReporter.fileName) { // @ts-expect-error htmlReporter.fileName rawOptions.htmlReporter.fileName = path.join( // @ts-expect-error htmlReporter.baseDir String(rawOptions.htmlReporter.baseDir), 'index.html'); } // @ts-expect-error htmlReporter.baseDir delete rawOptions.htmlReporter.baseDir; } } customValidation(options) { const additionalErrors = []; if (options.thresholds.high < options.thresholds.low) { additionalErrors.push('Config option "thresholds.high" should be higher than "thresholds.low".'); } if (options.maxConcurrentTestRunners !== Number.MAX_SAFE_INTEGER) { this.log.warn('DEPRECATED. Use of "maxConcurrentTestRunners" is deprecated. Please use "concurrency" instead.'); if (!options.concurrency && options.maxConcurrentTestRunners < os.cpus().length - 1) { options.concurrency = options.maxConcurrentTestRunners; } } if (CommandTestRunner.is(options.testRunner)) { if (options.testRunnerNodeArgs.length) { this.log.warn('Using "testRunnerNodeArgs" together with the "command" test runner is not supported, these arguments will be ignored. You can add your custom arguments by setting the "commandRunner.command" option.'); } } if (options.ignoreStatic && options.coverageAnalysis !== 'perTest') { additionalErrors.push(`Config option "${optionsPath('ignoreStatic')}" is not supported with coverage analysis "${options.coverageAnalysis}". Either turn off "${optionsPath('ignoreStatic')}", or configure "${optionsPath('coverageAnalysis')}" to be "perTest".`); } options.mutate.forEach((mutateString, index) => { const match = MUTATION_RANGE_REGEX.exec(mutateString); if (match) { if (new Minimatch(mutateString).hasMagic()) { additionalErrors.push(`Config option "mutate[${index}]" is invalid. Cannot combine a glob expression with a mutation range in "${mutateString}".`); } else { const [_, _fileName, mutationRange, startLine, _startColumn, endLine, _endColumn,] = match; const start = parseInt(startLine, 10); const end = parseInt(endLine, 10); if (start < 1) { additionalErrors.push(`Config option "mutate[${index}]" is invalid. Mutation range "${mutationRange}" is invalid, line ${start} does not exist (lines start at 1).`); } if (start > end) { additionalErrors.push(`Config option "mutate[${index}]" is invalid. Mutation range "${mutationRange}" is invalid. The "from" line number (${start}) should be less then the "to" line number (${end}).`); } } } }); additionalErrors.forEach((error) => this.log.error(error)); this.throwErrorIfNeeded(additionalErrors); } schemaValidate(options) { if (!this.validateFn(options)) { const describedErrors = describeErrors(this.validateFn.errors); describedErrors.forEach((error) => this.log.error(error)); this.throwErrorIfNeeded(describedErrors); } } throwErrorIfNeeded(errors) { if (errors.length > 0) { throw new ConfigError(errors.length === 1 ? 'Please correct this configuration error and try again.' : 'Please correct these configuration errors and try again.'); } } markOptions(options) { this.markExcessOptions(options); this.markUnserializableOptions(options); } markExcessOptions(options) { const OPTIONS_ADDED_BY_STRYKER = ['set', 'configFile', '$schema']; if (objectUtils.isWarningEnabled('unknownOptions', options.warnings)) { const schemaKeys = Object.keys(this.schema.properties); const excessPropertyNames = Object.keys(options) .filter((key) => !key.endsWith('_comment')) .filter((key) => !OPTIONS_ADDED_BY_STRYKER.includes(key)) .filter((key) => !schemaKeys.includes(key)); if (excessPropertyNames.length) { excessPropertyNames.forEach((excessPropertyName) => { this.log.warn(`Unknown stryker config option "${excessPropertyName}".`); }); this.log.warn(`Possible causes: * Is it a typo on your end? * Did you only write this property as a comment? If so, please postfix it with "_comment". * You might be missing a plugin that is supposed to use it. Stryker loaded plugins from: ${JSON.stringify(options.plugins)} * The plugin that is using it did not contribute explicit validation. (disable "${optionsPath('warnings', 'unknownOptions')}" to ignore this warning)`); } } } markUnserializableOptions(options) { if (objectUtils.isWarningEnabled('unserializableOptions', options.warnings)) { const unserializables = findUnserializables(options); if (unserializables) { unserializables.forEach((unserializable) => this.log.warn(`Config option "${unserializable.path.join('.')}" is not (fully) serializable. ${unserializable.reason}. Any test runner or checker worker processes might not receive this value as intended.`)); this.log.warn(`(disable ${optionsPath('warnings', 'unserializableOptions')} to ignore this warning)`); } } } } export function createDefaultOptions() { const options = {}; const validator = new OptionsValidator(strykerCoreSchema, noopLogger); validator.validate(options); return options; } export const defaultOptions = deepFreeze(createDefaultOptions()); //# sourceMappingURL=options-validator.js.map