diff --git a/packages/api/src/test-runner/run-options.ts b/packages/api/src/test-runner/run-options.ts index 94604fa027..ab6e37576f 100644 --- a/packages/api/src/test-runner/run-options.ts +++ b/packages/api/src/test-runner/run-options.ts @@ -16,6 +16,10 @@ export interface DryRunOptions extends RunOptions { * Indicates whether or not mutant coverage should be collected. */ coverageAnalysis: CoverageAnalysis; + /** + * Files to run tests for. + */ + files?: string[]; } export interface MutantRunOptions extends RunOptions { diff --git a/packages/core/src/process/3-dry-run-executor.ts b/packages/core/src/process/3-dry-run-executor.ts index d6c5fe84a5..9f8f823ede 100644 --- a/packages/core/src/process/3-dry-run-executor.ts +++ b/packages/core/src/process/3-dry-run-executor.ts @@ -27,6 +27,7 @@ import { ConfigError } from '../errors'; import { findMutantTestCoverage } from '../mutants'; import { ConcurrencyTokenProvider, Pool, createTestRunnerPool } from '../concurrent'; import { FileMatcher } from '../config'; +import { InputFileCollection } from '../input/input-file-collection'; import { MutationTestContext } from './4-mutation-test-executor'; import { MutantInstrumenterContext } from './2-mutant-instrumenter-executor'; @@ -38,6 +39,7 @@ export interface DryRunContext extends MutantInstrumenterContext { [coreTokens.mutants]: readonly Mutant[]; [coreTokens.checkerPool]: I>; [coreTokens.concurrencyTokenProvider]: I; + [coreTokens.inputFiles]: InputFileCollection; } /** @@ -123,6 +125,8 @@ export class DryRunExecutor { private async timeDryRun(testRunner: TestRunner): Promise<{ dryRunResult: CompleteDryRunResult; timing: Timing }> { const dryRunTimeout = this.options.dryRunTimeoutMinutes * 1000 * 60; + const inputFiles = this.injector.resolve(coreTokens.inputFiles); + const dryRunFiles = inputFiles.filesToMutate.map((file) => this.sandbox.sandboxFileFor(file.name)); this.timer.mark(INITIAL_TEST_RUN_MARKER); this.log.info( `Starting initial test run (${this.options.testRunner} test runner with "${this.options.coverageAnalysis}" coverage analysis). This may take a while.` @@ -132,6 +136,7 @@ export class DryRunExecutor { timeout: dryRunTimeout, coverageAnalysis: this.options.coverageAnalysis, disableBail: this.options.disableBail, + files: dryRunFiles, }); const grossTimeMS = this.timer.elapsedMs(INITIAL_TEST_RUN_MARKER); const humanReadableTimeElapsed = this.timer.humanReadableElapsed(INITIAL_TEST_RUN_MARKER); diff --git a/packages/core/test/unit/process/3-dry-run-executor.spec.ts b/packages/core/test/unit/process/3-dry-run-executor.spec.ts index 6e5ab56f97..cdbd7f18b6 100644 --- a/packages/core/test/unit/process/3-dry-run-executor.spec.ts +++ b/packages/core/test/unit/process/3-dry-run-executor.spec.ts @@ -9,6 +9,7 @@ import { Observable } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; import { I } from '@stryker-mutator/util'; +import { File } from '@stryker-mutator/api/core'; import { Timer } from '../../../src/utils/timer'; import { DryRunContext, DryRunExecutor, MutationTestContext } from '../../../src/process'; @@ -17,6 +18,7 @@ import { ConfigError } from '../../../src/errors'; import { ConcurrencyTokenProvider, Pool } from '../../../src/concurrent'; import { createTestRunnerPoolMock } from '../../helpers/producers'; import { Sandbox } from '../../../src/sandbox'; +import { InputFileCollection } from '../../../src/input/input-file-collection'; describe(DryRunExecutor.name, () => { let injectorMock: sinon.SinonStubbedInstance>; @@ -26,6 +28,7 @@ describe(DryRunExecutor.name, () => { let testRunnerMock: sinon.SinonStubbedInstance>; let concurrencyTokenProviderMock: sinon.SinonStubbedInstance; let sandbox: sinon.SinonStubbedInstance; + let inputFiles: InputFileCollection; beforeEach(() => { timerMock = sinon.createStubInstance(Timer); @@ -41,6 +44,8 @@ describe(DryRunExecutor.name, () => { injectorMock = factory.injector(); injectorMock.resolve.withArgs(coreTokens.testRunnerPool).returns(testRunnerPoolMock as I>); sandbox = sinon.createStubInstance(Sandbox); + inputFiles = new InputFileCollection([new File('bar.js', 'console.log("bar")')], ['bar.js'], []); + injectorMock.resolve.withArgs(coreTokens.inputFiles).returns(inputFiles); sut = new DryRunExecutor( injectorMock as Injector, testInjector.logger, @@ -109,6 +114,26 @@ describe(DryRunExecutor.name, () => { }); }); + describe('files', () => { + const dryRunFileName = '.sandbox/bar.js'; + let runResult: CompleteDryRunResult; + + beforeEach(() => { + sandbox.sandboxFileFor.withArgs(inputFiles.filesToMutate[0].name).returns(dryRunFileName); + + runResult = factory.completeDryRunResult(); + testRunnerMock.dryRun.resolves(runResult); + runResult.tests.push(factory.successTestResult()); + }); + + it('should test only for files to mutate', async () => { + await sut.execute(); + expect(testRunnerMock.dryRun).calledWithMatch({ + files: [dryRunFileName], + }); + }); + }); + describe('when the dryRun completes', () => { let runResult: CompleteDryRunResult; diff --git a/packages/jest-runner/src/jest-test-adapters/jest-greater-than-25-adapter.ts b/packages/jest-runner/src/jest-test-adapters/jest-greater-than-25-adapter.ts index b4ea5a2f9b..d8429ebd86 100644 --- a/packages/jest-runner/src/jest-test-adapters/jest-greater-than-25-adapter.ts +++ b/packages/jest-runner/src/jest-test-adapters/jest-greater-than-25-adapter.ts @@ -4,13 +4,13 @@ import { JestRunResult } from '../jest-run-result'; import { JestTestAdapter, RunSettings } from './jest-test-adapter'; export class JestGreaterThan25TestAdapter implements JestTestAdapter { - public async run({ jestConfig, fileNameUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise { + public async run({ jestConfig, fileNamesUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise { const config = JSON.stringify(jestConfig); const result = await jestWrapper.runCLI( { $0: 'stryker', - _: fileNameUnderTest ? [fileNameUnderTest] : [], - findRelatedTests: !!fileNameUnderTest, + _: fileNamesUnderTest ? fileNamesUnderTest : [], + findRelatedTests: !!fileNamesUnderTest, config, runInBand: true, silent: true, diff --git a/packages/jest-runner/src/jest-test-adapters/jest-less-than-25-adapter.ts b/packages/jest-runner/src/jest-test-adapters/jest-less-than-25-adapter.ts index 9c792b35ec..a0c78ef611 100644 --- a/packages/jest-runner/src/jest-test-adapters/jest-less-than-25-adapter.ts +++ b/packages/jest-runner/src/jest-test-adapters/jest-less-than-25-adapter.ts @@ -8,13 +8,13 @@ import { RunSettings, JestTestAdapter } from './jest-test-adapter'; * It has a lot of `any` typings here, since the installed typings are not in sync. */ export class JestLessThan25TestAdapter implements JestTestAdapter { - public run({ jestConfig, fileNameUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise { + public run({ jestConfig, fileNamesUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise { const config = JSON.stringify(jestConfig); return jestWrapper.runCLI( { $0: 'stryker', - _: fileNameUnderTest ? [fileNameUnderTest] : [], - findRelatedTests: !!fileNameUnderTest, + _: fileNamesUnderTest ? fileNamesUnderTest : [], + findRelatedTests: !!fileNamesUnderTest, config, runInBand: true, silent: true, diff --git a/packages/jest-runner/src/jest-test-adapters/jest-test-adapter.ts b/packages/jest-runner/src/jest-test-adapters/jest-test-adapter.ts index a67c560188..0556dbd58c 100644 --- a/packages/jest-runner/src/jest-test-adapters/jest-test-adapter.ts +++ b/packages/jest-runner/src/jest-test-adapters/jest-test-adapter.ts @@ -5,7 +5,7 @@ import { JestRunResult } from '../jest-run-result'; export interface RunSettings { jestConfig: Config.InitialOptions; testNamePattern?: string; - fileNameUnderTest?: string; + fileNamesUnderTest?: string[]; testLocationInResults?: boolean; } diff --git a/packages/jest-runner/src/jest-test-runner.ts b/packages/jest-runner/src/jest-test-runner.ts index 7d532a4b18..39033b0293 100644 --- a/packages/jest-runner/src/jest-test-runner.ts +++ b/packages/jest-runner/src/jest-test-runner.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { StrykerOptions, INSTRUMENTER_CONSTANTS, MutantCoverage } from '@stryker-mutator/api/core'; +import { StrykerOptions, INSTRUMENTER_CONSTANTS, MutantCoverage, CoverageAnalysis } from '@stryker-mutator/api/core'; import { Logger } from '@stryker-mutator/api/logging'; import { commonTokens, Injector, PluginContext, tokens } from '@stryker-mutator/api/plugin'; import { @@ -91,7 +91,11 @@ export class JestTestRunner implements TestRunner { } } - public async dryRun({ coverageAnalysis, disableBail }: Pick): Promise { + public async dryRun({ + coverageAnalysis, + disableBail, + files, + }: Pick): Promise { state.coverageAnalysis = coverageAnalysis; const mutantCoverage: MutantCoverage = { perTest: {}, static: {} }; const fileNamesWithMutantCoverage: string[] = []; @@ -101,9 +105,11 @@ export class JestTestRunner implements TestRunner { fileNamesWithMutantCoverage.push(fileName); }); } + const fileNamesUnderTest = this.enableFindRelatedTests ? files : undefined; try { const { dryRunResult, jestResult } = await this.run({ - jestConfig: withCoverageAnalysis({ ...this.jestConfig }, coverageAnalysis), + fileNamesUnderTest, + jestConfig: this.configForDryRun(fileNamesUnderTest, coverageAnalysis), testLocationInResults: true, }); if (dryRunResult.status === DryRunStatus.Complete && coverageAnalysis !== 'off') { @@ -134,7 +140,7 @@ export class JestTestRunner implements TestRunner { try { const { dryRunResult } = await this.run({ - fileNameUnderTest, + fileNamesUnderTest: fileNameUnderTest ? [fileNameUnderTest] : undefined, jestConfig: this.configForMutantRun(fileNameUnderTest), testNamePattern, }); @@ -144,14 +150,22 @@ export class JestTestRunner implements TestRunner { } } + private configForDryRun(fileNamesUnderTest: string[] | undefined, coverageAnalysis: CoverageAnalysis): jest.Config.InitialOptions { + return withCoverageAnalysis(this.configWithRoots(fileNamesUnderTest), coverageAnalysis); + } + private configForMutantRun(fileNameUnderTest: string | undefined): jest.Config.InitialOptions { + return this.configWithRoots(fileNameUnderTest ? [fileNameUnderTest] : undefined); + } + + private configWithRoots(fileNamesUnderTest: string[] | undefined): jest.Config.InitialOptions { let config: jest.Config.InitialOptions; - if (fileNameUnderTest && this.jestConfig.roots) { + if (fileNamesUnderTest && this.jestConfig.roots) { // Make sure the file under test lives inside one of the roots config = { ...this.jestConfig, - roots: [...this.jestConfig.roots, path.dirname(fileNameUnderTest)], + roots: [...this.jestConfig.roots, ...new Set(fileNamesUnderTest.map((file) => path.dirname(file)))], }; } else { config = this.jestConfig; diff --git a/packages/jest-runner/test/unit/jest-test-adapters/jest-greater-than-25-adapter.spec.ts b/packages/jest-runner/test/unit/jest-test-adapters/jest-greater-than-25-adapter.spec.ts index 78b106bbbf..4745ff762e 100644 --- a/packages/jest-runner/test/unit/jest-test-adapters/jest-greater-than-25-adapter.spec.ts +++ b/packages/jest-runner/test/unit/jest-test-adapters/jest-greater-than-25-adapter.spec.ts @@ -11,7 +11,7 @@ describe(JestGreaterThan25TestAdapter.name, () => { let sut: JestGreaterThan25TestAdapter; let runCLIStub: sinon.SinonStub; - const fileNameUnderTest = '/path/to/file'; + const fileNamesUnderTest = ['/path/to/file']; let jestConfig: Config.InitialOptions; beforeEach(() => { @@ -37,12 +37,12 @@ describe(JestGreaterThan25TestAdapter.name, () => { }); it('should call the runCLI method with the --findRelatedTests flag when provided', async () => { - await sut.run({ jestConfig, fileNameUnderTest }); + await sut.run({ jestConfig, fileNamesUnderTest }); expect(runCLIStub).calledWith( sinon.match({ $0: 'stryker', - _: [fileNameUnderTest], + _: fileNamesUnderTest, config: JSON.stringify(jestConfig), findRelatedTests: true, runInBand: true, @@ -96,7 +96,7 @@ describe(JestGreaterThan25TestAdapter.name, () => { }); it('should call the runCLI method and return the test result when run with --findRelatedTests flag', async () => { - const result = await sut.run({ jestConfig, fileNameUnderTest }); + const result = await sut.run({ jestConfig, fileNamesUnderTest }); expect(result).to.deep.equal({ config: jestConfig, diff --git a/packages/jest-runner/test/unit/jest-test-runner.spec.ts b/packages/jest-runner/test/unit/jest-test-runner.spec.ts index bb057731d9..e7cae2802d 100644 --- a/packages/jest-runner/test/unit/jest-test-runner.spec.ts +++ b/packages/jest-runner/test/unit/jest-test-runner.spec.ts @@ -292,6 +292,28 @@ describe(JestTestRunner.name, () => { }); }); + it('should use correct fileNamesUnderTest if findRelatedTests = true', async () => { + options.jest.enableFindRelatedTests = true; + const sut = createSut(); + await sut.dryRun(factory.dryRunOptions({ coverageAnalysis: 'off', files: ['.stryker-tmp/sandbox2/foo.js'] })); + expect(jestTestAdapterMock.run).calledWithExactly( + sinon.match({ + fileNamesUnderTest: ['.stryker-tmp/sandbox2/foo.js'], + }) + ); + }); + + it('should not set fileNamesUnderTest if findRelatedTests = false', async () => { + options.jest.enableFindRelatedTests = false; + const sut = createSut(); + await sut.dryRun(factory.dryRunOptions({ coverageAnalysis: 'off', files: ['.stryker-tmp/sandbox2/foo.js'] })); + expect(jestTestAdapterMock.run).calledWithExactly( + sinon.match({ + fileNamesUnderTest: undefined, + }) + ); + }); + describe('coverage analysis', () => { it('should handle mutant coverage when coverage analysis != "off"', async () => { // Arrange @@ -476,7 +498,7 @@ describe(JestTestRunner.name, () => { }); describe('mutantRun', () => { - it('should use correct fileUnderTest if findRelatedTests = true', async () => { + it('should use correct fileNamesUnderTest if findRelatedTests = true', async () => { options.jest.enableFindRelatedTests = true; const sut = createSut(); await sut.mutantRun( @@ -486,12 +508,12 @@ describe(JestTestRunner.name, () => { sinon.match({ jestConfig: sinon.match.object, testNamePattern: undefined, - fileNameUnderTest: '.stryker-tmp/sandbox2/foo.js', + fileNamesUnderTest: ['.stryker-tmp/sandbox2/foo.js'], }) ); }); - it('should not set fileUnderTest if findRelatedTests = false', async () => { + it('should not set fileNamesUnderTest if findRelatedTests = false', async () => { options.jest.enableFindRelatedTests = false; const sut = createSut(); await sut.mutantRun(factory.mutantRunOptions({ activeMutant: factory.mutant() })); @@ -499,7 +521,7 @@ describe(JestTestRunner.name, () => { sinon.match({ jestConfig: sinon.match.object, testNamePattern: undefined, - fileNameUnderTest: undefined, + fileNamesUnderTest: undefined, }) ); });