UNPKG

@stryker-mutator/vitest-runner

Version:

A plugin to use the vitest test runner and framework in Stryker, the JavaScript mutation testing framework

187 lines 8.13 kB
import { fileURLToPath } from 'url'; import path from 'path'; import fs from 'fs'; import { INSTRUMENTER_CONSTANTS } from '@stryker-mutator/api/core'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { DryRunStatus, toMutantRunResult, determineHitLimitReached, TestStatus, } from '@stryker-mutator/api/test-runner'; import { escapeRegExp, notEmpty } from '@stryker-mutator/util'; import { vitestWrapper } from './vitest-wrapper.js'; import { convertTestToTestResult, fromTestId, collectTestsFromSuite, normalizeCoverage } from './vitest-helpers.js'; const STRYKER_SETUP = fileURLToPath(new URL('./stryker-setup.js', import.meta.url)); export class VitestTestRunner { log; globalNamespace; static inject = [commonTokens.options, commonTokens.logger, 'globalNamespace']; ctx; options; localSetupFile = path.resolve(`./stryker-setup-${process.env.STRYKER_MUTATOR_WORKER_ID ?? 0}.js`); constructor(options, log, globalNamespace) { this.log = log; this.globalNamespace = globalNamespace; this.options = options; } capabilities() { return { reloadEnvironment: true }; } async init() { this.setEnv(); await fs.promises.copyFile(STRYKER_SETUP, this.localSetupFile); this.ctx = await vitestWrapper.createVitest('test', { config: this.options.vitest?.configFile, // @ts-expect-error threads got renamed to "pool: threads" in vitest 1.0.0 threads: true, pool: 'threads', coverage: { enabled: false }, poolOptions: { // Since vitest 1.0.0 threads: { maxThreads: 1, minThreads: 1, }, }, singleThread: false, maxConcurrency: 1, watch: false, dir: this.options.vitest.dir, bail: this.options.disableBail ? 0 : 1, onConsoleLog: () => false, }); this.ctx.provide('globalNamespace', this.globalNamespace); this.ctx.config.browser.screenshotFailures = false; this.ctx.projects.forEach((project) => { project.config.setupFiles = [this.localSetupFile, ...project.config.setupFiles]; project.config.browser.screenshotFailures = false; }); if (this.log.isDebugEnabled()) { this.log.debug(`vitest final config: ${JSON.stringify(this.ctx.config, null, 2)}`); } } async dryRun() { this.ctx.provide('mode', 'dry-run'); const testResult = await this.run(); const mutantCoverage = this.readMutantCoverage(); if (testResult.status === DryRunStatus.Complete) { return { status: testResult.status, tests: testResult.tests, mutantCoverage, }; } return testResult; } async mutantRun(options) { this.ctx.provide('mode', 'mutant'); this.ctx.provide('hitLimit', options.hitLimit); this.ctx.provide('mutantActivation', options.mutantActivation); this.ctx.provide('activeMutant', options.activeMutant.id); const dryRunResult = await this.run(options.testFilter); const hitCount = this.readHitCount(); const timeOut = determineHitLimitReached(hitCount, options.hitLimit); return toMutantRunResult(timeOut ?? dryRunResult); } async run(testIds = []) { this.resetContext(); if (testIds.length > 0) { const regexTestNameFilter = testIds .map(fromTestId) .map(({ test: name }) => escapeRegExp(name)) .join('|'); const regex = new RegExp(regexTestNameFilter); const testFiles = testIds.map(fromTestId).map(({ file }) => file); this.ctx.projects.forEach((project) => { project.config.testNamePattern = regex; }); await this.ctx.start(testFiles); } else { this.ctx.projects.forEach((project) => { project.config.testNamePattern = undefined; }); await this.ctx.start(); } const tests = this.ctx.state.getFiles() .flatMap((file) => collectTestsFromSuite(file)) .filter((test) => test.result); // if no result: it was skipped because of bail let failure = false; const testResults = tests.map((test) => { const testResult = convertTestToTestResult(test); failure ||= testResult.status === TestStatus.Failed; return testResult; }); if (!failure && this.ctx.state.errorsSet.size > 0) { const errorText = [...this.ctx.state.errorsSet].map((val) => JSON.stringify(val)).join('\n'); return { status: DryRunStatus.Error, errorMessage: `An error occurred outside of a test run, please be sure to properly await your promises! ${errorText}`, }; } return { tests: testResults, status: DryRunStatus.Complete }; } setEnv() { // Set node environment for issues like these: https://github.com/stryker-mutator/stryker-js/issues/4289 process.env.NODE_ENV = 'test'; // Set vitest environment to signal that we are running in vitest // as some plugins only initiate when this is set: https://github.com/testing-library/svelte-testing-library/blob/6096f05e805cf55474f52f303562f4013785d25f/src/vite.js#L20 process.env.VITEST = '1'; } resetContext() { // Clear the state from the previous run // Note that this is kind of a hack, see https://github.com/vitest-dev/vitest/discussions/3017#discussioncomment-5901751 this.ctx.state.filesMap.clear(); } readHitCount() { const hitCounters = this.ctx.state.getFiles() .map((file) => file.meta.hitCount) .filter(notEmpty); return hitCounters.reduce((acc, hitCount) => acc + hitCount, 0); } readMutantCoverage() { // Read coverage from all projects const coverages = [ ...new Map(this.ctx.state.getFiles().map((file) => [`${file.projectName}-${file.name}`, file])).entries(), ] .map(([, file]) => file.meta.mutantCoverage) .filter(notEmpty) .map(normalizeCoverage); if (coverages.length > 1) { return coverages.reduce((acc, projectCoverage) => { // perTest contains the coverage per test id Object.entries(projectCoverage.perTest).forEach(([testId, testCoverage]) => { if (testId in acc.perTest) { // Keys are mutant ids, the numbers are the amount of times it was hit. mergeCoverage(acc.perTest[testId], testCoverage); } else { acc.perTest[testId] = testCoverage; } }); mergeCoverage(acc.static, projectCoverage.static); return acc; }); } return coverages[0]; function mergeCoverage(to, from) { Object.entries(from).forEach(([mutantId, hitCount]) => { if (mutantId in to) { to[mutantId] += hitCount; } else { to[mutantId] = hitCount; } }); } } async dispose() { await this.ctx?.close(); await fs.promises.rm(this.localSetupFile, { force: true }); } } export const vitestTestRunnerFactory = createVitestTestRunnerFactory(); export function createVitestTestRunnerFactory(namespace = INSTRUMENTER_CONSTANTS.NAMESPACE) { createVitestTestRunner.inject = tokens(commonTokens.injector); function createVitestTestRunner(injector) { return injector.provideValue('globalNamespace', namespace).injectClass(VitestTestRunner); } return createVitestTestRunner; } //# sourceMappingURL=vitest-test-runner.js.map