How to use detailedMutantSummary method in stryker-parent

Best JavaScript code snippet using stryker-parent

incremental-differ.spec.ts

Source:incremental-differ.spec.ts Github

copy

Full Screen

1import path from 'path';2import { Mutant, MutantStatus, schema } from '@stryker-mutator/api/core';3import { factory, testInjector } from '@stryker-mutator/test-helpers';4import { deepFreeze } from '@stryker-mutator/util';5import { expect } from 'chai';6import chalk from 'chalk';7import sinon from 'sinon';8import { IncrementalDiffer } from '../../../src/mutants/index.js';9import { createMutant, loc, pos } from '../../helpers/producers.js';10import { TestCoverageTestDouble } from '../../helpers/test-coverage-test-double.js';11// Keep this files here for the indenting12const srcAddContent = `export function add(a, b) {13 return a + b;14} 15`;16const testAddContent = `import { expect } from 'chai';17import { add } from '../src/add.js';18describe('add' () => {19 it('should result in 42 for 2 and 40', () => {20 expect(add(40, 2)).eq(42);21 });22});23`;24const testAddContentTwoTests = `import { expect } from 'chai';25import { add } from '../src/add.js';26describe('add' () => {27 it('should result in 42 for 2 and 40', () => {28 expect(add(40, 2)).eq(42);29 });30 it('should result in 42 for 45 and -3', () => {31 expect(add(45, -3)).eq(42);32 });33});34`;35const testAddContentWithTestGeneration = `import { expect } from 'chai';36import { add } from '../src/add.js';37describe('add' () => {38 for(const [a, b, answer] of [[40, 2, 42], [45, -3, 42]]) {39 it(\`should result in \${answer} for \${a} and \${b}\`, () => {40 expect(add(a, b)).eq(answer);41 });42 }43 it('should have name "add"', () => {44 expect(add.name).eq('add');45 });46});47`;48const testAddContentWithTestGenerationUpdated = `import { expect } from 'chai';49import { add } from '../src/add.js';50describe('add' () => {51 for(const [a, b, answer] of [[40, 2, 42], [45, -3, 42]]) {52 it(\`should result in \${answer} for \${a} and \${b}\`, () => {53 expect(add(a, b)).eq(answer);54 });55 }56 it('should have name "add"', () => {57 // Add a comment as change58 expect(add.name).eq('add');59 });60});61`;62const srcMultiplyContent = `export function multiply(a, b) {63 return a * b;64}`;65const testMultiplyContent = `import { expect } from 'chai';66import { multiply } from '../src/multiply.js';67describe('multiply' () => {68 it('should result in 42 for 2 and 21', () => {69 expect(multiply(2, 21)).eq(42);70 });71});72`;73const srcAdd = 'src/add.js';74const srcMultiply = 'src/multiply.js';75const testAdd = 'test/add.spec.js';76const testMultiply = 'test/multiply.spec.js';77class ScenarioBuilder {78 public readonly oldSpecId = 'spec-1';79 public readonly newTestId = 'new-spec-2';80 public readonly mutantId = '2';81 public incrementalFiles: schema.FileResultDictionary = {};82 public incrementalTestFiles: schema.TestFileDefinitionDictionary = {};83 public currentFiles = new Map<string, string>();84 public mutants: Mutant[] = [];85 public testCoverage = new TestCoverageTestDouble();86 public sut?: IncrementalDiffer;87 public withMathProjectExample({ mutantState: mutantStatus = MutantStatus.Killed, isStatic = false } = {}): this {88 this.mutants.push(89 createMutant({ id: this.mutantId, fileName: srcAdd, replacement: '-', mutatorName: 'min-replacement', location: loc(1, 11, 1, 12) })90 );91 this.incrementalFiles[srcAdd] = factory.mutationTestReportSchemaFileResult({92 mutants: [93 factory.mutationTestReportSchemaMutantResult({94 id: 'mut-1',95 coveredBy: isStatic ? undefined : [this.oldSpecId],96 killedBy: [this.oldSpecId],97 replacement: '-',98 mutatorName: 'min-replacement',99 statusReason: 'Killed by first test',100 testsCompleted: 1,101 status: mutantStatus,102 location: loc(1, 11, 1, 12),103 }),104 ],105 source: srcAddContent,106 });107 this.incrementalTestFiles[testAdd] = factory.mutationTestReportSchemaTestFile({108 tests: [{ id: this.oldSpecId, name: 'add(2, 0) = 2' }],109 });110 this.currentFiles.set(srcAdd, srcAddContent);111 this.testCoverage.addTest(factory.testResult({ id: this.newTestId, fileName: testAdd, name: 'add(2, 0) = 2' }));112 if (isStatic) {113 this.testCoverage.hasCoverage = true;114 this.testCoverage.staticCoverage[this.mutantId] = true;115 } else {116 this.testCoverage.addCoverage(this.mutantId, [this.newTestId]);117 }118 return this;119 }120 public withoutTestCoverage(): this {121 Object.keys(this.incrementalTestFiles).forEach((testFile) => delete this.incrementalTestFiles[testFile]);122 this.testCoverage.clear();123 this.testCoverage.hasCoverage = false;124 return this;125 }126 public withTestFile(): this {127 this.currentFiles.set(testAdd, testAddContent);128 this.incrementalTestFiles[testAdd].source = testAddContent;129 return this;130 }131 public withLocatedTest({ includeEnd = false } = {}): this {132 this.incrementalTestFiles[testAdd].tests[0].location = loc(4, 2);133 if (includeEnd) {134 this.incrementalTestFiles[testAdd].tests[0].location.end = pos(6, 5);135 }136 [...this.testCoverage.forMutant(this.mutantId)!][0].startPosition = pos(4, 2);137 return this;138 }139 public withAddedLinesAboveTest(...lines: string[]): this {140 this.currentFiles.set(testAdd, `${lines.join('\n')}\n${testAddContent}`);141 for (const test of this.testCoverage.forMutant(this.mutantId)!) {142 if (test.startPosition) {143 test.startPosition = pos(4 + lines.length, 2);144 }145 }146 return this;147 }148 public withAddedLinesAboveMutant(...lines: string[]): this {149 this.currentFiles.set(srcAdd, `${lines.join('\n')}\n${srcAddContent}`);150 this.mutants[0].location = loc(1 + lines.length, 11, 1 + lines.length, 12);151 return this;152 }153 public withCrlfLineEndingsInIncrementalReport(): this {154 Object.values(this.incrementalFiles).forEach((file) => {155 file.source = file.source.replace(/\n/g, '\r\n');156 });157 Object.values(this.incrementalTestFiles).forEach((file) => {158 file.source = file.source?.replace(/\n/g, '\r\n');159 });160 return this;161 }162 public withRemovedLinesAboveMutant(...lines: string[]): this {163 this.incrementalFiles[srcAdd].source = `${lines.join('\n')}\n${srcAddContent}`;164 this.incrementalFiles[srcAdd].mutants[0].location = loc(1 + lines.length, 11, 1 + lines.length, 12);165 return this;166 }167 public withAddedTextBeforeMutant(text: string): this {168 this.currentFiles.set(169 srcAdd,170 srcAddContent171 .split('\n')172 .map((line, nr) => (nr === 1 ? `${text}${line}` : line))173 .join('\n')174 );175 this.mutants[0].location = loc(1, 11 + text.length, 1, 12 + text.length);176 return this;177 }178 public withAddedTextBeforeTest(text: string): this {179 this.currentFiles.set(180 testAdd,181 testAddContent182 .split('\n')183 .map((line, nr) => (nr === 4 ? `${text}${line}` : line))184 .join('\n')185 );186 for (const test of this.testCoverage.forMutant(this.mutantId)!) {187 if (test.startPosition) {188 test.startPosition = pos(4, 2 + text.length);189 }190 }191 return this;192 }193 public withAddedCodeInsideTheTest(code: string): this {194 this.currentFiles.set(195 testAdd,196 testAddContent197 .split('\n')198 .map((line, nr) => (nr === 5 ? ` ${code}\n${line}` : line))199 .join('\n')200 );201 for (const test of this.testCoverage.forMutant(this.mutantId)!) {202 if (test.startPosition) {203 test.startPosition = pos(4, 2);204 }205 }206 return this;207 }208 public withSecondTest({ located }: { located: boolean }): this {209 this.currentFiles.set(testAdd, testAddContentTwoTests);210 const secondTest = factory.testResult({ id: 'spec2', fileName: testAdd, name: 'add(45, -3) = 42' });211 if (located) {212 secondTest.startPosition = pos(7, 2);213 }214 this.testCoverage.addTest(secondTest);215 this.testCoverage.addCoverage(this.mutantId, [secondTest.id]);216 return this;217 }218 public withSecondTestInIncrementalReport({ isKillingTest = false } = {}): this {219 this.incrementalTestFiles[testAdd].tests.unshift(220 factory.mutationTestReportSchemaTestDefinition({ id: 'spec2', name: 'add(45, -3) = 42', location: loc(7, 0) })221 );222 if (isKillingTest) {223 this.incrementalFiles[srcAdd].mutants[0].killedBy = ['spec2'];224 }225 if (this.incrementalTestFiles[testAdd].source) {226 this.incrementalTestFiles[testAdd].source = testAddContentTwoTests;227 }228 return this;229 }230 public withUpdatedTestGeneration(): this {231 this.currentFiles.set(testAdd, testAddContentWithTestGenerationUpdated);232 const createAddWithTestGenerationTestResult = (a: number, b: number, answer: number) =>233 factory.testResult({ id: `spec${a}`, fileName: testAdd, name: `should result in ${answer} for ${a} and ${b}`, startPosition: pos(5, 4) });234 this.testCoverage.clear();235 this.testCoverage.addTest(factory.testResult({ id: 'new-spec-2', fileName: testAdd, name: 'should have name "add"', startPosition: pos(9, 2) }));236 const gen1 = createAddWithTestGenerationTestResult(40, 2, 42);237 const gen2 = createAddWithTestGenerationTestResult(45, -3, 42);238 this.testCoverage.addTest(gen1, gen2);239 this.testCoverage.addCoverage(this.mutantId, ['new-spec-2', gen1.id, gen2.id]);240 return this;241 }242 public withTestGenerationIncrementalReport(): this {243 this.incrementalTestFiles[testAdd].source = testAddContentWithTestGeneration;244 const createAddWithTestGenerationTestDefinition = (id: string, a: number, b: number, answer: number) =>245 factory.mutationTestReportSchemaTestDefinition({246 id,247 name: `should result in ${answer} for ${a} and ${b}`,248 location: loc(5, 4),249 });250 while (this.incrementalTestFiles[testAdd].tests.shift()) {}251 this.incrementalTestFiles[testAdd].tests.push(252 factory.mutationTestReportSchemaTestDefinition({ id: 'spec3', name: 'should have name "add"', location: loc(9, 2) }),253 createAddWithTestGenerationTestDefinition('spec4', 40, 2, 42),254 createAddWithTestGenerationTestDefinition('spec5', 45, -3, 42)255 );256 this.incrementalFiles[srcAdd].mutants[0].coveredBy = ['spec4', 'spec5'];257 this.incrementalFiles[srcAdd].mutants[0].killedBy = ['spec4'];258 return this;259 }260 public withRemovedTextBeforeMutant(text: string): this {261 this.incrementalFiles[srcAdd].source = srcAddContent262 .split('\n')263 .map((line, nr) => (nr === 1 ? `${text}${line}` : line))264 .join('\n');265 this.incrementalFiles[srcAdd].mutants[0].location = loc(1, 11 + text.length, 1, 12 + text.length);266 return this;267 }268 public withAddedTextAfterTest(text: string): this {269 const cnt = testAddContent270 .split('\n')271 .map((line, nr) => `${line}${nr === 6 ? text : ''}`)272 .join('\n');273 this.currentFiles.set(testAdd, cnt);274 return this;275 }276 public withChangedMutantText(replacement: string): this {277 this.currentFiles.set(srcAdd, srcAddContent.replace('+', replacement));278 return this;279 }280 public withDifferentMutator(mutatorName: string): this {281 this.mutants[0].mutatorName = mutatorName;282 return this;283 }284 public withDifferentReplacement(replacement: string): this {285 this.mutants[0].replacement = replacement;286 return this;287 }288 public withDifferentMutantLocation(): this {289 this.incrementalFiles[srcAdd].mutants[0].location = loc(2, 11, 2, 12);290 return this;291 }292 public withDifferentFileName(fileName: string): this {293 this.incrementalFiles[fileName] = this.incrementalFiles[srcAdd];294 delete this.incrementalFiles[srcAdd];295 return this;296 }297 public withSecondSourceAndTestFileInIncrementalReport(): this {298 this.incrementalTestFiles[testMultiply] = factory.mutationTestReportSchemaTestFile({299 source: testMultiplyContent,300 tests: [301 factory.mutationTestReportSchemaTestDefinition({ id: 'spec-3', location: loc(4, 2), name: 'multiply should result in 42 for 2 and 21' }),302 ],303 });304 this.incrementalFiles[srcMultiply] = factory.mutationTestReportSchemaFileResult({305 mutants: [306 factory.mutationTestReportSchemaMutantResult({307 id: 'mut-3',308 coveredBy: ['spec-3'],309 killedBy: ['spec-3'],310 replacement: '/',311 testsCompleted: 1,312 status: MutantStatus.Killed,313 location: loc(1, 11, 1, 12),314 }),315 ],316 source: srcMultiplyContent,317 });318 return this;319 }320 public withSecondSourceFile(): this {321 this.currentFiles.set(srcMultiply, srcMultiplyContent);322 return this;323 }324 public withSecondTestFile(): this {325 this.currentFiles.set(testMultiply, testMultiplyContent);326 return this;327 }328 public act() {329 this.sut = testInjector.injector.injectClass(IncrementalDiffer);330 deepFreeze(this.mutants); // make sure mutants aren't changed at all331 return this.sut.diff(332 this.mutants,333 this.testCoverage,334 factory.mutationTestReportSchemaMutationTestResult({335 files: this.incrementalFiles,336 testFiles: this.incrementalTestFiles,337 }),338 this.currentFiles339 );340 }341}342describe(IncrementalDiffer.name, () => {343 describe('mutant changes', () => {344 it('should copy status, statusReason, testsCompleted if nothing changed', () => {345 // Arrange346 const actualDiff = new ScenarioBuilder().withMathProjectExample().act();347 // Assert348 const actualMutant = actualDiff[0];349 const expected: Partial<Mutant> = {350 id: '2',351 fileName: srcAdd,352 replacement: '-',353 mutatorName: 'min-replacement',354 location: loc(1, 11, 1, 12),355 status: MutantStatus.Killed,356 statusReason: 'Killed by first test',357 testsCompleted: 1,358 };359 expect(actualMutant).deep.contains(expected);360 });361 it('should not reuse the result when --force is active', () => {362 // Arrange363 testInjector.options.force = true;364 const actualDiff = new ScenarioBuilder().withMathProjectExample().act();365 // Assert366 const actualMutant = actualDiff[0];367 expect(actualMutant.status).undefined;368 });369 it('should not reuse when the mutant was ignored', () => {370 // Arrange371 const actualDiff = new ScenarioBuilder().withMathProjectExample({ mutantState: MutantStatus.Ignored }).act();372 // Assert373 const actualMutant = actualDiff[0];374 expect(actualMutant.status).undefined;375 });376 it('should normalize line endings when comparing diffs', () => {377 const actualDiff = new ScenarioBuilder()378 .withMathProjectExample()379 .withTestFile()380 .withLocatedTest()381 .withCrlfLineEndingsInIncrementalReport()382 .act();383 const actualMutant = actualDiff[0];384 expect(actualMutant.status).eq(MutantStatus.Killed);385 });386 it('should map killedBy and coveredBy to the new test ids if a mutant result is reused', () => {387 const scenario = new ScenarioBuilder().withMathProjectExample();388 const actualDiff = scenario.act();389 const actualMutant = actualDiff[0];390 const expectedTestIds = [scenario.newTestId];391 const expected: Partial<Mutant> = {392 coveredBy: expectedTestIds,393 killedBy: expectedTestIds,394 };395 expect(actualMutant).deep.contains(expected);396 });397 it("should identify that a mutant hasn't changed if lines got added above", () => {398 const actualDiff = new ScenarioBuilder().withMathProjectExample().withAddedLinesAboveMutant("import path from 'path';", '', '').act();399 expect(actualDiff[0].status).eq(MutantStatus.Killed);400 });401 it("should identify that a mutant hasn't changed if characters got added before", () => {402 const actualDiff = new ScenarioBuilder().withMathProjectExample().withAddedTextBeforeMutant("/* text added this shouldn't matter */").act();403 expect(actualDiff[0].status).eq(MutantStatus.Killed);404 });405 it("should identify that a mutant hasn't changed if lines got removed above", () => {406 const actualDiff = new ScenarioBuilder().withMathProjectExample().withRemovedLinesAboveMutant('import path from "path";', '').act();407 expect(actualDiff[0].status).eq(MutantStatus.Killed);408 });409 it("should identify that a mutant hasn't changed if characters got removed before", () => {410 const actualDiff = new ScenarioBuilder().withMathProjectExample().withRemovedTextBeforeMutant("/* text removed, this shouldn't matter*/").act();411 expect(actualDiff[0].status).eq(MutantStatus.Killed);412 });413 it('should not reuse the status of a mutant in changed text', () => {414 const actualDiff = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*').act();415 expect(actualDiff[0].status).undefined;416 });417 it('should reuse the status when there is no test coverage', () => {418 const actualDiff = new ScenarioBuilder().withMathProjectExample().withoutTestCoverage().act();419 expect(actualDiff[0].status).eq(MutantStatus.Killed);420 });421 it('should not copy the status if the mutant came from a different mutator', () => {422 const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentMutator('max-replacement');423 const actualDiff = scenario.act();424 expect(actualDiff[0]).deep.eq(scenario.mutants[0]);425 });426 it('should not copy the status if the mutant has a different replacement', () => {427 const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentReplacement('other replacement');428 const actualDiff = scenario.act();429 expect(actualDiff[0]).deep.eq(scenario.mutants[0]);430 });431 it('should not copy the status if the mutant has a different location', () => {432 const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentMutantLocation();433 const actualDiff = scenario.act();434 expect(actualDiff[0]).deep.eq(scenario.mutants[0]);435 });436 it('should not copy the status if the mutant has a different file name', () => {437 const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentFileName('src/some-other-file.js');438 const actualDiff = scenario.act();439 expect(actualDiff).deep.eq(scenario.mutants);440 });441 it('should collect 1 added mutant and 1 removed mutant if the mutant changed', () => {442 const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*');443 scenario.act();444 expect(scenario.sut!.mutantStatisticsCollector!.changesByFile).lengthOf(1);445 const changes = scenario.sut!.mutantStatisticsCollector!.changesByFile.get(srcAdd)!;446 expect(changes).property('added', 1);447 expect(changes).property('removed', 1);448 });449 it('should collect the removed mutants if the file got removed', () => {450 const scenario = new ScenarioBuilder().withMathProjectExample().withDifferentFileName('src/some-other-file.js');451 scenario.act();452 expect(scenario.sut!.mutantStatisticsCollector!.changesByFile).lengthOf(2);453 const changesOldFile = scenario.sut!.mutantStatisticsCollector!.changesByFile.get('src/some-other-file.js')!;454 const changesNewFile = scenario.sut!.mutantStatisticsCollector!.changesByFile.get(srcAdd)!;455 expect(changesNewFile).property('added', 1);456 expect(changesNewFile).property('removed', 0);457 expect(changesOldFile).property('added', 0);458 expect(changesOldFile).property('removed', 1);459 });460 it('should collect 1 added mutant and 1 removed mutant if a mutant changed', () => {461 const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*');462 scenario.act();463 expect(scenario.sut!.mutantStatisticsCollector!.changesByFile).lengthOf(1);464 const changes = scenario.sut!.mutantStatisticsCollector!.changesByFile.get(srcAdd)!;465 expect(changes).property('added', 1);466 expect(changes).property('removed', 1);467 });468 it('should log an incremental report', () => {469 const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*');470 testInjector.logger.isInfoEnabled.returns(true);471 scenario.act();472 const { mutantStatisticsCollector, testStatisticsCollector } = scenario.sut!;473 sinon.assert.calledWithExactly(474 testInjector.logger.info,475 `Incremental report:\n\tMutants:\t${mutantStatisticsCollector!.createTotalsReport()}` +476 `\n\tTests:\t\t${testStatisticsCollector!.createTotalsReport()}` +477 `\n\tResult:\t\t${chalk.yellowBright(0)} of 1 mutant result(s) are reused.`478 );479 });480 it('should not log test diff when there is no test coverage', () => {481 const scenario = new ScenarioBuilder().withMathProjectExample().withoutTestCoverage();482 testInjector.logger.isInfoEnabled.returns(true);483 scenario.act();484 const { mutantStatisticsCollector } = scenario.sut!;485 sinon.assert.calledWithExactly(486 testInjector.logger.info,487 `Incremental report:\n\tMutants:\t${mutantStatisticsCollector!.createTotalsReport()}` +488 `\n\tResult:\t\t${chalk.yellowBright(1)} of 1 mutant result(s) are reused.`489 );490 });491 it('should log a detailed incremental report', () => {492 const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*');493 testInjector.logger.isDebugEnabled.returns(true);494 scenario.act();495 const { mutantStatisticsCollector } = scenario.sut!;496 const lineSeparator = '\n\t\t';497 const detailedMutantSummary = `${lineSeparator}${mutantStatisticsCollector!.createDetailedReport().join(lineSeparator)}`;498 sinon.assert.calledWithExactly(499 testInjector.logger.debug,500 `Detailed incremental report:\n\tMutants: ${detailedMutantSummary}\n\tTests: ${lineSeparator}No changes`501 );502 });503 it('should not log if logLevel "info" or "debug" aren\'t enabled', () => {504 const scenario = new ScenarioBuilder().withMathProjectExample().withChangedMutantText('*');505 testInjector.logger.isInfoEnabled.returns(false);506 testInjector.logger.isDebugEnabled.returns(false);507 scenario.act();508 sinon.assert.notCalled(testInjector.logger.debug);509 sinon.assert.notCalled(testInjector.logger.info);510 });511 });512 describe('test changes', () => {513 it('should identify that a mutant state can be reused when no tests changed', () => {514 const actualDiff = new ScenarioBuilder().withMathProjectExample().withTestFile().act();515 expect(actualDiff[0].status).eq(MutantStatus.Killed);516 });517 it('should identify that mutant state can be reused with changes above', () => {518 const actualDiff = new ScenarioBuilder()519 .withMathProjectExample()520 .withTestFile()521 .withLocatedTest()522 .withAddedLinesAboveTest("import foo from 'bar'", '')523 .act();524 // Assert525 expect(actualDiff[0].status).eq(MutantStatus.Killed);526 });527 it('should identify that mutant state can be reused with changes before', () => {528 const actualDiff = new ScenarioBuilder()529 .withMathProjectExample()530 .withTestFile()531 .withLocatedTest()532 .withAddedTextBeforeTest('/*text-added*/')533 .act();534 expect(actualDiff[0].status).eq(MutantStatus.Killed);535 });536 it('should identify that mutant state can be reused with changes below', () => {537 const actualDiff = new ScenarioBuilder()538 .withMathProjectExample()539 .withTestFile()540 .withLocatedTest({ includeEnd: true })541 .withSecondTest({ located: true })542 .act();543 expect(actualDiff[0].status).eq(MutantStatus.Killed);544 });545 it('should identify that mutant state can be reused with changes behind', () => {546 const actualDiff = new ScenarioBuilder()547 .withMathProjectExample()548 .withTestFile()549 .withLocatedTest({ includeEnd: true })550 .withAddedTextAfterTest('/*text-added*/')551 .act();552 expect(actualDiff[0].status).eq(MutantStatus.Killed);553 });554 it('should not reuse a mutant state when a covering test gets code added', () => {555 const actualDiff = new ScenarioBuilder()556 .withMathProjectExample()557 .withTestFile()558 .withLocatedTest({ includeEnd: true })559 .withAddedCodeInsideTheTest('addedText();')560 .act();561 expect(actualDiff[0].status).undefined;562 });563 it('should close locations of tests in the incremental report', () => {564 // All test runners currently only report the start positions of tests.565 // Add a workaround for 'inventing' the end position based on the next test's start position.566 const actualDiff = new ScenarioBuilder()567 .withMathProjectExample()568 .withTestFile()569 .withLocatedTest({ includeEnd: true })570 .withSecondTest({ located: true })571 .withSecondTestInIncrementalReport()572 .act();573 expect(actualDiff[0].status).eq(MutantStatus.Killed);574 });575 it('should close locations for tests on the same location in the incremental report', () => {576 // Test cases can generate tests, make sure the correct end position is chosen in those cases577 const actualDiff = new ScenarioBuilder().withMathProjectExample().withUpdatedTestGeneration().withTestGenerationIncrementalReport().act();578 expect(actualDiff[0].status).eq(MutantStatus.Killed);579 });580 it('should identify that a non-"Killed" state can be reused when a test is removed', () => {581 const actualDiff = new ScenarioBuilder()582 .withMathProjectExample({ mutantState: MutantStatus.Survived })583 .withSecondTestInIncrementalReport()584 .act();585 expect(actualDiff[0].status).eq(MutantStatus.Survived);586 });587 it('should identify that a non-"Killed" state cannot be reused when a test is added', () => {588 const actualDiff = new ScenarioBuilder()589 .withMathProjectExample({ mutantState: MutantStatus.Survived })590 .withSecondTest({ located: false })591 .act();592 expect(actualDiff[0].status).undefined;593 });594 it('should identify that a "Killed" state can be reused when the killing test didn\'t change', () => {595 const actualDiff = new ScenarioBuilder()596 .withMathProjectExample({ mutantState: MutantStatus.Killed })597 .withTestFile()598 .withLocatedTest()599 .withSecondTestInIncrementalReport()600 .act();601 expect(actualDiff[0].status).eq(MutantStatus.Killed);602 });603 it('should identify that a "Killed" state cannot be reused when the killing test was removed', () => {604 const actualDiff = new ScenarioBuilder()605 .withMathProjectExample({ mutantState: MutantStatus.Killed })606 .withTestFile()607 .withSecondTestInIncrementalReport({ isKillingTest: true })608 .act();609 expect(actualDiff[0].status).undefined;610 });611 it('should identify that a "Killed" state for a static mutant (no covering tests) can be reused when the killing test didn\'t change', () => {612 const actualDiff = new ScenarioBuilder().withMathProjectExample({ mutantState: MutantStatus.Killed, isStatic: true }).act();613 expect(actualDiff[0].status).eq(MutantStatus.Killed);614 });615 it('should collect an added test', () => {616 const scenario = new ScenarioBuilder()617 .withMathProjectExample()618 .withTestFile()619 .withLocatedTest({ includeEnd: true })620 .withSecondTest({ located: true });621 scenario.act();622 const actualCollector = scenario.sut!.testStatisticsCollector!;623 expect(actualCollector.changesByFile).lengthOf(1);624 const changes = actualCollector.changesByFile.get(testAdd)!;625 expect(changes).property('added', 1);626 expect(changes).property('removed', 0);627 });628 it('should collect an added and removed test when a test changes', () => {629 const scenario = new ScenarioBuilder()630 .withMathProjectExample()631 .withTestFile()632 .withLocatedTest()633 .withAddedCodeInsideTheTest('arrangeSomething()');634 scenario.act();635 const actualCollector = scenario.sut!.testStatisticsCollector!;636 expect(actualCollector.changesByFile).lengthOf(1);637 const changes = actualCollector.changesByFile.get(testAdd)!;638 expect(changes).property('added', 1);639 expect(changes).property('removed', 1);640 });641 });642 describe('with history', () => {643 it('should keep historic mutants in other files', () => {644 const scenario = new ScenarioBuilder().withMathProjectExample().withSecondSourceAndTestFileInIncrementalReport().withSecondSourceFile();645 const mutants = scenario.act();646 expect(mutants).lengthOf(2);647 const actualMutant = mutants[1];648 expect(actualMutant.id).includes('src/multiply.js@1:11-1:12');649 expect(actualMutant.status).eq(MutantStatus.Killed);650 expect(actualMutant.fileName).eq(path.resolve(srcMultiply));651 });652 it("should keep historic tests that didn't run this time around", () => {653 const scenario = new ScenarioBuilder()654 .withMathProjectExample()655 .withSecondSourceAndTestFileInIncrementalReport()656 .withSecondSourceFile()657 .withSecondTestFile();658 const mutants = scenario.act();659 const actualTest = scenario.testCoverage.testsById.get(`${testMultiply}@4:2\nmultiply should result in 42 for 2 and 21`)!;660 expect(actualTest).ok;661 expect(actualTest.fileName).eq(path.resolve(testMultiply));662 expect(actualTest.name).eq('multiply should result in 42 for 2 and 21');663 expect(actualTest.startPosition).deep.eq(pos(4, 2));664 expect(scenario.testCoverage.forMutant(mutants[1].id)).deep.eq(new Set([actualTest]));665 });666 it('should not keep historic mutants when they are inside of a mutated file', () => {667 testInjector.fileDescriptions[path.resolve(srcMultiply)] = { mutate: true };668 const scenario = new ScenarioBuilder().withMathProjectExample().withSecondSourceAndTestFileInIncrementalReport().withSecondSourceFile();669 const mutants = scenario.act();670 expect(mutants).lengthOf(1);671 });672 it('should not keep historic mutants when they are inside of a mutated scope of a file', () => {673 testInjector.fileDescriptions[path.resolve(srcMultiply)] = { mutate: [loc(1, 11, 1, 12), loc(2, 2, 2, 3)] };674 const scenario = new ScenarioBuilder().withMathProjectExample().withSecondSourceAndTestFileInIncrementalReport().withSecondSourceFile();675 const mutants = scenario.act();676 expect(mutants).lengthOf(1);677 });678 it('should keep historic mutants when they are outside of a mutated scope of a file', () => {679 testInjector.fileDescriptions[path.resolve(srcMultiply)] = { mutate: [loc(1, 9, 1, 10), loc(2, 11, 2, 12)] };680 const scenario = new ScenarioBuilder().withMathProjectExample().withSecondSourceAndTestFileInIncrementalReport().withSecondSourceFile();681 const mutants = scenario.act();682 expect(mutants).lengthOf(2);683 });684 });...

