UNPKG

@stryker-mutator/core

Version:

The extendable JavaScript mutation testing framework

746 lines (740 loc) 37.7 kB
import path from 'path'; import { MutantStatus } from '@stryker-mutator/api/core'; import { factory, testInjector } from '@stryker-mutator/test-helpers'; import { deepFreeze } from '@stryker-mutator/util'; import { expect } from 'chai'; import chalk from 'chalk'; import sinon from 'sinon'; import { IncrementalDiffer } from '../../../src/mutants/index.js'; import { createMutant, loc, pos } from '../../helpers/producers.js'; import { TestCoverageTestDouble } from '../../helpers/test-coverage-test-double.js'; // Keep this files here for the indenting const srcAddContent = `export function add(a, b) { return a + b; } `; const testAddContent = `import { expect } from 'chai'; import { add } from '../src/add.js'; describe('add' () => { it('should result in 42 for 2 and 40', () => { expect(add(40, 2)).eq(42); }); }); `; const testAddContentTwoTests = `import { expect } from 'chai'; import { add } from '../src/add.js'; describe('add' () => { it('should result in 42 for 2 and 40', () => { expect(add(40, 2)).eq(42); }); it('should result in 42 for 45 and -3', () => { expect(add(45, -3)).eq(42); }); }); `; const testAddContentWithTestGenerationAndAdditionalTest = `import { expect } from 'chai'; import { add } from '../src/add.js'; describe('add' () => { for(const [a, b, answer] of [[40, 2, 42], [45, -3, 42]]) { it(\`should result in \${answer} for \${a} and \${b}\`, () => { expect(add(a, b)).eq(answer); }); } it('should have name "add"', () => { expect(add.name).eq('add'); }); }); `; const testAddContentWithTestGeneration = `import { add } from '../add.js'; test.each\` x | y | expected ${1} | ${1} | ${2} ${1} | ${2} | ${3} ${2} | ${2} | ${4} \`("add($x, $y) = $expected", ({ x, y, expected }) => { expect(add(x, y)).toBe(expected); });`; const testAddContentWithTestGenerationAndAdditionalTestUpdated = `import { expect } from 'chai'; import { add } from '../src/add.js'; describe('add' () => { for(const [a, b, answer] of [[40, 2, 42], [45, -3, 42]]) { it(\`should result in \${answer} for \${a} and \${b}\`, () => { expect(add(a, b)).eq(answer); }); } it('should have name "add"', () => { // Add a comment as change expect(add.name).eq('add'); }); }); `; const srcMultiplyContent = `export function multiply(a, b) { return a * b; }`; const testMultiplyContent = `import { expect } from 'chai'; import { multiply } from '../src/multiply.js'; describe('multiply' () => { it('should result in 42 for 2 and 21', () => { expect(multiply(2, 21)).eq(42); }); }); `; const srcAdd = 'src/add.js'; const srcMultiply = 'src/multiply.js'; const testAdd = 'test/add.spec.js'; const testMultiply = 'test/multiply.spec.js'; class ScenarioBuilder { constructor() { this.oldSpecId = 'spec-1'; this.newTestId = 'new-spec-2'; this.mutantId = '2'; this.incrementalFiles = {}; this.incrementalTestFiles = {}; this.currentFiles = new Map(); this.mutants = []; this.testCoverage = new TestCoverageTestDouble(); } withMathProjectExample({ mutantState: mutantStatus = MutantStatus.Killed, isStatic = false } = {}) { this.mutants.push(createMutant({ id: this.mutantId, fileName: srcAdd, replacement: '-', mutatorName: 'min-replacement', location: loc(1, 11, 1, 12) })); this.incrementalFiles[srcAdd] = factory.mutationTestReportSchemaFileResult({ mutants: [ factory.mutationTestReportSchemaMutantResult({ id: 'mut-1', coveredBy: isStatic ? undefined : [this.oldSpecId], killedBy: [this.oldSpecId], replacement: '-', mutatorName: 'min-replacement', statusReason: 'Killed by first test', testsCompleted: 1, status: mutantStatus, location: loc(1, 11, 1, 12), }), ], source: srcAddContent, }); this.incrementalTestFiles[testAdd] = factory.mutationTestReportSchemaTestFile({ tests: [{ id: this.oldSpecId, name: 'add(2, 0) = 2' }], source: testAddContent, }); this.currentFiles.set(srcAdd, srcAddContent); this.currentFiles.set(testAdd, testAddContent); this.testCoverage.addTest(factory.testResult({ id: this.newTestId, fileName: testAdd, name: 'add(2, 0) = 2' })); if (isStatic) { this.testCoverage.hasCoverage = true; this.testCoverage.staticCoverage[this.mutantId] = true; } else { this.testCoverage.addCoverage(this.mutantId, [this.newTestId]); } return this; } withoutTestCoverage() { Object.keys(this.incrementalTestFiles).forEach((testFile) => delete this.incrementalTestFiles[testFile]); this.testCoverage.clear(); this.testCoverage.hasCoverage = false; return this; } withTestFile() { this.currentFiles.set(testAdd, testAddContent); this.incrementalTestFiles[testAdd].source = testAddContent; return this; } withLocatedTest({ includeEnd = false } = {}) { this.incrementalTestFiles[testAdd].tests[0].location = loc(4, 2); if (includeEnd) { this.incrementalTestFiles[testAdd].tests[0].location.end = pos(6, 5); } [...this.testCoverage.forMutant(this.mutantId)][0].startPosition = pos(4, 2); return this; } withAddedLinesAboveTest(...lines) { this.currentFiles.set(testAdd, `${lines.join('\n')}\n${testAddContent}`); for (const test of this.testCoverage.forMutant(this.mutantId)) { if (test.startPosition) { test.startPosition = pos(4 + lines.length, 2); } } return this; } withAddedLinesAboveMutant(...lines) { this.currentFiles.set(srcAdd, `${lines.join('\n')}\n${srcAddContent}`); this.mutants[0].location = loc(1 + lines.length, 11, 1 + lines.length, 12); return this; } withCrlfLineEndingsInIncrementalReport() { Object.values(this.incrementalFiles).forEach((file) => { file.source = file.source.replace(/\n/g, '\r\n'); }); Object.values(this.incrementalTestFiles).forEach((file) => { var _a; file.source = (_a = file.source) === null || _a === void 0 ? void 0 : _a.replace(/\n/g, '\r\n'); }); return this; } withRemovedLinesAboveMutant(...lines) { this.incrementalFiles[srcAdd].source = `${lines.join('\n')}\n${srcAddContent}`; this.incrementalFiles[srcAdd].mutants[0].location = loc(1 + lines.length, 11, 1 + lines.length, 12); return this; } withAddedTextBeforeMutant(text) { this.currentFiles.set(srcAdd, srcAddContent .split('\n') .map((line, nr) => (nr === 1 ? `${text}${line}` : line)) .join('\n')); this.mutants[0].location = loc(1, 11 + text.length, 1, 12 + text.length); return this; } withAddedTextBeforeTest(text) { this.currentFiles.set(testAdd, testAddContent .split('\n') .map((line, nr) => (nr === 4 ? `${text}${line}` : line)) .join('\n')); for (const test of this.testCoverage.forMutant(this.mutantId)) { if (test.startPosition) { test.startPosition = pos(4, 2 + text.length); } } return this; } withAddedCodeInsideTheTest(code) { this.currentFiles.set(testAdd, testAddContent .split('\n') .map((line, nr) => (nr === 5 ? ` ${code}\n${line}` : line)) .join('\n')); for (const test of this.testCoverage.forMutant(this.mutantId)) { if (test.startPosition) { test.startPosition = pos(4, 2); } } return this; } withSecondTest({ located }) { this.currentFiles.set(testAdd, testAddContentTwoTests); const secondTest = factory.testResult({ id: 'spec2', fileName: testAdd, name: 'add(45, -3) = 42' }); if (located) { secondTest.startPosition = pos(7, 2); } this.testCoverage.addTest(secondTest); this.testCoverage.addCoverage(this.mutantId, [secondTest.id]); return this; } withSecondTestInIncrementalReport({ isKillingTest = false } = {}) { this.currentFiles.set(testAdd, testAddContentTwoTests); this.incrementalTestFiles[testAdd].tests.unshift(factory.mutationTestReportSchemaTestDefinition({ id: 'spec2', name: 'add(45, -3) = 42', location: loc(7, 0) })); if (isKillingTest) { this.incrementalFiles[srcAdd].mutants[0].killedBy = ['spec2']; } if (this.incrementalTestFiles[testAdd].source) { this.incrementalTestFiles[testAdd].source = testAddContentTwoTests; } return this; } withTestGeneration() { this.currentFiles.set(testAdd, testAddContentWithTestGeneration); const generateTest = (id, a, b, answer) => factory.testResult({ id, fileName: testAdd, name: `add(${a}, ${b}) = ${answer}`, startPosition: pos(8, 3) }); this.testCoverage.clear(); this.testCoverage.addTest(generateTest('spec1', 1, 1, 2), generateTest('spec2', 1, 2, 3), generateTest('spec3', 2, 2, 4)); this.testCoverage.addCoverage(this.mutantId, ['spec1', 'spec2', 'spec3']); return this; } withTestGenerationIncrementalReport() { this.incrementalTestFiles[testAdd].source = testAddContentWithTestGeneration; const generateTest = (id, a, b, answer) => factory.mutationTestReportSchemaTestDefinition({ id, name: `add(${a}, ${b}) = ${answer}`, location: loc(8, 3), }); // clear all tests while (this.incrementalTestFiles[testAdd].tests.shift()) { } this.incrementalTestFiles[testAdd].tests.push(generateTest('spec4', 1, 1, 2), generateTest('spec5', 1, 2, 3), generateTest('spec6', 2, 2, 4)); this.incrementalFiles[srcAdd].mutants[0].coveredBy = ['spec4', 'spec5', 'spec6']; this.incrementalFiles[srcAdd].mutants[0].killedBy = ['spec4']; return this; } withTestGenerationAndAdditionalTest() { this.currentFiles.set(testAdd, testAddContentWithTestGenerationAndAdditionalTest); const createAddWithTestGenerationTestResult = (a, b, answer) => factory.testResult({ id: `spec${a}`, fileName: testAdd, name: `should result in ${answer} for ${a} and ${b}`, startPosition: pos(5, 4) }); this.testCoverage.clear(); this.testCoverage.addTest(factory.testResult({ id: 'new-spec-2', fileName: testAdd, name: 'should have name "add"', startPosition: pos(9, 2) })); const gen1 = createAddWithTestGenerationTestResult(40, 2, 42); const gen2 = createAddWithTestGenerationTestResult(45, -3, 42); this.testCoverage.addTest(gen1, gen2); this.testCoverage.addCoverage(this.mutantId, ['new-spec-2', gen1.id, gen2.id]); return this; } withUpdatedTestGenerationAndAdditionalTest() { this.currentFiles.set(testAdd, testAddContentWithTestGenerationAndAdditionalTestUpdated); const createAddWithTestGenerationTestResult = (a, b, answer) => factory.testResult({ id: `spec${a}`, fileName: testAdd, name: `should result in ${answer} for ${a} and ${b}`, startPosition: pos(5, 4) }); this.testCoverage.clear(); this.testCoverage.addTest(factory.testResult({ id: 'new-spec-2', fileName: testAdd, name: 'should have name "add"', startPosition: pos(9, 2) })); const gen1 = createAddWithTestGenerationTestResult(40, 2, 42); const gen2 = createAddWithTestGenerationTestResult(45, -3, 42); this.testCoverage.addTest(gen1, gen2); this.testCoverage.addCoverage(this.mutantId, ['new-spec-2', gen1.id, gen2.id]); return this; } withTestGenerationAndAdditionalTestIncrementalReport() { this.incrementalTestFiles[testAdd].source = testAddContentWithTestGenerationAndAdditionalTest; const createAddWithTestGenerationTestDefinition = (id, a, b, answer) => factory.mutationTestReportSchemaTestDefinition({ id, name: `should result in ${answer} for ${a} and ${b}`, location: loc(5, 4), }); // clear all tests while (this.incrementalTestFiles[testAdd].tests.shift()) { } this.incrementalTestFiles[testAdd].tests.push(factory.mutationTestReportSchemaTestDefinition({ id: 'spec3', name: 'should have name "add"', location: loc(9, 2) }), createAddWithTestGenerationTestDefinition('spec4', 40, 2, 42), createAddWithTestGenerationTestDefinition('spec5', 45, -3, 42)); this.incrementalFiles[srcAdd].mutants[0].coveredBy = ['spec4', 'spec5']; this.incrementalFiles[srcAdd].mutants[0].killedBy = ['spec4']; return this; } withRemovedTextBeforeMutant(text) { this.incrementalFiles[srcAdd].source = srcAddContent .split('\n') .map((line, nr) => (nr === 1 ? `${text}${line}` : line)) .join('\n'); this.incrementalFiles[srcAdd].mutants[0].location = loc(1, 11 + text.length, 1, 12 + text.length); return this; } withAddedTextAfterTest(text) { const cnt = testAddContent .split('\n') .map((line, nr) => `${line}${nr === 6 ? text : ''}`) .join('\n'); this.currentFiles.set(testAdd, cnt); return this; } withChangedMutantText(replacement) { this.currentFiles.set(srcAdd, srcAddContent.replace('+', replacement)); return this; } withDifferentMutator(mutatorName) { this.mutants[0].mutatorName = mutatorName; return this; } withDifferentReplacement(replacement) { this.mutants[0].replacement = replacement; return this; } withDifferentMutantLocation() { this.incrementalFiles[srcAdd].mutants[0].location = loc(2, 11, 2, 12); return this; } withDifferentFileName(fileName) { this.incrementalFiles[fileName] = this.incrementalFiles[srcAdd]; delete this.incrementalFiles[srcAdd]; return this; } withSecondSourceAndTestFileInIncrementalReport() { this.incrementalTestFiles[testMultiply] = factory.mutationTestReportSchemaTestFile({ source: testMultiplyContent, tests: [ factory.mutationTestReportSchemaTestDefinition({ id: 'spec-3', location: loc(4, 2), name: 'multiply should result in 42 for 2 and 21' }), ], }); this.incrementalFiles[srcMultiply] = factory.mutationTestReportSchemaFileResult({ mutants: [ factory.mutationTestReportSchemaMutantResult({ id: 'mut-3', coveredBy: ['spec-3'], killedBy: ['spec-3'], replacement: '/', testsCompleted: 1, status: MutantStatus.Killed, location: loc(1, 11, 1, 12), }), ], source: srcMultiplyContent, }); return this; } withSecondSourceFile() { this.currentFiles.set(srcMultiply, srcMultiplyContent); return this; } withSecondTestFile() { this.currentFiles.set(testMultiply, testMultiplyContent); return this; } withRemovedTestFile() { this.currentFiles.delete(testAdd); this.testCoverage.clear(); this.testCoverage.hasCoverage = false; return this; } withEmptyFileNameTestFile() { this.incrementalTestFiles[''] = this.incrementalTestFiles[testAdd]; delete this.incrementalTestFiles[testAdd]; this.testCoverage.clear(); this.testCoverage.addTest(factory.testResult({ id: this.newTestId, name: 'add(2, 0) = 2' })); this.testCoverage.addCoverage(this.mutantId, [this.newTestId]); return this; } act() { this.sut = testInjector.injector.injectClass(IncrementalDiffer); deepFreeze(this.mutants); // make sure mutants aren't changed at all return this.sut.diff(this.mutants, this.testCoverage, factory.mutationTestReportSchemaMutationTestResult({ files: this.incrementalFiles, testFiles: this.incrementalTestFiles, }), this.currentFiles); } } describe(IncrementalDiffer.name, () => { describe('mutant changes', () => { it('should copy status, statusReason, testsCompleted if nothing changed', () => { // Arrange const actualDiff = new ScenarioBuilder().withMathProjectExample().act(); // Assert const actualMutant = actualDiff[0]; const expected = { id: '2', fileName: srcAdd, replacement: '-', mutatorName: 'min-replacement', location: loc(1, 11, 1, 12), status: MutantStatus.Killed, statusReason: 'Killed by first test', testsCompleted: 1, }; expect(actualMutant).deep.contains(expected); }); it('should not reuse the result when --force is active', () => { // Arrange testInjector.options.force = true; const actualDiff = new ScenarioBuilder().withMathProjectExample().act(); // Assert const actualMutant = actualDiff[0]; expect(actualMutant.status).undefined; }); it('should not reuse when the mutant was ignored', () => { // Arrange const actualDiff = new ScenarioBuilder().withMathProjectExample({ mutantState: MutantStatus.Ignored }).act(); // Assert const actualMutant = actualDiff[0]; expect(actualMutant.status).undefined; }); it('should normalize line endings when comparing diffs', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample() .withTestFile() .withLocatedTest() .withCrlfLineEndingsInIncrementalReport() .act(); const actualMutant = actualDiff[0]; expect(actualMutant.status).eq(MutantStatus.Killed); }); it('should map killedBy and coveredBy to the new test ids if a mutant result is reused', () => { const scenario = new ScenarioBuilder().withMathProjectExample(); const actualDiff = scenario.act(); const actualMutant = actualDiff[0]; const expectedTestIds = [scenario.newTestId]; const expected = { coveredBy: expectedTestIds, killedBy: expectedTestIds, }; expect(actualMutant).deep.contains(expected); }); it("should identify that a mutant hasn't changed if lines got added above", () => { const actualDiff = new ScenarioBuilder().withMathProjectExample().withAddedLinesAboveMutant("import path from 'path';", '', '').act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it("should identify that a mutant hasn't changed if characters got added before", () => { const actualDiff = new ScenarioBuilder().withMathProjectExample().withAddedTextBeforeMutant("/* text added this shouldn't matter */").act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it("should identify that a mutant hasn't changed if lines got removed above", () => { const actualDiff = new ScenarioBuilder().withMathProjectExample().withRemovedLinesAboveMutant('import path from "path";', '').act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it("should identify that a mutant hasn't changed if characters got removed before", () => { const actualDiff = new ScenarioBuilder().withMathProjectExample().withRemovedTextBeforeMutant("/* text removed, this shouldn't matter*/").act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should not reuse the status of a mutant in changed text', () => { const actualDiff = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*').act(); expect(actualDiff[0].status).undefined; }); it('should reuse the status when there is no test coverage', () => { const actualDiff = new ScenarioBuilder().withMathProjectExample().withoutTestCoverage().act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should reuse the status when there is a test with empty file name', () => { const actualDiff = new ScenarioBuilder().withMathProjectExample().withEmptyFileNameTestFile().act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should not copy the status if the mutant came from a different mutator', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentMutator('max-replacement'); const actualDiff = scenario.act(); expect(actualDiff[0]).deep.eq(scenario.mutants[0]); }); it('should not copy the status if the mutant has a different replacement', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentReplacement('other replacement'); const actualDiff = scenario.act(); expect(actualDiff[0]).deep.eq(scenario.mutants[0]); }); it('should not copy the status if the mutant has a different location', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentMutantLocation(); const actualDiff = scenario.act(); expect(actualDiff[0]).deep.eq(scenario.mutants[0]); }); it('should not copy the status if the mutant has a different file name', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentFileName('src/some-other-file.js'); const actualDiff = scenario.act(); expect(actualDiff).deep.eq(scenario.mutants); }); it('should collect 1 added mutant and 1 removed mutant if the mutant changed', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*'); scenario.act(); expect(scenario.sut.mutantStatisticsCollector.changesByFile).lengthOf(1); const changes = scenario.sut.mutantStatisticsCollector.changesByFile.get(srcAdd); expect(changes).property('added', 1); expect(changes).property('removed', 1); }); it('should collect the removed mutants if the file got removed', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentFileName('src/some-other-file.js'); scenario.act(); expect(scenario.sut.mutantStatisticsCollector.changesByFile).lengthOf(2); const changesOldFile = scenario.sut.mutantStatisticsCollector.changesByFile.get('src/some-other-file.js'); const changesNewFile = scenario.sut.mutantStatisticsCollector.changesByFile.get(srcAdd); expect(changesNewFile).property('added', 1); expect(changesNewFile).property('removed', 0); expect(changesOldFile).property('added', 0); expect(changesOldFile).property('removed', 1); }); it('should collect 1 added mutant and 1 removed mutant if a mutant changed', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*'); scenario.act(); expect(scenario.sut.mutantStatisticsCollector.changesByFile).lengthOf(1); const changes = scenario.sut.mutantStatisticsCollector.changesByFile.get(srcAdd); expect(changes).property('added', 1); expect(changes).property('removed', 1); }); it('should log an incremental report', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*'); testInjector.logger.isInfoEnabled.returns(true); scenario.act(); const { mutantStatisticsCollector, testStatisticsCollector } = scenario.sut; sinon.assert.calledWithExactly(testInjector.logger.info, `Incremental report:\n\tMutants:\t${mutantStatisticsCollector.createTotalsReport()}` + `\n\tTests:\t\t${testStatisticsCollector.createTotalsReport()}` + `\n\tResult:\t\t${chalk.yellowBright(0)} of 1 mutant result(s) are reused.`); }); it('should not log test diff when there is no test coverage', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withoutTestCoverage(); testInjector.logger.isInfoEnabled.returns(true); scenario.act(); const { mutantStatisticsCollector } = scenario.sut; sinon.assert.calledWithExactly(testInjector.logger.info, `Incremental report:\n\tMutants:\t${mutantStatisticsCollector.createTotalsReport()}` + `\n\tResult:\t\t${chalk.yellowBright(1)} of 1 mutant result(s) are reused.`); }); it('should log a detailed incremental report', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*'); testInjector.logger.isDebugEnabled.returns(true); scenario.act(); const { mutantStatisticsCollector } = scenario.sut; const lineSeparator = '\n\t\t'; const detailedMutantSummary = `${lineSeparator}${mutantStatisticsCollector.createDetailedReport().join(lineSeparator)}`; sinon.assert.calledWithExactly(testInjector.logger.debug, `Detailed incremental report:\n\tMutants: ${detailedMutantSummary}\n\tTests: ${lineSeparator}No changes`); }); it('should not log if logLevel "info" or "debug" aren\'t enabled', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*'); testInjector.logger.isInfoEnabled.returns(false); testInjector.logger.isDebugEnabled.returns(false); scenario.act(); sinon.assert.notCalled(testInjector.logger.debug); sinon.assert.notCalled(testInjector.logger.info); }); }); describe('test changes', () => { it('should identify that a mutant state can be reused when no tests changed', () => { const actualDiff = new ScenarioBuilder().withMathProjectExample().withTestFile().act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should identify that mutant state can be reused with changes above', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample() .withTestFile() .withLocatedTest() .withAddedLinesAboveTest("import foo from 'bar'", '') .act(); // Assert expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should identify that mutant state can be reused with changes before', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample() .withTestFile() .withLocatedTest() .withAddedTextBeforeTest('/*text-added*/') .act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should identify that mutant state can be reused with changes below', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample() .withTestFile() .withLocatedTest({ includeEnd: true }) .withSecondTest({ located: true }) .act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should identify that mutant state can be reused with changes behind', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample() .withTestFile() .withLocatedTest({ includeEnd: true }) .withAddedTextAfterTest('/*text-added*/') .act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should not reuse a mutant state when a covering test gets code added', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample() .withTestFile() .withLocatedTest({ includeEnd: true }) .withAddedCodeInsideTheTest('addedText();') .act(); expect(actualDiff[0].status).undefined; }); it('should close locations of tests in the incremental report', () => { // All test runners currently only report the start positions of tests. // Add a workaround for 'inventing' the end position based on the next test's start position. const actualDiff = new ScenarioBuilder() .withMathProjectExample() .withTestFile() .withLocatedTest({ includeEnd: true }) .withSecondTest({ located: true }) .withSecondTestInIncrementalReport() .act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should close locations for tests on the same location in the incremental report', () => { // Test cases can generate tests, make sure the correct end position is chosen in those cases const actualDiff = new ScenarioBuilder() .withMathProjectExample() .withUpdatedTestGenerationAndAdditionalTest() .withTestGenerationAndAdditionalTestIncrementalReport() .act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); // See #3909 it('should close locations for tests on the same location in the incremental report when they are the last tests', () => { // Test cases can generate tests, make sure the correct end position is chosen in those cases const actualDiff = new ScenarioBuilder().withMathProjectExample().withTestGeneration().withTestGenerationIncrementalReport().act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should identify that a non-"Killed" state can be reused when a test is removed', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample({ mutantState: MutantStatus.Survived }) .withSecondTestInIncrementalReport() .act(); expect(actualDiff[0].status).eq(MutantStatus.Survived); }); it('should identify that a non-"Killed" state cannot be reused when a test is added', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample({ mutantState: MutantStatus.Survived }) .withSecondTest({ located: false }) .act(); expect(actualDiff[0].status).undefined; }); it('should identify that a "Killed" state can be reused when the killing test didn\'t change', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample({ mutantState: MutantStatus.Killed }) .withTestFile() .withLocatedTest() .withSecondTestInIncrementalReport() .act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should identify that a "Killed" state cannot be reused when the killing test was removed', () => { const actualDiff = new ScenarioBuilder() .withMathProjectExample({ mutantState: MutantStatus.Killed }) .withTestFile() .withSecondTestInIncrementalReport({ isKillingTest: true }) .act(); expect(actualDiff[0].status).undefined; }); it('should identify that a "Killed" state for a static mutant (no covering tests) can be reused when the killing test didn\'t change', () => { const actualDiff = new ScenarioBuilder().withMathProjectExample({ mutantState: MutantStatus.Killed, isStatic: true }).act(); expect(actualDiff[0].status).eq(MutantStatus.Killed); }); it('should collect an added test', () => { const scenario = new ScenarioBuilder() .withMathProjectExample() .withTestFile() .withLocatedTest({ includeEnd: true }) .withSecondTest({ located: true }); scenario.act(); const actualCollector = scenario.sut.testStatisticsCollector; expect(actualCollector.changesByFile).lengthOf(1); const changes = actualCollector.changesByFile.get(testAdd); expect(changes).property('added', 1); expect(changes).property('removed', 0); }); it('should collect a removed test', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withRemovedTestFile(); scenario.act(); const actualCollector = scenario.sut.testStatisticsCollector; expect(actualCollector.changesByFile).lengthOf(1); const changes = actualCollector.changesByFile.get(testAdd); expect(changes).property('added', 0); expect(changes).property('removed', 1); sinon.assert.calledWithExactly(testInjector.logger.debug, 'Test file removed: %s', testAdd); }); it('should collect an added and removed test when a test changes', () => { const scenario = new ScenarioBuilder() .withMathProjectExample() .withTestFile() .withLocatedTest() .withAddedCodeInsideTheTest('arrangeSomething()'); scenario.act(); const actualCollector = scenario.sut.testStatisticsCollector; expect(actualCollector.changesByFile).lengthOf(1); const changes = actualCollector.changesByFile.get(testAdd); expect(changes).property('added', 1); expect(changes).property('removed', 1); }); }); describe('with history', () => { it('should keep historic mutants in other files', () => { const scenario = new ScenarioBuilder().withMathProjectExample().withSecondSourceAndTestFileInIncrementalReport().withSecondSourceFile(); const mutants = scenario.act(); expect(mutants).lengthOf(2); const actualMutant = mutants[1]; expect(actualMutant.id).includes('src/multiply.js@1:11-1:12'); expect(actualMutant.status).eq(MutantStatus.Killed); expect(actualMutant.fileName).eq(path.resolve(srcMultiply)); }); it("should keep historic tests that didn't run this time around", () => { const scenario = new ScenarioBuilder() .withMathProjectExample() .withSecondSourceAndTestFileInIncrementalReport() .withSecondSourceFile() .withSecondTestFile(); const mutants = scenario.act(); const actualTest = scenario.testCoverage.testsById.get(`${testMultiply}@4:2\nmultiply should result in 42 for 2 and 21`); expect(actualTest).ok; expect(actualTest.fileName).eq(path.resolve(testMultiply)); expect(actualTest.name).eq('multiply should result in 42 for 2 and 21'); expect(actualTest.startPosition).deep.eq(pos(4, 2)); expect(scenario.testCoverage.forMutant(mutants[1].id)).deep.eq(new Set([actualTest])); }); it('should not keep historic mutants when they are inside of a mutated file', () => { testInjector.fileDescriptions[path.resolve(srcMultiply)] = { mutate: true }; const scenario = new ScenarioBuilder().withMathProjectExample().withSecondSourceAndTestFileInIncrementalReport().withSecondSourceFile(); const mutants = scenario.act(); expect(mutants).lengthOf(1); }); it('should not keep historic mutants when they are inside of a mutated scope of a file', () => { testInjector.fileDescriptions[path.resolve(srcMultiply)] = { mutate: [loc(1, 11, 1, 12), loc(2, 2, 2, 3)] }; const scenario = new ScenarioBuilder().withMathProjectExample().withSecondSourceAndTestFileInIncrementalReport().withSecondSourceFile(); const mutants = scenario.act(); expect(mutants).lengthOf(1); }); it('should keep historic mutants when they are outside of a mutated scope of a file', () => { testInjector.fileDescriptions[path.resolve(srcMultiply)] = { mutate: [loc(1, 9, 1, 10), loc(2, 11, 2, 12)] }; const scenario = new ScenarioBuilder().withMathProjectExample().withSecondSourceAndTestFileInIncrementalReport().withSecondSourceFile(); const mutants = scenario.act(); expect(mutants).lengthOf(2); }); }); }); //# sourceMappingURL=incremental-differ.spec.js.map