diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index 230f787d7d..f4d10d3f4b 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -270,11 +270,6 @@ "description": "Set the concurrency of workers. Stryker will always run checkers and test runners in parallel by creating worker processes (note, not `worker_threads`). This defaults to `n-1` where `n` is the number of logical CPU cores available on your machine, unless `n <= 4`, in that case it uses `n`. This is a sane default for most use cases.", "type": "number" }, - "maxTestRunnerReuse": { - "description": "Restart each test runner worker process after `n` runs. Not recommended unless you are experiencing memory leaks that you are unable to resolve. Configuring `0` here means infinite reuse.", - "type": "number", - "default": 0 - }, "commandRunner": { "description": "Options used by the command test runner. Note: these options will only be used when the command test runner is activated (this is the default)", "$ref": "#/definitions/commandRunnerOptions", @@ -313,6 +308,11 @@ "type": "boolean", "default": false }, + "incremental": { + "description": "Experimental: Enable 'incremental mode'. Stryker will store results in \"reports/.mutation-incremental.json\" and use those to speed up the next --incremental run", + "type": "boolean", + "default": false + }, "fileLogLevel": { "description": "Set the log level that Stryker uses to write to the \"stryker.log\" file", "$ref": "#/definitions/logLevel", @@ -333,6 +333,11 @@ "type": "number", "default": 9007199254740991 }, + "maxTestRunnerReuse": { + "description": "Restart each test runner worker process after `n` runs. Not recommended unless you are experiencing memory leaks that you are unable to resolve. Configuring `0` here means infinite reuse.", + "type": "number", + "default": 0 + }, "mutate": { "description": "With mutate you configure the subset of files to use for mutation testing. Generally speaking, these should be your own source files.", "type": "array", diff --git a/packages/core/package.json b/packages/core/package.json index 751411d6f0..b2089580db 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -75,6 +75,7 @@ "mkdirp": "~1.0.3", "mutation-testing-elements": "1.7.10", "mutation-testing-metrics": "1.7.10", + "mutation-testing-report-schema": "1.7.10", "npm-run-path": "~5.1.0", "progress": "~2.0.0", "rimraf": "~3.0.0", diff --git a/packages/core/src/di/core-tokens.ts b/packages/core/src/di/core-tokens.ts index 3d2ce4988f..0431257da5 100644 --- a/packages/core/src/di/core-tokens.ts +++ b/packages/core/src/di/core-tokens.ts @@ -26,4 +26,5 @@ export const validationSchema = 'validationSchema'; export const optionsValidator = 'optionsValidator'; export const requireFromCwd = 'requireFromCwd'; export const fs = 'fs'; +export const incrementalResult = 'incrementalResult'; export const project = 'project'; diff --git a/packages/core/src/fs/project-reader.ts b/packages/core/src/fs/project-reader.ts index 663eba2f0e..f157e76f26 100644 --- a/packages/core/src/fs/project-reader.ts +++ b/packages/core/src/fs/project-reader.ts @@ -2,19 +2,21 @@ import path from 'path'; import { isDeepStrictEqual } from 'util'; import minimatch, { type IMinimatch } from 'minimatch'; -import { StrykerOptions, FileDescriptions, FileDescription } from '@stryker-mutator/api/core'; +import { StrykerOptions, FileDescriptions, FileDescription, Location, Position } from '@stryker-mutator/api/core'; import { Logger } from '@stryker-mutator/api/logging'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; -import { I } from '@stryker-mutator/util'; +import { ERROR_CODES, I, isErrnoException } from '@stryker-mutator/util'; +import type { MutationTestResult } from 'mutation-testing-report-schema/api'; + +import { OpenEndLocation } from 'mutation-testing-report-schema'; import { defaultOptions, FileMatcher } from '../config/index.js'; import { coreTokens } from '../di/index.js'; -import { Project } from './project.js'; +import { INCREMENTAL_REPORT_FILE, Project } from './project.js'; import { FileSystem } from './file-system.js'; const { Minimatch } = minimatch; - const ALWAYS_IGNORE = Object.freeze(['node_modules', '.git', '/reports', '*.tsbuildinfo', '/stryker.log']); export const IGNORE_PATTERN_CHARACTER = '!'; @@ -30,17 +32,23 @@ export const MUTATION_RANGE_REGEX = /(.*?):((\d+)(?::(\d+))?-(\d+)(?::(\d+))?)$/ export class ProjectReader { private readonly mutatePatterns: readonly string[]; private readonly ignoreRules: readonly string[]; + private readonly incremental: boolean; public static inject = tokens(coreTokens.fs, commonTokens.logger, commonTokens.options); - constructor(private readonly fs: I, private readonly log: Logger, { mutate, tempDirName, ignorePatterns }: StrykerOptions) { + constructor( + private readonly fs: I, + private readonly log: Logger, + { mutate, tempDirName, ignorePatterns, incremental }: StrykerOptions + ) { this.mutatePatterns = mutate; this.ignoreRules = [...ALWAYS_IGNORE, tempDirName, ...ignorePatterns]; + this.incremental = incremental; } public async read(): Promise { const inputFileNames = await this.resolveInputFileNames(); const fileDescriptions = this.resolveFileDescriptions(inputFileNames); - const project = new Project(this.fs, fileDescriptions); + const project = new Project(this.fs, fileDescriptions, this.incremental ? await this.readIncrementalReport() : undefined); project.logFiles(this.log, this.ignoreRules); return project; } @@ -182,4 +190,60 @@ export class ProjectReader { const files = await crawlDir(process.cwd()); return files; } + + private async readIncrementalReport(): Promise { + try { + // TODO: Validate against the schema or stryker version? + const result: MutationTestResult = JSON.parse(await this.fs.readFile(INCREMENTAL_REPORT_FILE, 'utf-8')); + return { + ...result, + files: Object.fromEntries( + Object.entries(result.files).map(([fileName, file]) => [ + fileName, + { ...file, mutants: file.mutants.map((mutant) => ({ ...mutant, location: reportLocationToStrykerLocation(mutant.location) })) }, + ]) + ), + testFiles: + result.testFiles && + Object.fromEntries( + Object.entries(result.testFiles).map(([fileName, file]) => [ + fileName, + { + ...file, + tests: file.tests.map((test) => ({ ...test, location: test.location && reportOpenEndLocationToStrykerLocation(test.location) })), + }, + ]) + ), + }; + } catch (err: unknown) { + if (isErrnoException(err) && err.code === ERROR_CODES.NoSuchFileOrDirectory) { + this.log.info('No incremental result file found, Stryker will perform a full run.'); + return undefined; + } + // Whoops, didn't mean to catch this one! + throw err; + } + } +} + +function reportOpenEndLocationToStrykerLocation({ start, end }: OpenEndLocation): OpenEndLocation { + return { + start: reportPositionToStrykerPosition(start), + end: end && reportPositionToStrykerPosition(end), + }; +} + +function reportLocationToStrykerLocation({ start, end }: Location): Location { + return { + start: reportPositionToStrykerPosition(start), + end: reportPositionToStrykerPosition(end), + }; +} + +function reportPositionToStrykerPosition({ line, column }: Position): Position { + // stryker's positions are 0-based + return { + line: line - 1, + column: column - 1, + }; } diff --git a/packages/core/src/fs/project.ts b/packages/core/src/fs/project.ts index ee39d03b38..c43f22918f 100644 --- a/packages/core/src/fs/project.ts +++ b/packages/core/src/fs/project.ts @@ -1,10 +1,15 @@ +import path from 'path'; + import { Logger } from '@stryker-mutator/api/logging'; import { FileDescriptions } from '@stryker-mutator/api/core'; import { I, normalizeWhitespaces } from '@stryker-mutator/util'; +import { MutationTestResult } from 'mutation-testing-report-schema'; import { FileSystem } from './file-system.js'; import { ProjectFile } from './project-file.js'; +export const INCREMENTAL_REPORT_FILE = path.join('reports', '.mutation-incremental.json'); + /** * Represents the project that is under test by Stryker users. * This represents the files in the current working directory. @@ -14,7 +19,11 @@ export class Project { public readonly files = new Map(); public readonly filesToMutate = new Map(); - constructor(fs: I, public readonly fileDescriptions: FileDescriptions) { + constructor( + private readonly fs: I, + public readonly fileDescriptions: FileDescriptions, + public readonly incrementalReport?: MutationTestResult + ) { Object.entries(fileDescriptions).forEach(([name, desc]) => { const file = new ProjectFile(fs, name, desc.mutate); this.files.set(name, file); @@ -49,4 +58,9 @@ export class Project { } } } + + public async writeIncrementalReport(report: MutationTestResult): Promise { + await this.fs.mkdir(path.dirname(INCREMENTAL_REPORT_FILE), { recursive: true }); + await this.fs.writeFile(INCREMENTAL_REPORT_FILE, JSON.stringify(report, null, 2), 'utf-8'); + } } diff --git a/packages/core/src/mutants/mutant-test-planner.ts b/packages/core/src/mutants/mutant-test-planner.ts index 7de1e7b936..4b891c343d 100644 --- a/packages/core/src/mutants/mutant-test-planner.ts +++ b/packages/core/src/mutants/mutant-test-planner.ts @@ -1,3 +1,5 @@ +import path from 'path'; + import { CompleteDryRunResult, TestResult } from '@stryker-mutator/api/test-runner'; import { MutantRunPlan, @@ -9,6 +11,7 @@ import { StrykerOptions, MutantStatus, MutantEarlyResultPlan, + MutantResult, } from '@stryker-mutator/api/core'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { Logger } from '@stryker-mutator/api/logging'; @@ -19,6 +22,7 @@ import { StrictReporter } from '../reporters/strict-reporter.js'; import { Sandbox } from '../sandbox/index.js'; import { objectUtils } from '../utils/object-utils.js'; import { optionsPath } from '../utils/index.js'; +import { Project } from '../fs/project.js'; /** * The factor by which hit count from dry run is multiplied to calculate the hit limit for a mutant. @@ -38,6 +42,7 @@ export class MutantTestPlanner { coreTokens.dryRunResult, coreTokens.reporter, coreTokens.sandbox, + coreTokens.project, coreTokens.timeOverheadMS, commonTokens.options, commonTokens.logger @@ -45,11 +50,13 @@ export class MutantTestPlanner { private readonly testsByMutantId: Map>; private readonly hitsByMutantId: Map; private readonly timeSpentAllTests: number; + private readonly previousFiles: Map | undefined; constructor( private readonly dryRunResult: CompleteDryRunResult, private readonly reporter: StrictReporter, private readonly sandbox: I, + private readonly project: I, private readonly timeOverheadMS: number, private readonly options: StrykerOptions, private readonly logger: Logger @@ -57,18 +64,27 @@ export class MutantTestPlanner { this.testsByMutantId = this.findTestsByMutant(this.dryRunResult.mutantCoverage?.perTest, dryRunResult.tests); this.hitsByMutantId = findHitsByMutantId(this.dryRunResult.mutantCoverage); this.timeSpentAllTests = calculateTotalTime(this.dryRunResult.tests); + const { incrementalReport } = this.project; + if (incrementalReport) { + const { files, testFiles } = incrementalReport; + this.previousFiles = new Map(Object.entries(files).map(([fileName, { source }]) => [fileName, source])); + if (testFiles) { + Object.entries(testFiles).forEach(([fileName, { source }]) => source && this.previousFiles!.set(fileName, source)); + } + } } - public makePlan(mutants: readonly Mutant[]): readonly MutantTestPlan[] { - const mutantPlans = mutants.map((mutant) => this.planMutant(mutant)); + public async makePlan(mutants: readonly Mutant[]): Promise { + const mutantsDiff = await this.incrementalDiff(mutants); + const mutantPlans = mutantsDiff.map((mutant) => this.planMutant(mutant)); this.reporter.onMutationTestingPlanReady({ mutantPlans }); this.warnAboutSlow(mutantPlans); return mutantPlans; } private planMutant(mutant: Mutant): MutantTestPlan { - // If mutant was already ignored, pass that along const isStatic = this.dryRunResult.mutantCoverage && this.dryRunResult.mutantCoverage.static[mutant.id] > 0; + if (mutant.status) { // If this mutant was already ignored, early result return this.createMutantEarlyResultPlan(mutant, { isStatic, status: mutant.status, statusReason: mutant.statusReason }); @@ -218,6 +234,66 @@ export class MutantTestPlanner { } } } + + private async incrementalDiff(currentMutants: readonly Mutant[]): Promise { + const { incrementalReport } = this.project; + if (incrementalReport) { + let earlyResultCount = 0; + const { files, testFiles } = incrementalReport; + const previousMutantResults = new Map( + Object.entries(files).flatMap(([fileName, { mutants }]) => mutants.map((mutant) => [toUniqueKey(mutant, fileName), mutant] as const)) + ); + const fileChanges = new Map(); + const previousFiles = new Map(Object.entries(files).map(([fileName, { source }]) => [path.resolve(fileName), source])); + if (testFiles) { + Object.entries(testFiles).forEach(([fileName, { source }]) => source && previousFiles.set(path.resolve(fileName), source)); + } + const fileIsSame = async (fileName: string): Promise => { + if (!fileChanges.has(fileName)) { + const previousFile = previousFiles.get(fileName); + const currentContent = await this.project.files.get(fileName)!.readOriginal(); + fileChanges.set(fileName, previousFile === currentContent); + } + return fileChanges.get(fileName)!; + }; + + const result = await Promise.all( + currentMutants.map(async (mutant) => { + const previousMutant = previousMutantResults.get(toUniqueKey(mutant, mutant.fileName)); + if (previousMutant && (await fileIsSame(mutant.fileName))) { + const tests = this.testsByMutantId.get(mutant.id); + let testFilesAreSame = true; + for (const test of tests ?? []) { + if (test.fileName) { + testFilesAreSame = await fileIsSame(test.fileName); + if (!testFilesAreSame) { + break; + } + } + } + if (testFilesAreSame) { + earlyResultCount++; + return { + ...mutant, + status: previousMutant.status, + }; + } + } + return mutant; + }) + ); + this.logger.info(`Incremental mode: ${earlyResultCount}/${currentMutants.length} are reused.`); + return result; + } + return currentMutants; + } +} + +function toUniqueKey( + { mutatorName, replacement, location: { start, end } }: Pick & { replacement?: string }, + fileName: string +) { + return `${path.relative(process.cwd(), fileName)}@${start.line}:${start.column}-${end.line}:${end.column}\n${mutatorName}: ${replacement}`; } function calculateTotalTime(testResults: Iterable): number { diff --git a/packages/core/src/process/1-prepare-executor.ts b/packages/core/src/process/1-prepare-executor.ts index bf48b1cb09..ad0c87ad5d 100644 --- a/packages/core/src/process/1-prepare-executor.ts +++ b/packages/core/src/process/1-prepare-executor.ts @@ -56,19 +56,19 @@ export class PrepareExecutor { const loggingContext = await LogConfigurator.configureLoggingServer(options.logLevel, options.fileLogLevel, options.allowConsoleColors); // Resolve input files - const inputFileResolverInjector = optionsValidatorInjector + const projectFileReaderInjector = optionsValidatorInjector .provideValue(commonTokens.options, options) .provideClass(coreTokens.temporaryDirectory, TemporaryDirectory) .provideClass(coreTokens.fs, FileSystem) .provideValue(coreTokens.pluginsByKind, loadedPlugins.pluginsByKind); - const project = await inputFileResolverInjector.injectClass(ProjectReader).read(); + const project = await projectFileReaderInjector.injectClass(ProjectReader).read(); if (project.isEmpty) { throw new ConfigError('No input files found.'); } else { // Done preparing, finish up and return - await inputFileResolverInjector.resolve(coreTokens.temporaryDirectory).initialize(); - return inputFileResolverInjector + await projectFileReaderInjector.resolve(coreTokens.temporaryDirectory).initialize(); + return projectFileReaderInjector .provideValue(coreTokens.project, project) .provideValue(commonTokens.fileDescriptions, project.fileDescriptions) .provideClass(coreTokens.pluginCreator, PluginCreator) diff --git a/packages/core/src/process/4-mutation-test-executor.ts b/packages/core/src/process/4-mutation-test-executor.ts index ff0b9a6b49..1dee3cae76 100644 --- a/packages/core/src/process/4-mutation-test-executor.ts +++ b/packages/core/src/process/4-mutation-test-executor.ts @@ -64,12 +64,12 @@ export class MutationTestExecutor { ) {} public async execute(): Promise { - const mutantTestPlans = this.planner.makePlan(this.mutants); - const { ignoredResult$, notIgnoredMutant$ } = this.executeEarlyResult(from(mutantTestPlans)); - const { passedMutant$, checkResult$ } = this.executeCheck(notIgnoredMutant$); + const mutantTestPlans = await this.planner.makePlan(this.mutants); + const { earlyResult$, runMutant$ } = this.executeEarlyResult(from(mutantTestPlans)); + const { passedMutant$, checkResult$ } = this.executeCheck(runMutant$); const { coveredMutant$, noCoverageResult$ } = this.executeNoCoverage(passedMutant$); const testRunnerResult$ = this.executeRunInTestRunner(coveredMutant$); - const results = await lastValueFrom(merge(testRunnerResult$, checkResult$, noCoverageResult$, ignoredResult$).pipe(toArray())); + const results = await lastValueFrom(merge(testRunnerResult$, checkResult$, noCoverageResult$, earlyResult$).pipe(toArray())); await this.mutationTestReportHelper.reportAll(results); await this.reporter.wrapUp(); this.logDone(); @@ -77,9 +77,9 @@ export class MutationTestExecutor { } private executeEarlyResult(input$: Observable) { - const [ignoredMutant$, notIgnoredMutant$] = partition(input$.pipe(shareReplay()), isEarlyResult); - const ignoredResult$ = ignoredMutant$.pipe(map(({ mutant }) => this.mutationTestReportHelper.reportMutantStatus(mutant, MutantStatus.Ignored))); - return { ignoredResult$, notIgnoredMutant$ }; + const [earlyResultMutants$, runMutant$] = partition(input$.pipe(shareReplay()), isEarlyResult); + const earlyResult$ = earlyResultMutants$.pipe(map(({ mutant }) => this.mutationTestReportHelper.reportMutantStatus(mutant, mutant.status))); + return { earlyResult$, runMutant$ }; } private executeNoCoverage(input$: Observable) { diff --git a/packages/core/src/reporters/mutation-test-report-helper.ts b/packages/core/src/reporters/mutation-test-report-helper.ts index bf0fc302df..5d333babd0 100644 --- a/packages/core/src/reporters/mutation-test-report-helper.ts +++ b/packages/core/src/reporters/mutation-test-report-helper.ts @@ -111,6 +111,9 @@ export class MutationTestReportHelper { const metrics = calculateMutationTestMetrics(report); this.reporter.onAllMutantsTested(results); this.reporter.onMutationTestReportReady(report, metrics); + if (this.options.incremental) { + await this.project.writeIncrementalReport(report); + } this.determineExitCode(metrics); } diff --git a/packages/core/src/stryker-cli.ts b/packages/core/src/stryker-cli.ts index c1b0ccacdc..2a3c780464 100644 --- a/packages/core/src/stryker-cli.ts +++ b/packages/core/src/stryker-cli.ts @@ -73,6 +73,10 @@ export class StrykerCli { list ) .option('--ignoreStatic', 'Ignore static mutants. Static mutants are mutants which are only executed during the loading of a file.') + .option( + '--incremental', + 'Experimental: Enable \'incremental mode\'. Stryker will store results in "reports/stryker-incremental.json" and use those to speed up next --incremental run' + ) .option( '-m, --mutate ', 'A comma separated list of globbing expression used for selecting the files that should be mutated. Example: src/**/*.js,a.js. You can also specify specific lines and columns to mutate by adding :startLine[:startColumn]-endLine[:endColumn]. This will execute all mutants inside that range. It cannot be combined with glob patterns. Example: src/index.js:1:3-1:5', diff --git a/packages/core/test/unit/config/options-validator.spec.ts b/packages/core/test/unit/config/options-validator.spec.ts index 67e5093615..fefbccc2ef 100644 --- a/packages/core/test/unit/config/options-validator.spec.ts +++ b/packages/core/test/unit/config/options-validator.spec.ts @@ -35,6 +35,7 @@ describe(OptionsValidator.name, () => { cleanTempDir: true, inPlace: false, ignorePatterns: [], + incremental: false, ignoreStatic: false, checkerNodeArgs: [], clearTextReporter: { diff --git a/packages/core/test/unit/mutants/mutant-test-planner.spec.ts b/packages/core/test/unit/mutants/mutant-test-planner.spec.ts index a746431155..006f45104d 100644 --- a/packages/core/test/unit/mutants/mutant-test-planner.spec.ts +++ b/packages/core/test/unit/mutants/mutant-test-planner.spec.ts @@ -8,17 +8,21 @@ import { Reporter } from '@stryker-mutator/api/report'; import { MutantTestPlanner } from '../../../src/mutants/mutant-test-planner.js'; import { coreTokens } from '../../../src/di/index.js'; import { Sandbox } from '../../../src/sandbox/index.js'; +import { Project } from '../../../src/fs/index.js'; +import { FileSystemTestDouble } from '../../helpers/file-system-test-double.js'; const TIME_OVERHEAD_MS = 501; describe(MutantTestPlanner.name, () => { let reporterMock: sinon.SinonStubbedInstance>; let sandboxMock: sinon.SinonStubbedInstance; + let fileSystemTestDouble: FileSystemTestDouble; beforeEach(() => { reporterMock = factory.reporter(); sandboxMock = sinon.createStubInstance(Sandbox); sandboxMock.sandboxFileFor.returns('sandbox/foo.js'); + fileSystemTestDouble = new FileSystemTestDouble(); }); function act(dryRunResult: CompleteDryRunResult, mutants: Mutant[]) { @@ -27,17 +31,18 @@ describe(MutantTestPlanner.name, () => { .provideValue(coreTokens.dryRunResult, dryRunResult) .provideValue(coreTokens.mutants, mutants) .provideValue(coreTokens.sandbox, sandboxMock) + .provideValue(coreTokens.project, new Project(fileSystemTestDouble, fileSystemTestDouble.toFileDescriptions())) .provideValue(coreTokens.timeOverheadMS, TIME_OVERHEAD_MS) .injectClass(MutantTestPlanner) .makePlan(mutants); } - it('should make an early result plan for an ignored mutant', () => { + it('should make an early result plan for an ignored mutant', async () => { const mutant = factory.mutant({ id: '2', status: MutantStatus.Ignored, statusReason: 'foo should ignore' }); const dryRunResult = factory.completeDryRunResult({ mutantCoverage: { static: {}, perTest: { '1': { 2: 2 } } } }); // Act - const result = act(dryRunResult, [mutant]); + const result = await act(dryRunResult, [mutant]); // Assert const expected: MutantEarlyResultPlan[] = [ @@ -46,13 +51,13 @@ describe(MutantTestPlanner.name, () => { expect(result).deep.eq(expected); }); - it('should make a plan with an empty test filter for a mutant without coverage', () => { + it('should make a plan with an empty test filter for a mutant without coverage', async () => { // Arrange const mutant = factory.mutant({ id: '3' }); const dryRunResult = factory.completeDryRunResult({ mutantCoverage: { static: {}, perTest: { '1': { 2: 2 } } } }); // Act - const [result] = act(dryRunResult, [mutant]); + const [result] = await act(dryRunResult, [mutant]); // Assert assertIsRunPlan(result); @@ -67,7 +72,7 @@ describe(MutantTestPlanner.name, () => { const dryRunResult = factory.completeDryRunResult({ mutantCoverage: { static: {}, perTest: { '1': { 2: 2 } } } }); // Act - const [result] = act(dryRunResult, [mutant]); + const [result] = await act(dryRunResult, [mutant]); // Assert assertIsRunPlan(result); @@ -81,14 +86,14 @@ describe(MutantTestPlanner.name, () => { testInjector.options.disableBail = true; // Act - const [result] = act(dryRunResult, [mutant]); + const [result] = await act(dryRunResult, [mutant]); // Assert assertIsRunPlan(result); expect(result.runOptions.disableBail).true; }); - it('should report onMutationTestingPlanReady', () => { + it('should report onMutationTestingPlanReady', async () => { // Arrange const mutants = [ factory.mutant({ @@ -112,14 +117,14 @@ describe(MutantTestPlanner.name, () => { }); // Act - const mutantPlans = act(dryRunResult, mutants); + const mutantPlans = await act(dryRunResult, mutants); // Assert sinon.assert.calledOnceWithExactly(reporterMock.onMutationTestingPlanReady, { mutantPlans }); }); describe('without mutant coverage data', () => { - it('should disable the test filter', () => { + it('should disable the test filter', async () => { // Arrange const mutant1 = factory.mutant({ id: '1' }); const mutant2 = factory.mutant({ id: '2' }); @@ -127,7 +132,7 @@ describe(MutantTestPlanner.name, () => { const dryRunResult = factory.completeDryRunResult({ mutantCoverage: undefined }); // Act - const [plan1, plan2] = act(dryRunResult, mutants); + const [plan1, plan2] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(plan1); @@ -140,20 +145,20 @@ describe(MutantTestPlanner.name, () => { expect(plan2.mutant.static).undefined; }); - it('should disable the hitLimit', () => { + it('should disable the hitLimit', async () => { // Arrange const mutants = [factory.mutant({ id: '1' })]; const dryRunResult = factory.completeDryRunResult({ mutantCoverage: undefined }); // Act - const [result] = act(dryRunResult, mutants); + const [result] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(result); expect(result.runOptions.hitLimit).undefined; }); - it('should calculate timeout and net time using the sum of all tests', () => { + it('should calculate timeout and net time using the sum of all tests', async () => { // Arrange const mutant1 = factory.mutant({ id: '1' }); const mutants = [mutant1]; @@ -163,7 +168,7 @@ describe(MutantTestPlanner.name, () => { }); // Act - const [result] = act(dryRunResult, mutants); + const [result] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(result); @@ -184,7 +189,7 @@ describe(MutantTestPlanner.name, () => { }); // Act - const result = act(dryRunResult, mutants); + const result = await act(dryRunResult, mutants); // Assert const expected: MutantTestPlan[] = [ @@ -202,7 +207,7 @@ describe(MutantTestPlanner.name, () => { expect(result).deep.eq(expected); }); - it('should disable test filtering, set reload environment and activate mutant statically when ignoreStatic is disabled', () => { + it('should disable test filtering, set reload environment and activate mutant statically when ignoreStatic is disabled', async () => { // Arrange testInjector.options.ignoreStatic = false; const mutants = [factory.mutant({ id: '1' })]; @@ -212,7 +217,7 @@ describe(MutantTestPlanner.name, () => { }); // Act - const [result] = act(dryRunResult, mutants); + const [result] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(result); @@ -223,20 +228,20 @@ describe(MutantTestPlanner.name, () => { expect(result.runOptions.mutantActivation).eq('static'); }); - it('should set activeMutant on the runOptions', () => { + it('should set activeMutant on the runOptions', async () => { // Arrange const mutants = [Object.freeze(factory.mutant({ id: '1' }))]; const dryRunResult = factory.completeDryRunResult({ tests: [factory.successTestResult({ id: 'spec1', timeSpentMs: 0 })] }); // Act - const [result] = act(dryRunResult, mutants); + const [result] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(result); expect(result.runOptions.activeMutant).deep.eq(mutants[0]); }); - it('should calculate the hitLimit based on total hits (perTest and static)', () => { + it('should calculate the hitLimit based on total hits (perTest and static)', async () => { // Arrange const mutant = factory.mutant({ id: '1' }); const mutants = [mutant]; @@ -246,14 +251,14 @@ describe(MutantTestPlanner.name, () => { }); // Act - const [result] = act(dryRunResult, mutants); + const [result] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(result); expect(result.runOptions.hitLimit).deep.eq(600); }); - it('should calculate timeout and net time using the sum of all tests', () => { + it('should calculate timeout and net time using the sum of all tests', async () => { // Arrange const mutant = factory.mutant({ id: '1' }); const mutants = [mutant]; @@ -263,7 +268,7 @@ describe(MutantTestPlanner.name, () => { }); // Act - const [result] = act(dryRunResult, mutants); + const [result] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(result); @@ -283,7 +288,7 @@ describe(MutantTestPlanner.name, () => { }); // Act - const [result] = act(dryRunResult, mutants); + const [result] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(result); @@ -304,7 +309,7 @@ describe(MutantTestPlanner.name, () => { }); // Act - const [result] = act(dryRunResult, mutants); + const [result] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(result); @@ -317,7 +322,7 @@ describe(MutantTestPlanner.name, () => { }); describe('with perTest coverage', () => { - it('should enable test filtering with runtime mutant activation for covered tests', () => { + it('should enable test filtering with runtime mutant activation for covered tests', async () => { // Arrange const mutants = [factory.mutant({ id: '1' }), factory.mutant({ id: '2' })]; const dryRunResult = factory.completeDryRunResult({ @@ -326,7 +331,7 @@ describe(MutantTestPlanner.name, () => { }); // Act - const [plan1, plan2] = act(dryRunResult, mutants); + const [plan1, plan2] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(plan1); @@ -343,7 +348,7 @@ describe(MutantTestPlanner.name, () => { expect(mutant2.static).false; }); - it('should calculate timeout and net time using the sum of covered tests', () => { + it('should calculate timeout and net time using the sum of covered tests', async () => { // Arrange const mutants = [factory.mutant({ id: '1' }), factory.mutant({ id: '2' })]; const dryRunResult = factory.completeDryRunResult({ @@ -356,7 +361,7 @@ describe(MutantTestPlanner.name, () => { }); // Act - const [plan1, plan2] = act(dryRunResult, mutants); + const [plan1, plan2] = await act(dryRunResult, mutants); // Assert assertIsRunPlan(plan1); @@ -367,7 +372,7 @@ describe(MutantTestPlanner.name, () => { expect(plan2.runOptions.timeout).eq(calculateTimeout(10)); // spec2 }); - it('should allow for non-existing tests (#2485)', () => { + it('should allow for non-existing tests (#2485)', async () => { // Arrange const mutant1 = factory.mutant({ id: '1' }); const mutant2 = factory.mutant({ id: '2' }); @@ -378,7 +383,7 @@ describe(MutantTestPlanner.name, () => { }); // Act - const actualMatches = act(dryRunResult, mutants); + const actualMatches = await act(dryRunResult, mutants); // Assert expect(actualMatches.find(({ mutant }) => mutant.id === '1')?.mutant.coveredBy).deep.eq(['spec1']); @@ -412,13 +417,13 @@ describe(MutantTestPlanner.name, () => { return { mutants, dryRunResult }; } - it('should warn when the estimated time to run all static mutants exceeds 40% and the performance impact of a static mutant is estimated to be twice that of other mutants', () => { + it('should warn when the estimated time to run all static mutants exceeds 40% and the performance impact of a static mutant is estimated to be twice that of other mutants', async () => { // Arrange testInjector.options.ignoreStatic = false; const { mutants, dryRunResult } = arrangeStaticWarning(); // Act - act(dryRunResult, mutants); + await act(dryRunResult, mutants); // Assert expect(testInjector.logger.warn) @@ -426,32 +431,32 @@ describe(MutantTestPlanner.name, () => { .and.calledWithMatch('(disable "warnings.slow" to ignore this warning)'); }); - it('should not warn when ignore static is enabled', () => { + it('should not warn when ignore static is enabled', async () => { // Arrange testInjector.options.ignoreStatic = true; const { mutants, dryRunResult } = arrangeStaticWarning(); // Act - act(dryRunResult, mutants); + await act(dryRunResult, mutants); // Assert expect(testInjector.logger.warn).not.called; }); - it('should not warn when "warning.slow" is disabled', () => { + it('should not warn when "warning.slow" is disabled', async () => { // Arrange testInjector.options.ignoreStatic = false; testInjector.options.warnings = factory.warningOptions({ slow: false }); const { mutants, dryRunResult } = arrangeStaticWarning(); // Act - act(dryRunResult, mutants); + await act(dryRunResult, mutants); // Assert expect(testInjector.logger.warn).not.called; }); - it('should not warn when all static mutants is not estimated to exceed 40%', () => { + it('should not warn when all static mutants is not estimated to exceed 40%', async () => { // Arrange const mutants = [ factory.mutant({ id: '1' }), @@ -473,13 +478,13 @@ describe(MutantTestPlanner.name, () => { }); // Act - act(dryRunResult, mutants); + await act(dryRunResult, mutants); // Assert expect(testInjector.logger.warn).not.called; }); - it('should not warn when the performance impact of a static mutant is estimated to be twice that of other mutants', () => { + it('should not warn when the performance impact of a static mutant is estimated to be twice that of other mutants', async () => { // Arrange const mutants = [ factory.mutant({ id: '1' }), @@ -504,12 +509,13 @@ describe(MutantTestPlanner.name, () => { }); // Act - act(dryRunResult, mutants); + await act(dryRunResult, mutants); // Assert expect(testInjector.logger.warn).not.called; }); }); + console.log('changed'); }); function assertIsRunPlan(plan: MutantTestPlan): asserts plan is MutantRunPlan { diff --git a/packages/core/test/unit/process/4-mutation-test-executor.spec.ts b/packages/core/test/unit/process/4-mutation-test-executor.spec.ts index df2882e1e0..28fab9c57c 100644 --- a/packages/core/test/unit/process/4-mutation-test-executor.spec.ts +++ b/packages/core/test/unit/process/4-mutation-test-executor.spec.ts @@ -74,7 +74,7 @@ describe(MutationTestExecutor.name, () => { mutants = [factory.mutant()]; mutantTestPlans = []; - mutantTestPlannerMock.makePlan.returns(mutantTestPlans); + mutantTestPlannerMock.makePlan.resolves(mutantTestPlans); sut = testInjector.injector .provideValue(coreTokens.reporter, reporterMock) .provideValue(coreTokens.checkerPool, checkerPoolMock) diff --git a/packages/core/test/unit/stryker-cli.spec.ts b/packages/core/test/unit/stryker-cli.spec.ts index 98c403cefd..eb48c3ce84 100644 --- a/packages/core/test/unit/stryker-cli.spec.ts +++ b/packages/core/test/unit/stryker-cli.spec.ts @@ -48,6 +48,7 @@ describe(StrykerCli.name, () => { [['--testRunner', 'foo-running'], { testRunner: 'foo-running' }], [['--testRunnerNodeArgs', '--inspect=1337 --gc'], { testRunnerNodeArgs: ['--inspect=1337', '--gc'] }], [['--coverageAnalysis', 'all'], { coverageAnalysis: 'all' }], + [['--incremental'], { incremental: true }], [['--inPlace'], { inPlace: true }], [['--ignoreStatic'], { ignoreStatic: true }], [['--concurrency', '5'], { concurrency: 5 }], diff --git a/packages/util/src/errors.ts b/packages/util/src/errors.ts index 238ad2c194..bb5078f834 100644 --- a/packages/util/src/errors.ts +++ b/packages/util/src/errors.ts @@ -20,3 +20,7 @@ export function errorToString(error: any): string { } return error.toString(); } + +export const ERROR_CODES = Object.freeze({ + NoSuchFileOrDirectory: 'ENOENT', +});