Full Screen

Full Screen

incremental-differ.ts

Source:incremental-differ.ts Github

copy

Full Screen

1import path from 'path';2import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';3import chalk from 'chalk';4import { schema, Mutant, Position, Location, MutantStatus, StrykerOptions, FileDescriptions, MutateDescription } from '@stryker-mutator/api/core';5import { Logger } from '@stryker-mutator/api/logging';6import { TestResult, TestStatus } from '@stryker-mutator/api/test-runner';7import { I, normalizeFileName, normalizeLineEndings, notEmpty } from '@stryker-mutator/util';8import { TestDefinition } from 'mutation-testing-report-schema';9import { commonTokens } from '@stryker-mutator/api/plugin';10import { DiffChange, DiffStatisticsCollector } from './diff-statistics-collector.js';11import { TestCoverage } from './test-coverage.js';12/**13 * The 'diff match patch' high-performant 'diffing' of files.14 * @see https://github.com/google/diff-match-patch15 */16const diffMatchPatch = new DiffMatchPatch();17/**18 * This class is responsible for calculating the diff between a run and a previous run based on the incremental report.19 *20 * Since the ids of tests and mutants can differ across reports (they are only unique within 1 report), this class21 * identifies mutants and tests by attributes that make them unique:22 * - Mutant: file name, mutator name, location and replacement23 * - Test: test name, test file name (if present) and location (if present).24 *25 * We're storing these identifiers in local variables (maps and sets) as strings.26 * We should move to 'records' for these when they come available: https://github.com/tc39/proposal-record-tuple27 *28 * A mutant result from the previous run is reused if the following conditions were met:29 * - The location of the mutant refers to a piece of code that didn't change30 * - If mutant was killed:31 * - The culprit test wasn't changed32 * - If the mutant survived:33 * - No test was added34 *35 * It uses google's "diff-match-patch" project to calculate the new locations for tests and mutants, see link.36 * @link https://github.com/google/diff-match-patch37 */38export class IncrementalDiffer {39 public mutantStatisticsCollector: DiffStatisticsCollector | undefined;40 public testStatisticsCollector: DiffStatisticsCollector | undefined;41 private readonly mutateDescriptionByRelativeFileName: Map<string, MutateDescription>;42 public static inject = [commonTokens.logger, commonTokens.options, commonTokens.fileDescriptions] as const;43 constructor(private readonly logger: Logger, private readonly options: StrykerOptions, fileDescriptions: FileDescriptions) {44 this.mutateDescriptionByRelativeFileName = new Map(45 Object.entries(fileDescriptions).map(([name, description]) => [toRelativeNormalizedFileName(name), description.mutate])46 );47 }48 private isInMutatedScope(relativeFileName: string, mutant: schema.MutantResult): boolean {49 const mutate = this.mutateDescriptionByRelativeFileName.get(relativeFileName);50 return mutate === true || (Array.isArray(mutate) && mutate.some((range) => locationIncluded(range, mutant.location)));51 }52 public diff(53 currentMutants: readonly Mutant[],54 testCoverage: I<TestCoverage>,55 incrementalReport: schema.MutationTestResult,56 currentRelativeFiles: Map<string, string>57 ): readonly Mutant[] {58 const { files, testFiles } = incrementalReport;59 const mutantStatisticsCollector = new DiffStatisticsCollector();60 const testStatisticsCollector = new DiffStatisticsCollector();61 // Expose the collectors for unit testing purposes62 this.mutantStatisticsCollector = mutantStatisticsCollector;63 this.testStatisticsCollector = testStatisticsCollector;64 // Collect what we can reuse, while correcting for diff in the locations65 const reusableMutantsByKey = collectReusableMutantsByKey(this.logger);66 const { byId: oldTestsById, byKey: oldTestInfoByKey } = collectReusableTestInfo(this.logger);67 // Collect some helper maps and sets68 const { oldCoverageByMutantKey: oldCoverageTestKeysByMutantKey, oldKilledByMutantKey: oldKilledTestKeysByMutantKey } =69 collectOldKilledAndCoverageMatrix();70 const oldTestKeys = new Set([...oldTestsById.values()].map(({ key }) => key));71 const newTestKeys = new Set(72 [...testCoverage.testsById].map(([, test]) => testToIdentifyingKey(test, toRelativeNormalizedFileName(test.fileName)))73 );74 // Create a dictionary to more easily get test information75 const testInfoByKey = collectCurrentTestInfo();76 // Mark which tests are added77 for (const [key, { relativeFileName }] of testInfoByKey) {78 if (!oldTestKeys.has(key)) {79 testStatisticsCollector.count(relativeFileName, 'added');80 }81 }82 // Make sure that tests that didn't run this time around aren't forgotten83 for (const [84 testKey,85 {86 test: { name, location },87 relativeFileName,88 },89 ] of oldTestInfoByKey) {90 if (!testInfoByKey.has(testKey)) {91 const test: TestResult = {92 status: TestStatus.Success,93 id: testKey,94 name,95 startPosition: location?.start,96 timeSpentMs: 0,97 fileName: path.resolve(relativeFileName),98 };99 testInfoByKey.set(testKey, { test, relativeFileName: relativeFileName });100 testCoverage.addTest(test);101 }102 }103 // Done with preparations, time to map over the mutants104 let reusedMutantCount = 0;105 const currentMutantKeys = new Set<string>();106 const mutants = currentMutants.map((mutant) => {107 const relativeFileName = toRelativeNormalizedFileName(mutant.fileName);108 const mutantKey = mutantToIdentifyingKey(mutant, relativeFileName);109 currentMutantKeys.add(mutantKey);110 if (!mutant.status && !this.options.force) {111 const oldMutant = reusableMutantsByKey.get(mutantKey);112 if (oldMutant) {113 const coveringTests = testCoverage.forMutant(mutant.id);114 const killedByTestKeys = oldKilledTestKeysByMutantKey.get(mutantKey);115 if (mutantCanBeReused(mutant, oldMutant, mutantKey, coveringTests, killedByTestKeys)) {116 reusedMutantCount++;117 const { status, statusReason, testsCompleted } = oldMutant;118 return {119 ...mutant,120 status,121 statusReason,122 testsCompleted,123 coveredBy: [...(coveringTests ?? [])].map(({ id }) => id),124 killedBy: testKeysToId(killedByTestKeys),125 };126 }127 } else {128 mutantStatisticsCollector.count(relativeFileName, 'added');129 }130 }131 return mutant;132 });133 // Make sure that old mutants that didn't run this time around aren't forgotten134 for (const [mutantKey, oldResult] of reusableMutantsByKey) {135 // Do an additional check to see if the mutant is in mutated range.136 //137 // For example:138 // ```diff139 // - return a || b;140 // + return a && b;141 // ```142 // The conditional expression mutator here decides to _not_ mutate b to `false` the second time around. (even though the text of "b" itself didn't change)143 // Not doing this additional check would result in a sticky mutant that is never removed144 if (!currentMutantKeys.has(mutantKey) && !this.isInMutatedScope(oldResult.relativeFileName, oldResult)) {145 const coverage = oldCoverageTestKeysByMutantKey.get(mutantKey) ?? [];146 const killed = oldKilledTestKeysByMutantKey.get(mutantKey) ?? [];147 const coveredBy = testKeysToId(coverage);148 const killedBy = testKeysToId(killed);149 const reusedMutant = {150 ...oldResult,151 id: mutantKey,152 fileName: path.resolve(oldResult.relativeFileName),153 replacement: oldResult.replacement ?? oldResult.mutatorName,154 coveredBy,155 killedBy,156 };157 mutants.push(reusedMutant);158 testCoverage.addCoverage(reusedMutant.id, coveredBy);159 }160 }161 if (this.logger.isInfoEnabled()) {162 const testInfo = testCoverage.hasCoverage ? `\n\tTests:\t\t${testStatisticsCollector.createTotalsReport()}` : '';163 this.logger.info(164 `Incremental report:\n\tMutants:\t${mutantStatisticsCollector.createTotalsReport()}` +165 testInfo +166 `\n\tResult:\t\t${chalk.yellowBright(reusedMutantCount)} of ${currentMutants.length} mutant result(s) are reused.`167 );168 }169 if (this.logger.isDebugEnabled()) {170 const lineSeparator = '\n\t\t';171 const noChanges = 'No changes';172 const detailedMutantSummary = `${lineSeparator}${mutantStatisticsCollector.createDetailedReport().join(lineSeparator) || noChanges}`;173 const detailedTestsSummary = `${lineSeparator}${testStatisticsCollector.createDetailedReport().join(lineSeparator) || noChanges}`;174 this.logger.debug(`Detailed incremental report:\n\tMutants: ${detailedMutantSummary}\n\tTests: ${detailedTestsSummary}`);175 }176 return mutants;177 function testKeysToId(testKeys: Iterable<string> | undefined) {178 return [...(testKeys ?? [])]179 .map((id) => testInfoByKey.get(id))180 .filter(notEmpty)181 .map(({ test: { id } }) => id);182 }183 function collectReusableMutantsByKey(log: Logger) {184 return new Map(185 Object.entries(files).flatMap(([fileName, oldFile]) => {186 const relativeFileName = toRelativeNormalizedFileName(fileName);187 const currentFileSource = currentRelativeFiles.get(relativeFileName);188 if (currentFileSource) {189 log.trace('Diffing %s', relativeFileName);190 const { results, removeCount } = performFileDiff(oldFile.source, currentFileSource, oldFile.mutants);191 mutantStatisticsCollector.count(relativeFileName, 'removed', removeCount);192 return results.map((m) => [193 mutantToIdentifyingKey(m, relativeFileName),194 {195 ...m,196 relativeFileName,197 },198 ]);199 }200 mutantStatisticsCollector.count(relativeFileName, 'removed', oldFile.mutants.length);201 // File has since been deleted, these mutants are not reused202 return [];203 })204 );205 }206 function collectReusableTestInfo(log: Logger) {207 const byId = new Map<string, { relativeFileName: string; test: TestDefinition; key: string }>();208 const byKey = new Map<string, TestInfo>();209 Object.entries(testFiles ?? {}).forEach(([fileName, oldTestFile]) => {210 const relativeFileName = toRelativeNormalizedFileName(fileName);211 const currentFileSource = currentRelativeFiles.get(relativeFileName);212 if (currentFileSource !== undefined && oldTestFile.source !== undefined) {213 log.trace('Diffing %s', relativeFileName);214 const locatedTests = closeLocations(oldTestFile);215 const { results, removeCount } = performFileDiff(oldTestFile.source, currentFileSource, locatedTests);216 testStatisticsCollector.count(relativeFileName, 'removed', removeCount);217 results.forEach((test) => {218 const key = testToIdentifyingKey(test, relativeFileName);219 const testInfo = { key, test, relativeFileName };220 byId.set(test.id, testInfo);221 byKey.set(key, testInfo);222 });223 } else {224 // No sources to compare, we should do our best with the info we do have225 oldTestFile.tests.map((test) => {226 const key = testToIdentifyingKey(test, relativeFileName);227 const testInfo = { key, test, relativeFileName };228 byId.set(test.id, testInfo);229 byKey.set(key, testInfo);230 });231 }232 });233 return { byId, byKey };234 }235 function collectOldKilledAndCoverageMatrix() {236 const oldCoverageByMutantKey = new Map<string, Set<string>>();237 const oldKilledByMutantKey = new Map<string, Set<string>>();238 for (const [key, mutant] of reusableMutantsByKey) {239 const killedRow = new Set(mutant.killedBy?.map((testId) => oldTestsById.get(testId)?.key).filter(notEmpty));240 const coverageRow = new Set(mutant.coveredBy?.map((testId) => oldTestsById.get(testId)?.key).filter(notEmpty));241 killedRow.forEach((killed) => coverageRow.add(killed));242 oldCoverageByMutantKey.set(key, coverageRow);243 oldKilledByMutantKey.set(key, killedRow);244 }245 return { oldCoverageByMutantKey, oldKilledByMutantKey };246 }247 function collectCurrentTestInfo() {248 const byTestKey = new Map<string, { relativeFileName: string; test: TestResult }>();249 for (const testResult of testCoverage.testsById.values()) {250 const relativeFileName = toRelativeNormalizedFileName(testResult.fileName);251 const key = testToIdentifyingKey(testResult, relativeFileName);252 const info = { relativeFileName, test: testResult, key: key };253 byTestKey.set(key, info);254 }255 return byTestKey;256 }257 function mutantCanBeReused(258 mutant: Mutant,259 oldMutant: schema.MutantResult,260 mutantKey: string,261 coveringTests: ReadonlySet<TestResult> | undefined,262 oldKillingTests: Set<string> | undefined263 ): boolean {264 if (!testCoverage.hasCoverage) {265 // This is the best we can do when the test runner didn't report coverage.266 // We assume that all mutant test results can be reused,267 // End users can use --force to force retesting of certain mutants268 return true;269 }270 if (oldMutant.status === MutantStatus.Ignored) {271 // Was previously ignored, but not anymore, we need to run it now272 return false;273 }274 const testsDiff = diffTestCoverage(mutant.id, oldCoverageTestKeysByMutantKey.get(mutantKey), coveringTests);275 if (oldMutant.status === MutantStatus.Killed) {276 if (oldKillingTests) {277 for (const killingTest of oldKillingTests) {278 if (testsDiff.get(killingTest) === 'same') {279 return true;280 }281 }282 }283 // Killing tests has changed or no longer exists284 return false;285 }286 for (const action of testsDiff.values()) {287 if (action === 'added') {288 // A non-killed mutant got a new test, we need to run it289 return false;290 }291 }292 // A non-killed mutant did not get new tests, no need to rerun it293 return true;294 }295 /**296 * Determines if there is a diff between old test coverage and new test coverage.297 */298 function diffTestCoverage(299 mutantId: string,300 oldCoveringTestKeys: Set<string> | undefined,301 newCoveringTests: ReadonlySet<TestResult> | undefined302 ): Map<string, DiffAction> {303 const result = new Map<string, DiffAction>();304 if (newCoveringTests) {305 for (const newTest of newCoveringTests) {306 const key = testToIdentifyingKey(newTest, toRelativeNormalizedFileName(newTest.fileName));307 result.set(key, oldCoveringTestKeys?.has(key) ? 'same' : 'added');308 }309 }310 if (oldCoveringTestKeys) {311 const isStatic = testCoverage.hasStaticCoverage(mutantId);312 for (const oldTestKey of oldCoveringTestKeys) {313 if (!result.has(oldTestKey)) {314 // Static mutants might not have covering tests, but the test might still exist315 if (isStatic && newTestKeys.has(oldTestKey)) {316 result.set(oldTestKey, 'same');317 } else {318 result.set(oldTestKey, 'removed');319 }320 }321 }322 }323 return result;324 }325 }326}327/**328 * Finds the diff of mutants and tests. Removes mutants / tests that no longer exist (changed or removed). Updates locations of mutants or tests that do still exist.329 * @param oldCode The old code to use for the diff330 * @param newCode The new (current) code to use for the diff331 * @param items The mutants or tests to be looked . These will be treated as immutable.332 * @returns A list of items with updated locations, without items that are changed.333 */334function performFileDiff<T extends { location: Location }>(oldCode: string, newCode: string, items: T[]): { results: T[]; removeCount: number } {335 const oldSourceNormalized = normalizeLineEndings(oldCode);336 const currentSrcNormalized = normalizeLineEndings(newCode);337 const diffChanges = diffMatchPatch.diff_main(oldSourceNormalized, currentSrcNormalized);338 const toDo = new Set(items.map((m) => ({ ...m, location: deepClone(m.location) })));339 const [added, removed] = [1, -1];340 const done: T[] = [];341 const currentPosition: Position = { column: 0, line: 0 };342 let removeCount = 0;343 for (const [change, text] of diffChanges) {344 if (toDo.size === 0) {345 // There are more changes, but nothing left to update.346 break;347 }348 const offset = calculateOffset(text);349 if (change === added) {350 for (const test of toDo) {351 const { location } = test;352 if (gte(currentPosition, location.start) && gte(location.end, currentPosition)) {353 // This item cannot be reused, code was added here354 removeCount++;355 toDo.delete(test);356 } else {357 locationAdd(location, offset, currentPosition.line === location.start.line);358 }359 }360 positionMove(currentPosition, offset);361 } else if (change === removed) {362 for (const item of toDo) {363 const {364 location: { start },365 } = item;366 const endOffset = positionMove({ ...currentPosition }, offset);367 if (gte(endOffset, start)) {368 // This item cannot be reused, the code it covers has changed369 removeCount++;370 toDo.delete(item);371 } else {372 locationAdd(item.location, negate(offset), currentPosition.line === start.line);373 }374 }375 } else {376 positionMove(currentPosition, offset);377 toDo.forEach((item) => {378 const { end } = item.location;379 if (gte(currentPosition, end)) {380 // We're done with this item, it can be reused381 toDo.delete(item);382 done.push(item);383 }384 });385 }386 }387 done.push(...toDo);388 return { results: done, removeCount };389}390/**391 * A greater-than-equals implementation for positions392 */393function gte(a: Position, b: Position) {394 return a.line > b.line || (a.line === b.line && a.column >= b.column);395}396function locationIncluded(haystack: Location, needle: Location) {397 const startIncluded = gte(needle.start, haystack.start);398 const endIncluded = gte(haystack.end, needle.end);399 return startIncluded && endIncluded;400}401function deepClone(loc: Location): Location {402 return { start: { ...loc.start }, end: { ...loc.end } };403}404/**405 * Reduces a mutant to a string that identifies the mutant across reports.406 * Consists of the relative file name, mutator name, replacement, and location407 */408function mutantToIdentifyingKey(409 { mutatorName, replacement, location: { start, end } }: Pick<Mutant, 'location' | 'mutatorName'> & { replacement?: string },410 relativeFileName: string411) {412 return `${relativeFileName}@${start.line}:${start.column}-${end.line}:${end.column}\n${mutatorName}: ${replacement}`;413}414function testToIdentifyingKey(415 { name, location, startPosition }: Pick<schema.TestDefinition, 'location' | 'name'> & Pick<TestResult, 'startPosition'>,416 relativeFileName: string | undefined417) {418 startPosition = startPosition ?? location?.start ?? { line: 0, column: 0 };419 return `${relativeFileName}@${startPosition.line}:${startPosition.column}\n${name}`;420}421export function toRelativeNormalizedFileName(fileName: string | undefined): string {422 return normalizeFileName(path.relative(process.cwd(), fileName ?? ''));423}424function calculateOffset(text: string): Position {425 const pos: Position = { line: 0, column: 0 };426 for (const char of text) {427 if (char === '\n') {428 pos.line++;429 pos.column = 0;430 } else {431 pos.column++;432 }433 }434 return pos;435}436function positionMove(pos: Position, diff: Position): Position {437 pos.line += diff.line;438 if (diff.line === 0) {439 pos.column += diff.column;440 } else {441 pos.column = diff.column;442 }443 return pos;444}445function locationAdd({ start, end }: Location, { line, column }: Position, currentLine: boolean) {446 start.line += line;447 if (currentLine) {448 start.column += column;449 }450 end.line += line;451 if (line === 0 && currentLine) {452 end.column += column;453 }454}455function negate({ line, column }: Position): Position {456 return { line: -1 * line, column: -1 * column };457}458interface TestInfo {459 relativeFileName: string;460 test: TestDefinition;461 key: string;462}463type DiffAction = DiffChange | 'same';464/**465 * Sets the end position of each test to the start position of the next test.466 * This is an educated guess and necessary.467 * If a test has no location, it is assumed it spans the entire file (line 0 to Infinity)468 *469 * Knowing the end location of tests is necessary in order to know if the test was changed.470 */471function closeLocations(testFile: schema.TestFile): LocatedTest[] {472 const locatedTests: LocatedTest[] = [];473 const openEndedTests: OpenEndedTest[] = [];474 testFile.tests.forEach((test) => {475 if (testHasLocation(test)) {476 if (isClosed(test)) {477 locatedTests.push(test);478 } else {479 openEndedTests.push(test);480 }481 } else {482 locatedTests.push({ ...test, location: { start: { line: 0, column: 0 }, end: { line: Number.POSITIVE_INFINITY, column: 0 } } });483 }484 });485 if (openEndedTests.length) {486 // Sort the opened tests in order to close their locations487 openEndedTests.sort((a, b) => a.location.start.line - b.location.start.line);488 const startPositions = uniqueStartPositions(openEndedTests);489 let currentPositionIndex = 0;490 let currentPosition = startPositions[currentPositionIndex];491 openEndedTests.forEach((test) => {492 if (currentPosition && test.location.start.line === currentPosition.line && test.location.start.column === currentPosition.column) {493 currentPositionIndex++;494 currentPosition = startPositions[currentPositionIndex];495 }496 if (currentPosition) {497 locatedTests.push({ ...test, location: { start: test.location.start, end: currentPosition } });498 }499 });500 // Don't forget about the last test501 const lastTest = openEndedTests[openEndedTests.length - 1];502 locatedTests.push({ ...lastTest, location: { start: lastTest.location.start, end: { line: Number.POSITIVE_INFINITY, column: 0 } } });503 }504 return locatedTests;505}506/**507 * Determines the unique start positions of a sorted list of tests in order508 */509function uniqueStartPositions(sortedTests: OpenEndedTest[]) {510 let current: Position | undefined;511 const startPositions = sortedTests.reduce<Position[]>((collector, { location: { start } }) => {512 if (!current || current.line !== start.line || current.column !== start.column) {513 current = start;514 collector.push(current);515 }516 return collector;517 }, []);518 return startPositions;519}520function testHasLocation(test: schema.TestDefinition): test is OpenEndedTest {521 return !!test.location?.start;522}523function isClosed(test: Required<schema.TestDefinition>): test is LocatedTest {524 return !!test.location.end;525}526type LocatedTest = schema.TestDefinition & { location: Location };...

