UNPKG

@stryker-mutator/core

Version:

The extendable JavaScript mutation testing framework

258 lines 13.8 kB
import { EOL } from 'os'; import { assertions, factory, testInjector } from '@stryker-mutator/test-helpers'; import sinon from 'sinon'; import { expect } from 'chai'; import { mergeMap } from 'rxjs'; import { Timer } from '../../../src/utils/timer.js'; import { DryRunExecutor } from '../../../src/process/index.js'; import { coreTokens } from '../../../src/di/index.js'; import { ConfigError } from '../../../src/errors.js'; import { ConcurrencyTokenProvider } from '../../../src/concurrent/index.js'; import { createTestRunnerPoolMock } from '../../helpers/producers.js'; import { Sandbox } from '../../../src/sandbox/index.js'; import { Project } from '../../../src/fs/index.js'; import { FileSystemTestDouble } from '../../helpers/file-system-test-double.js'; describe(DryRunExecutor.name, () => { let injectorMock; let testRunnerPoolMock; let testRunnerCapabilities; let sut; let timerMock; let testRunnerMock; let concurrencyTokenProviderMock; let sandbox; let project; let reporterStub; beforeEach(() => { reporterStub = factory.reporter(); timerMock = sinon.createStubInstance(Timer); testRunnerCapabilities = factory.testRunnerCapabilities(); testRunnerMock = factory.testRunner(); testRunnerMock.capabilities.resolves(testRunnerCapabilities); testRunnerPoolMock = createTestRunnerPoolMock(); testRunnerPoolMock.schedule.callsFake((item$, task) => item$.pipe(mergeMap((item) => task(testRunnerMock, item)))); concurrencyTokenProviderMock = sinon.createStubInstance(ConcurrencyTokenProvider); injectorMock = factory.injector(); injectorMock.resolve.withArgs(coreTokens.testRunnerPool).returns(testRunnerPoolMock); sandbox = sinon.createStubInstance(Sandbox); const fsTestDouble = new FileSystemTestDouble({ 'bar.js': 'console.log("bar")' }); project = new Project(fsTestDouble, fsTestDouble.toFileDescriptions()); injectorMock.resolve.withArgs(coreTokens.project).returns(project); sut = new DryRunExecutor(injectorMock, testInjector.logger, testInjector.options, timerMock, concurrencyTokenProviderMock, sandbox, reporterStub); }); it('should pass through any rejections', async () => { const expectedError = new Error('expected error'); testRunnerMock.dryRun.rejects(expectedError); await expect(sut.execute()).rejectedWith(expectedError); }); describe('timeout', () => { let runResult; beforeEach(() => { runResult = factory.completeDryRunResult(); testRunnerMock.dryRun.resolves(runResult); runResult.tests.push(factory.successTestResult()); }); it('should use the configured timeout in ms if option provided', async () => { testInjector.options.dryRunTimeoutMinutes = 7.5; const timeoutMS = testInjector.options.dryRunTimeoutMinutes * 60 * 1000; await sut.execute(); expect(testRunnerMock.dryRun).calledWithMatch({ timeout: timeoutMS, }); }); it('should use the default timeout value if option not provided', async () => { const defaultTimeoutMS = 5 * 60 * 1000; await sut.execute(); expect(testRunnerMock.dryRun).calledWithMatch({ timeout: defaultTimeoutMS, }); }); }); describe('disable bail', () => { let runResult; beforeEach(() => { runResult = factory.completeDryRunResult(); testRunnerMock.dryRun.resolves(runResult); runResult.tests.push(factory.successTestResult()); }); it('should bail by default', async () => { await sut.execute(); expect(testRunnerMock.dryRun).calledWithMatch({ disableBail: false, }); }); it('should bail when given the option', async () => { testInjector.options.disableBail = true; await sut.execute(); expect(testRunnerMock.dryRun).calledWithMatch({ disableBail: true, }); }); }); describe('files', () => { const dryRunFileName = '.sandbox/bar.js'; let runResult; beforeEach(() => { sandbox.sandboxFileFor.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(); sinon.assert.calledWithMatch(testRunnerMock.dryRun, { files: [dryRunFileName], }); }); }); describe('when the dryRun completes', () => { let runResult; beforeEach(() => { runResult = factory.completeDryRunResult(); testRunnerMock.dryRun.resolves(runResult); }); it('should log about that this might take a while', async () => { runResult.tests.push(factory.successTestResult()); await sut.execute(); expect(testInjector.logger.info).calledWith('Starting initial test run (command test runner with "perTest" coverage analysis). This may take a while.'); }); it('should report "onDryRunCompleted" with expected timing', async () => { // Arrange const expectedOverHeadTimeMs = 90; const expectedNetTime = 90; runResult.tests.push(factory.successTestResult({ timeSpentMs: expectedNetTime })); timerMock.elapsedMs.returns(expectedOverHeadTimeMs + expectedNetTime); // Act await sut.execute(); // Assert sinon.assert.calledOnceWithExactly(reporterStub.onDryRunCompleted, { result: runResult, timing: { overhead: expectedOverHeadTimeMs, net: expectedNetTime, }, capabilities: testRunnerCapabilities, }); }); describe('with successful tests', () => { it('should calculate the overhead time milliseconds', async () => { // Arrange runResult.tests.push(factory.successTestResult({ timeSpentMs: 10 })); runResult.tests.push(factory.successTestResult({ timeSpentMs: 2 })); runResult.tests.push(factory.successTestResult({ timeSpentMs: 6 })); const expectedOverHeadTimeMs = 82; timerMock.elapsedMs.returns(100); // Act const actualResultInjector = await sut.execute(); // Assert expect(timerMock.mark).calledWith('Initial test run'); expect(timerMock.elapsedMs).calledWith('Initial test run'); expect(timerMock.mark).calledBefore(timerMock.elapsedMs); expect(actualResultInjector.provideValue).calledWithExactly(coreTokens.timeOverheadMS, expectedOverHeadTimeMs); }); it('should never calculate a negative overhead time', async () => { runResult.tests.push(factory.successTestResult({ timeSpentMs: 10 })); timerMock.elapsedMs.returns(9); const injector = await sut.execute(); expect(injector.provideValue).calledWithExactly(coreTokens.timeOverheadMS, 0); }); it('should provide the dry run result', async () => { timerMock.elapsedMs.returns(42); runResult.tests.push(factory.successTestResult()); runResult.mutantCoverage = { perTest: {}, static: {}, }; const actualInjector = await sut.execute(); expect(actualInjector.provideValue).calledWithExactly(coreTokens.dryRunResult, runResult); }); it('should remap test files that are reported', async () => { runResult.tests.push(factory.successTestResult({ fileName: '.stryker-tmp/sandbox-123/test/foo.spec.js' })); sandbox.originalFileFor.returns('test/foo.spec.js'); await sut.execute(); const actualDryRunResult = injectorMock.provideValue.getCalls().find((call) => call.args[0] === coreTokens.dryRunResult) .args[1]; assertions.expectCompleted(actualDryRunResult); expect(actualDryRunResult.tests[0].fileName).eq('test/foo.spec.js'); expect(sandbox.originalFileFor).calledWith('.stryker-tmp/sandbox-123/test/foo.spec.js'); }); it('should remap test locations when type checking was disabled for a test file', async () => { testInjector.options.disableTypeChecks = '{src,test}/**/*.js'; runResult.tests.push(factory.successTestResult({ fileName: '.stryker-tmp/sandbox-123/test/foo.spec.js', startPosition: { line: 3, column: 1 } }), factory.successTestResult({ fileName: '.stryker-tmp/sandbox-123/testResources/foo.spec.js', startPosition: { line: 5, column: 1 } })); sandbox.originalFileFor .withArgs('.stryker-tmp/sandbox-123/test/foo.spec.js') .returns('test/foo.spec.js') .withArgs('.stryker-tmp/sandbox-123/testResources/foo.spec.js') .returns('testResources/foo.spec.js'); await sut.execute(); const actualDryRunResult = injectorMock.provideValue.getCalls().find((call) => call.args[0] === coreTokens.dryRunResult) .args[1]; assertions.expectCompleted(actualDryRunResult); expect(actualDryRunResult.tests[0].startPosition).deep.eq({ line: 2, column: 1 }); expect(actualDryRunResult.tests[1].startPosition).deep.eq({ line: 5, column: 1 }); // should not have been remapped, since type checking wasn't disabled here }); it('should have logged the amount of tests ran', async () => { runResult.tests.push(factory.successTestResult({ timeSpentMs: 10 })); runResult.tests.push(factory.successTestResult({ timeSpentMs: 10 })); timerMock.humanReadableElapsed.returns('30 seconds'); timerMock.humanReadableElapsed.withArgs('Initial test run').returns('2 seconds'); timerMock.elapsedMs.returns(30000); timerMock.elapsedMs.withArgs('Initial test run').returns(2000); await sut.execute(); expect(testInjector.logger.info).to.have.been.calledWith('Initial test run succeeded. Ran %s tests in %s (net %s ms, overhead %s ms).', 2, '2 seconds', 20, 1980); }); it('should log when there were no tests and allowEmpty is set to false', async () => { testInjector.options.allowEmpty = false; await expect(sut.execute()).rejectedWith(ConfigError, 'No tests were executed. Stryker will exit prematurely. Please check your configuration.'); }); it('should prevent dryRun to throw an error when no tests are found and allowEmpty is set to true', async () => { testInjector.options.allowEmpty = true; await sut.execute(); expect(testInjector.logger.info).to.have.been.calledWith('No tests were found'); }); }); describe('with failed tests', () => { beforeEach(() => { runResult.tests.push(factory.failedTestResult({ name: 'foo is bar', failureMessage: 'foo was baz' })); runResult.tests.push(factory.failedTestResult({ name: 'bar is baz', failureMessage: 'bar was qux' })); }); it('should have logged the errors', async () => { await expect(sut.execute()).rejected; expect(testInjector.logger.error).calledWith(`One or more tests failed in the initial test run:${EOL}\tfoo is bar${EOL}\t\tfoo was baz${EOL}\tbar is baz${EOL}\t\tbar was qux`); }); it('should reject with correct message', async () => { await expect(sut.execute()).rejectedWith(ConfigError, 'There were failed tests in the initial test run.'); }); }); }); describe('when dryRun errors', () => { let runResult; beforeEach(() => { runResult = factory.errorDryRunResult(); testRunnerMock.dryRun.resolves(runResult); }); it('should have logged the errors', async () => { runResult.errorMessage = 'cannot call foo() on undefined'; await expect(sut.execute()).rejected; expect(testInjector.logger.error).calledWith(`One or more tests resulted in an error:${EOL}\tcannot call foo() on undefined`); }); it('should reject with correct message', async () => { await expect(sut.execute()).rejectedWith('Something went wrong in the initial test run'); }); }); describe('when dryRun timedOut', () => { let runResult; beforeEach(() => { runResult = factory.timeoutDryRunResult(); testRunnerMock.dryRun.resolves(runResult); }); it('should have logged the timeout', async () => { await expect(sut.execute()).rejected; expect(testInjector.logger.error).calledWith('Initial test run timed out!'); }); it('should reject with correct message', async () => { await expect(sut.execute()).rejectedWith('Something went wrong in the initial test run'); }); }); }); //# sourceMappingURL=3-dry-run-executor.spec.js.map