Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: run initial test using jest findRelatedTests #3234

Merged
merged 1 commit into from
Nov 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/api/src/test-runner/run-options.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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