Full Screen

Full Screen

Using AI Code Generation

copy

Full Screen

1const strykerParent = require('stryker-parent');2const mutantSummary = strykerParent.detailedMutantSummary();3console.log(mutantSummary);4module.exports = function(config) {5 config.set({6 });7};

Full Screen

Using AI Code Generation

copy

Full Screen

1var stryker = require('stryker-parent');2var summary = stryker.detailedMutantSummary();3console.log(summary);4module.exports = function(config) {5 config.set({6 htmlReporter: {7 },8 });9};

Full Screen

Using AI Code Generation

copy

Full Screen

1var mutantSummary = require('stryker-parent').detailedMutantSummary;2var summary = mutantSummary({3});4console.log(summary);5{6 "scripts": {7 },8 "dependencies": {9 }10}11{ killed: 1,12 mutationScoreBasedOnCoveredCode: 50 }13{14 "scripts": {15 },

Full Screen

Using AI Code Generation

copy

Full Screen

1const strykerParent = require('stryker-parent');2const stryker = require('stryker-api/core');3const sandbox = require('sandboxed-module');4const sut = sandbox.require('../src/index', {5 requires: {6 }7});8describe('My first test', () => {9 it('should be able to use stryker-parent', () => {10 const result = sut.detailedMutantSummary('foo');11 expect(result).to.equal('foo');12 });13});14module.exports = {15 detailedMutantSummary: function (mutant) {16 return mutant;17 }18};19at Function.Stryker.create (/Users/username/stryker-test/node_modules/stryker/src/Stryker.js:8:25)20at Object.detailedMutantSummary (/Users/username/stryker-test/src/index.js:4:18)21at Context. (/Users/username/stryker-test/test/test.js:14:22)

Full Screen

Using AI Code Generation

copy

Full Screen

1var stryker = require('stryker-parent');2var reporter = stryker.reporters.DetailedMutantSummaryReporter;3reporter.detailedMutantSummary();4reporter.detailedMutantSummary('my-report.html');5reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html');6reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html', true);7reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html', false);8reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html', true, true);9reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html', true, false);10reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html', false, true);11reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html', false, false);12reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html', false, false, true);13reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html', false, false, false);14reporter.detailedMutantSummary('my-report.html', 'path/to/my-report.html', false, false, false, true);

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run stryker-parent automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful