Skip to content

Commit

Permalink
feat(jest-runner): support --findRelatedTests in dry run (#3234)
Browse files Browse the repository at this point in the history
  • Loading branch information
Djaler committed Nov 9, 2021
1 parent f8ea774 commit b2e4584
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 21 deletions.
4 changes: 4 additions & 0 deletions packages/api/src/test-runner/run-options.ts
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/process/3-dry-run-executor.ts
Expand Up @@ -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';
Expand All @@ -38,6 +39,7 @@ export interface DryRunContext extends MutantInstrumenterContext {
[coreTokens.mutants]: readonly Mutant[];
[coreTokens.checkerPool]: I<Pool<Checker>>;
[coreTokens.concurrencyTokenProvider]: I<ConcurrencyTokenProvider>;
[coreTokens.inputFiles]: InputFileCollection;
}

/**
Expand Down Expand Up @@ -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.`
Expand All @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions packages/core/test/unit/process/3-dry-run-executor.spec.ts
Expand Up @@ -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';
Expand All @@ -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<Injector<MutationTestContext>>;
Expand All @@ -26,6 +28,7 @@ describe(DryRunExecutor.name, () => {
let testRunnerMock: sinon.SinonStubbedInstance<Required<TestRunner>>;
let concurrencyTokenProviderMock: sinon.SinonStubbedInstance<ConcurrencyTokenProvider>;
let sandbox: sinon.SinonStubbedInstance<Sandbox>;
let inputFiles: InputFileCollection;

beforeEach(() => {
timerMock = sinon.createStubInstance(Timer);
Expand All @@ -41,6 +44,8 @@ describe(DryRunExecutor.name, () => {
injectorMock = factory.injector();
injectorMock.resolve.withArgs(coreTokens.testRunnerPool).returns(testRunnerPoolMock as I<Pool<TestRunner>>);
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<DryRunContext>,
testInjector.logger,
Expand Down Expand Up @@ -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;

Expand Down
Expand Up @@ -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<JestRunResult> {
public async run({ jestConfig, fileNamesUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise<JestRunResult> {
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,
Expand Down
Expand Up @@ -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<JestRunResult> {
public run({ jestConfig, fileNamesUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise<JestRunResult> {
const config = JSON.stringify(jestConfig);
return jestWrapper.runCLI(
{
$0: 'stryker',
_: fileNameUnderTest ? [fileNameUnderTest] : [],
findRelatedTests: !!fileNameUnderTest,
_: fileNamesUnderTest ? fileNamesUnderTest : [],
findRelatedTests: !!fileNamesUnderTest,
config,
runInBand: true,
silent: true,
Expand Down
Expand Up @@ -5,7 +5,7 @@ import { JestRunResult } from '../jest-run-result';
export interface RunSettings {
jestConfig: Config.InitialOptions;
testNamePattern?: string;
fileNameUnderTest?: string;
fileNamesUnderTest?: string[];
testLocationInResults?: boolean;
}

Expand Down
26 changes: 20 additions & 6 deletions 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 {
Expand Down Expand Up @@ -91,7 +91,11 @@ export class JestTestRunner implements TestRunner {
}
}

public async dryRun({ coverageAnalysis, disableBail }: Pick<DryRunOptions, 'coverageAnalysis' | 'disableBail'>): Promise<DryRunResult> {
public async dryRun({
coverageAnalysis,
disableBail,
files,
}: Pick<DryRunOptions, 'coverageAnalysis' | 'disableBail' | 'files'>): Promise<DryRunResult> {
state.coverageAnalysis = coverageAnalysis;
const mutantCoverage: MutantCoverage = { perTest: {}, static: {} };
const fileNamesWithMutantCoverage: string[] = [];
Expand All @@ -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') {
Expand Down Expand Up @@ -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,
});
Expand All @@ -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;
Expand Down
Expand Up @@ -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(() => {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 26 additions & 4 deletions packages/jest-runner/test/unit/jest-test-runner.spec.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -486,20 +508,20 @@ 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() }));
expect(jestTestAdapterMock.run).calledWithExactly(
sinon.match({
jestConfig: sinon.match.object,
testNamePattern: undefined,
fileNameUnderTest: undefined,
fileNamesUnderTest: undefined,
})
);
});
Expand Down

0 comments on commit b2e4584

Please sign in to comment.