UNPKG

@stryker-mutator/tap-runner

Version:

A plugin to use the TAP (test anything protocol) test runner in Stryker, the JavaScript mutation testing framework

150 lines 5.99 kB
import childProcess from 'child_process'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs/promises'; import { commonTokens, tokens, } from '@stryker-mutator/api/plugin'; import { determineHitLimitReached, DryRunStatus, TestStatus, toMutantRunResult, } from '@stryker-mutator/api/test-runner'; import { INSTRUMENTER_CONSTANTS, } from '@stryker-mutator/api/core'; import { normalizeFileName, testFilesProvided } from '@stryker-mutator/util'; import * as pluginTokens from './plugin-tokens.js'; import { findTestyLookingFiles, captureTapResult, buildArguments, } from './tap-helper.js'; import { strykerHitLimit, strykerNamespace, strykerDryRun, tempTapOutputFileName, } from './setup/env.cjs'; export function createTapTestRunnerFactory(namespace = INSTRUMENTER_CONSTANTS.NAMESPACE) { createTapTestRunner.inject = tokens(commonTokens.injector); function createTapTestRunner(injector) { return injector .provideValue(pluginTokens.globalNamespace, namespace) .injectClass(TapTestRunner); } return createTapTestRunner; } export const createTapTestRunner = createTapTestRunnerFactory(); class HitLimitError extends Error { result; constructor(result) { super(result.reason); this.result = result; } } export class TapTestRunner { log; globalNamespace; static inject = tokens(commonTokens.options, commonTokens.logger, pluginTokens.globalNamespace); testFiles = []; static hookFile = normalizeFileName(path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'setup', 'hook.cjs')); options; constructor(options, log, globalNamespace) { this.log = log; this.globalNamespace = globalNamespace; this.options = options; } capabilities() { return { reloadEnvironment: true }; } async init() { this.testFiles = await findTestyLookingFiles(this.options.tap.testFiles); } async dryRun(options) { const testFilesToRun = testFilesProvided(options) ? options.testFiles : this.testFiles; return this.run({ disableBail: options.disableBail, dryRun: true }, testFilesToRun); } async mutantRun(options) { return toMutantRunResult(await this.run({ disableBail: options.disableBail, activeMutant: options.activeMutant.id, hitLimit: options.hitLimit, }, this.testFiles, options.testFilter)); } async run(testOptions, testFilesToRun, testFilter) { const testFiles = testFilter ?? testFilesToRun; const runs = []; const totalCoverage = { static: {}, perTest: {}, }; for (const testFile of testFiles) { try { const { testResult, coverage } = await this.runFile(testFile, testOptions); runs.push(testResult); totalCoverage.perTest[testFile] = coverage?.static ?? {}; if (testResult.status !== TestStatus.Success && !testOptions.disableBail) { break; } } catch (err) { if (err instanceof HitLimitError) { return err.result; } else if (err instanceof Error) { return { status: DryRunStatus.Error, errorMessage: `Error running file "${testFile}". ${err.message}`, }; } throw err; } } return { status: DryRunStatus.Complete, tests: runs, mutantCoverage: totalCoverage, }; } async runFile(testFile, testOptions) { const env = { ...process.env, [strykerHitLimit]: testOptions.hitLimit?.toString(), [strykerNamespace]: this.globalNamespace, [INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE]: testOptions.activeMutant, [strykerDryRun]: testOptions.dryRun?.toString(), }; const args = buildArguments(this.options.tap.nodeArgs, TapTestRunner.hookFile, testFile); if (this.log.isDebugEnabled()) { this.log.debug(`Running: \`node ${args.map((arg) => `"${arg}"`).join(' ')}\` in ${process.cwd()}`); } const now = () => new Date().getTime(); const before = now(); const tapProcess = childProcess.spawn('node', args, { env }); const result = await captureTapResult(tapProcess, !testOptions.disableBail && this.options.tap.forceBail); const timeSpentMs = now() - before; const fileName = tempTapOutputFileName(tapProcess.pid); const fileContent = await fs.readFile(fileName, 'utf-8'); await fs.rm(fileName); const file = JSON.parse(fileContent); const hitLimitReached = determineHitLimitReached(file.hitCount, testOptions.hitLimit); if (hitLimitReached) { throw new HitLimitError(hitLimitReached); } return { testResult: this.tapResultToTestResult(testFile, result, timeSpentMs), coverage: file.mutantCoverage, }; } tapResultToTestResult(fileName, { result, failedTests }, timeSpentMs) { const generic = { id: fileName, name: fileName, timeSpentMs: result.time ?? timeSpentMs, fileName: fileName, startPosition: undefined, }; if (result.ok) { return { ...generic, status: TestStatus.Success, }; } else { return { ...generic, status: TestStatus.Failed, failureMessage: failedTests.map((f) => `${f.fullname}: ${f.name}`).join(', ') ?? 'Unknown issue', }; } } } //# sourceMappingURL=tap-test-runner.js.map