import fs from 'fs'; import path from 'path'; import { Logger } from '@stryker-mutator/api/logging'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { propertyPath } from '@stryker-mutator/util'; import { MochaOptions, MochaRunnerOptions } from '../src-generated/mocha-runner-options.js'; import { LibWrapper } from './lib-wrapper.js'; import { filterConfig, serializeMochaLoadOptionsArguments } from './utils.js'; import { MochaRunnerWithStrykerOptions } from './mocha-runner-with-stryker-options.js'; /** * Subset of defaults for mocha options * @see https://github.com/mochajs/mocha/blob/master/lib/mocharc.json */ export const DEFAULT_MOCHA_OPTIONS: Readonly = Object.freeze({ extension: ['js'], require: [], file: [], ignore: [], opts: './test/mocha.opts', spec: ['test'], ui: 'bdd', 'no-package': false, 'no-opts': false, 'no-config': false, 'async-only': false, }); export class MochaOptionsLoader { public static inject = tokens(commonTokens.logger); constructor(private readonly log: Logger) {} public load(strykerOptions: MochaRunnerWithStrykerOptions): MochaOptions { const mochaOptions = { ...strykerOptions.mochaOptions } as MochaOptions; const options = { ...DEFAULT_MOCHA_OPTIONS, ...this.loadMochaOptions(mochaOptions), ...mochaOptions }; if (this.log.isDebugEnabled()) { this.log.debug(`Loaded options: ${JSON.stringify(options, null, 2)}`); } return options; } private loadMochaOptions(overrides: MochaOptions) { if (LibWrapper.loadOptions) { this.log.debug("Mocha >= 6 detected. Using mocha's `%s` to load mocha options", LibWrapper.loadOptions.name); return this.loadMocha6Options(overrides); } else { this.log.warn('DEPRECATED: Mocha < 6 detected. Please upgrade to at least Mocha version 6. Stryker will drop support for Mocha < 6 in V5.'); this.log.debug('Mocha < 6 detected. Using custom logic to parse mocha options'); return this.loadLegacyMochaOptsFile(overrides); } } private loadMocha6Options(overrides: MochaOptions) { const args = serializeMochaLoadOptionsArguments(overrides); const rawConfig = LibWrapper.loadOptions(args) ?? {}; if (this.log.isTraceEnabled()) { this.log.trace(`Mocha: ${LibWrapper.loadOptions.name}([${args.map((arg) => `'${arg}'`).join(',')}]) => ${JSON.stringify(rawConfig)}`); } const options = filterConfig(rawConfig); return options; } 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!); if (fs.existsSync(defaultMochaOptsFileName)) { return this.readMochaOptsFile(defaultMochaOptsFileName); } else { 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(options.opts); if (fs.existsSync(optsFileName)) { return this.readMochaOptsFile(optsFileName); } else { this.log.error(`Could not load opts from "${optsFileName}". Please make sure opts file exists.`); return {}; } default: return {}; } } private readMochaOptsFile(optsFileName: string) { this.log.info(`Loading mochaOpts from "${optsFileName}"`); return this.parseOptsFile(fs.readFileSync(optsFileName, 'utf8')); } private parseOptsFile(optsFileContent: string): MochaOptions { const options = optsFileContent.split('\n').map((val) => val.trim()); const mochaRunnerOptions: MochaOptions = Object.create(null); options.forEach((option) => { const args = option.split(' ').filter(Boolean); if (args[0]) { switch (args[0]) { case '--require': case '-r': args.shift(); if (!mochaRunnerOptions.require) { mochaRunnerOptions.require = []; } mochaRunnerOptions.require.push(...args); break; case '--async-only': case '-A': mochaRunnerOptions['async-only'] = true; break; case '--ui': case '-u': mochaRunnerOptions.ui = (this.parseNextString(args) as 'bdd' | 'exports' | 'qunit' | 'tdd') ?? DEFAULT_MOCHA_OPTIONS.ui!; break; case '--grep': case '-g': let arg = `${this.parseNextString(args)}`; if (arg.startsWith('/') && arg.endsWith('/')) { arg = arg.substring(1, arg.length - 1); } mochaRunnerOptions.grep = arg; break; default: this.log.debug(`Ignoring option "${args[0]}" as it is not supported.`); break; } } }); return mochaRunnerOptions; } private parseNextString(args: string[]): string | undefined { if (args.length > 1) { return args[1]; } else { return undefined; } } }