From 9124f0d7d81d7ee5b8f3584cb90df9a20aa4ed5a Mon Sep 17 00:00:00 2001 From: systemfsoftware-maker Date: Fri, 2 Oct 2026 01:29:57 +0000 Subject: [PATCH] test(repo): declare the deciding coverage classes of two under-drawn properties mutant-set-policy and interpret-vitest-mutant-run now declare cover classes for every branch that decides their subject, and their generators reach each class constructively under the mutation-worker budget Verdict-Semantics: unchanged --- ...utant-set-policy.workflow.property.test.ts | 125 +++++++++++++++++- ...itest-mutant-run.workflow.property.test.ts | 113 ++++++++++++++-- 2 files changed, 227 insertions(+), 11 deletions(-) diff --git a/packages/stryker-js-instrumenter/src/__tests__/mutant-set-policy.workflow.property.test.ts b/packages/stryker-js-instrumenter/src/__tests__/mutant-set-policy.workflow.property.test.ts index efa5bdc14..870c17350 100644 --- a/packages/stryker-js-instrumenter/src/__tests__/mutant-set-policy.workflow.property.test.ts +++ b/packages/stryker-js-instrumenter/src/__tests__/mutant-set-policy.workflow.property.test.ts @@ -1,9 +1,11 @@ import { it } from '@systemfsoftware/vitest' +import * as Arbitrary from 'effect/Arbitrary' import * as Result from 'effect/Result' import * as S from 'effect/Schema' import { MutantKept, + type MutantSetFacts, MutantSetFactsSchema, type MutantSetOutcome, mutantSetPolicy, @@ -11,6 +13,8 @@ import { MutantSuppressed, } from '../mutant-set-policy.workflow.js' +const holds = (conditions: readonly boolean[]): boolean => conditions.every((condition) => condition) + const outcomeOf = ( decided: Result.Result, ): MutantSetOutcome | undefined => @@ -19,9 +23,106 @@ const outcomeOf = ( onSuccess: (outcomes) => (outcomes.length === 1 ? outcomes[0] : undefined), }) +const isFacts = (value: unknown): value is MutantSetFacts => typeof value === 'object' && value !== null + +const literalCode = (code: string): boolean => + holds([code.length > 0, code.includes('(') === false, code.includes(')') === false, code.trim() === code]) + +const sameLiteralCode = (facts: MutantSetFacts): boolean => + holds([facts.replacementCode === facts.originalCode, literalCode(facts.originalCode)]) + +const sameReplacementAsEarlier = ( + candidates: readonly MutantSetFacts[], + facts: MutantSetFacts, + index: number, +): boolean => + holds([ + literalCode(facts.replacementCode), + candidates.slice(0, index).some((earlier) => earlier.replacementCode === facts.replacementCode), + ]) + +const repeatsAReplacement = (candidates: readonly MutantSetFacts[]): boolean => + candidates.some((facts, index) => sameReplacementAsEarlier(candidates, facts, index)) + +const tokenCharacterArb = Arbitrary.schema(S.Literals(['a', 'b', 'c', '0', '1', 'Z'])) + +const tokenArb = Arbitrary.array(tokenCharacterArb, { minLength: 1, maxLength: 6 }).pipe( + Arbitrary.map((characters) => characters.join('')), +) + +const plainFactsArb: Arbitrary.Arbitrary = Arbitrary.schema(MutantSetFactsSchema) + +const literalFactsArb: Arbitrary.Arbitrary = Arbitrary.all({ + code: tokenArb, + relationalSufficient: Arbitrary.schema(S.Boolean), +}).pipe( + Arbitrary.map(({ code, relationalSufficient }) => + MutantSetFactsSchema.make({ originalCode: code, replacementCode: code, relationalSufficient }) + ), +) + +const candidateFactsArb: Arbitrary.Arbitrary = Arbitrary.schema(S.Boolean).pipe( + Arbitrary.flatMap((literal) => (literal ? literalFactsArb : plainFactsArb)), +) + +const relationalInsufficientFactsArb: Arbitrary.Arbitrary = plainFactsArb.pipe( + Arbitrary.map((facts) => MutantSetFactsSchema.make({ ...facts, relationalSufficient: false })), +) + +const candidateArbs = { + plain: plainFactsArb, + literal: literalFactsArb, + relational: relationalInsufficientFactsArb, +} as const + +const candidateArb: Arbitrary.Arbitrary = Arbitrary.schema( + S.Literals(['plain', 'literal', 'relational']), +).pipe(Arbitrary.flatMap((kind) => candidateArbs[kind])) + +const candidateListArb = Arbitrary.array(candidateArb, { maxLength: 3 }) + +const duplicatedListArb = Arbitrary.all({ + head: literalFactsArb, + tail: candidateListArb, +}).pipe(Arbitrary.map(({ head, tail }) => [head, head, ...tail])) + +const candidatesArb: Arbitrary.Arbitrary = Arbitrary.schema(S.Boolean).pipe( + Arbitrary.flatMap((duplicated) => (duplicated ? duplicatedListArb : candidateListArb)), +) + +const fullPolicyCommandArb: Arbitrary.Arbitrary = candidatesArb.pipe( + Arbitrary.map((candidates) => MutantSetPolicyCommand.make({ policy: 'full', candidates })), +) + it.prop( '∀pf_PolicyAndFacts_≡TheRelationalVerdictDecidesTheSingleCandidateOutcome', - { of: [S.Literals(['default', 'full']), MutantSetFactsSchema], subject: mutantSetPolicy }, + { + of: [S.Literals(['default', 'full']), candidateFactsArb], + subject: mutantSetPolicy, + cover: { + fullPolicy: [(policy) => policy === 'full', 0.2], + relationalSuppression: [ + (policy, facts) => holds([policy === 'default', isFacts(facts) && facts.relationalSufficient === false]), + 0.1, + ], + equivalentSuppression: [ + (policy, facts) => + holds([ + policy === 'default', + isFacts(facts) && facts.relationalSufficient === true && sameLiteralCode(facts), + ]), + 0.05, + ], + keptUnderDefault: [ + (policy, facts) => + holds([ + policy === 'default', + isFacts(facts) && facts.relationalSufficient === true && sameLiteralCode(facts) === false, + ]), + 0.05, + ], + }, + }, (subject, [policy, facts]) => { const outcome = outcomeOf(subject(MutantSetPolicyCommand.make({ policy, candidates: [facts] }))) if (policy === 'full') { @@ -30,13 +131,31 @@ it.prop( if (facts.relationalSufficient === false) { return outcome !== undefined && S.is(MutantSuppressed)(outcome) && outcome.ruleId === 'redundant-relational' } - return true + if (sameLiteralCode(facts)) { + return outcome !== undefined && S.is(MutantSuppressed)(outcome) && outcome.ruleId === 'equivalent-to-original' + } + return ( + outcome !== undefined && + (S.is(MutantKept)(outcome) || (S.is(MutantSuppressed)(outcome) && outcome.ruleId === 'equivalent-to-original')) + ) }, ) it.prop( '∀c_Command_≡TheFullPolicySuppressesNothing', - { of: [MutantSetPolicyCommand], subject: mutantSetPolicy }, + { + of: [fullPolicyCommandArb], + subject: mutantSetPolicy, + cover: { + emptyCandidates: [(command) => command.candidates.length === 0, 0.05], + relationalInsufficientCandidate: [ + (command) => command.candidates.some((facts) => facts.relationalSufficient === false), + 0.2, + ], + equivalentCandidate: [(command) => command.candidates.some((facts) => sameLiteralCode(facts)), 0.2], + duplicateReplacement: [(command) => repeatsAReplacement(command.candidates), 0.15], + }, + }, (subject, [command]) => Result.match( subject(MutantSetPolicyCommand.make({ policy: 'full', candidates: [...command.candidates] })), diff --git a/packages/stryker-js-vitest-runner/src/__tests__/interpret-vitest-mutant-run.workflow.property.test.ts b/packages/stryker-js-vitest-runner/src/__tests__/interpret-vitest-mutant-run.workflow.property.test.ts index f239020b8..188ce49ee 100644 --- a/packages/stryker-js-vitest-runner/src/__tests__/interpret-vitest-mutant-run.workflow.property.test.ts +++ b/packages/stryker-js-vitest-runner/src/__tests__/interpret-vitest-mutant-run.workflow.property.test.ts @@ -1,6 +1,7 @@ import { Mutant } from '@systemfsoftware/stryker-js-plugin-interface' import { TestRunner } from '@systemfsoftware/stryker-js-plugin-interface' import { describe } from '@systemfsoftware/vitest' +import * as Arbitrary from 'effect/Arbitrary' import * as Result from 'effect/Result' import * as S from 'effect/Schema' @@ -17,6 +18,43 @@ const TRAP_MUTANT_ID = Mutant.MutantId.make('0000000000000000') const OTHER_MUTANT_ID = Mutant.MutantId.make('0000000000000001') const TRAP_FILE = 'b.ts' +const holds = (conditions: readonly boolean[]): boolean => conditions.every((condition) => condition) + +type MutantRunCoverValue = VitestMutantRunCommand | TestRunner.TestResult | string + +const isTestResult = (value: MutantRunCoverValue): value is TestRunner.TestResult => + typeof value === 'object' && 'status' in value + +const isCommand = (value: MutantRunCoverValue): value is VitestMutantRunCommand => + typeof value === 'object' && ('status' in value) === false + +const commandOf = (value: MutantRunCoverValue): VitestMutantRunCommand | undefined => + isCommand(value) ? value : undefined + +const limitOf = (value: MutantRunCoverValue): number => commandOf(value)?.hitLimit ?? 0 + +const externalError = (value: MutantRunCoverValue): boolean => commandOf(value)?.hasExternalError === true + +const testsOf = (value: MutantRunCoverValue): readonly TestRunner.TestResult[] => commandOf(value)?.tests ?? [] + +const anyFailedTest = (tests: readonly TestRunner.TestResult[]): boolean => + tests.some((test) => test.status === 'failed') + +const anySkippedTest = (tests: readonly TestRunner.TestResult[]): boolean => + tests.some((test) => test.status === 'skipped') + +const statusOf = (value: MutantRunCoverValue): TestRunner.TestStatus | undefined => + isTestResult(value) ? value.status : undefined + +const textCharacterArb = Arbitrary.schema(S.Literals(['a', 'b', 'x', '1', '-', ' '])) + +const errorTextArb: Arbitrary.Arbitrary = Arbitrary.schema( + S.Int.check(S.isBetween({ minimum: 1, maximum: 32 })), +).pipe( + Arbitrary.flatMap((length) => Arbitrary.array(textCharacterArb, { minLength: length, maxLength: length })), + Arbitrary.map((characters) => characters.join('')), +) + const commandWith = ( input: VitestMutantRunCommand, override: { @@ -47,7 +85,14 @@ const commandWith = ( describe('interpretVitestMutantRun', (it) => { it.prop( '→h_HitLimitOnNamedTrap_=Timeout', - { of: [VitestMutantRunCommand], subject: interpretVitestMutantRun }, + { + of: [VitestMutantRunCommand], + subject: interpretVitestMutantRun, + cover: { + hitLimitAtZero: [(input) => limitOf(input) === 0, 0.2], + hitLimitPositive: [(input) => limitOf(input) > 0, 0.05], + }, + }, (subject, [input]) => { const hitLimit = input.hitLimit ?? 0 const hitCount = hitLimit + 1 @@ -67,7 +112,14 @@ describe('interpretVitestMutantRun', (it) => { it.prop( '→h_HitLimitOnTrapFile_=Timeout', - { of: [VitestMutantRunCommand], subject: interpretVitestMutantRun }, + { + of: [VitestMutantRunCommand], + subject: interpretVitestMutantRun, + cover: { + hitLimitAtZero: [(input) => limitOf(input) === 0, 0.2], + hitLimitPositive: [(input) => limitOf(input) > 0, 0.05], + }, + }, (subject, [input]) => { const hitLimit = input.hitLimit ?? 0 const command = commandWith(input, { @@ -85,7 +137,14 @@ describe('interpretVitestMutantRun', (it) => { it.prop( '→h_HitLimitOnOtherMutant_=Killed', - { of: [VitestMutantRunCommand], subject: interpretVitestMutantRun }, + { + of: [VitestMutantRunCommand], + subject: interpretVitestMutantRun, + cover: { + hitLimitAtZero: [(input) => limitOf(input) === 0, 0.2], + hitLimitPositive: [(input) => limitOf(input) > 0, 0.05], + }, + }, (subject, [input]) => { const hitLimit = input.hitLimit ?? 0 const command = commandWith(input, { @@ -106,7 +165,18 @@ describe('interpretVitestMutantRun', (it) => { it.prop( '→h_HitCountAtBound_≠Timeout', - { of: [VitestMutantRunCommand], subject: interpretVitestMutantRun }, + { + of: [VitestMutantRunCommand], + subject: interpretVitestMutantRun, + cover: { + noHitKilled: [(input) => anyFailedTest(testsOf(input)), 0.2], + noHitExternalError: [(input) => holds([anyFailedTest(testsOf(input)) === false, externalError(input)]), 0.05], + noHitSurvived: [ + (input) => holds([anyFailedTest(testsOf(input)) === false, externalError(input) === false]), + 0.05, + ], + }, + }, (subject, [input]) => { const hitLimit = input.hitLimit ?? 0 const command = commandWith(input, { @@ -123,7 +193,14 @@ describe('interpretVitestMutantRun', (it) => { it.prop( '→f_FailedTestPlusHitBound_=Killed', - { of: [VitestMutantRunCommand, TestRunner.TestResultSchema], subject: interpretVitestMutantRun }, + { + of: [VitestMutantRunCommand, TestRunner.TestResultSchema], + subject: interpretVitestMutantRun, + cover: { + hitLimitAtZero: [(input) => limitOf(input) === 0, 0.2], + hitLimitPositive: [(input) => limitOf(input) > 0, 0.05], + }, + }, (subject, [input, test]) => { const hitLimit = input.hitLimit ?? 0 const command = commandWith(input, { @@ -144,7 +221,15 @@ describe('interpretVitestMutantRun', (it) => { it.prop( '∀t_TestResult_≡KilledIffFailed', - { of: [VitestMutantRunCommand, TestRunner.TestResultSchema], subject: interpretVitestMutantRun }, + { + of: [VitestMutantRunCommand, TestRunner.TestResultSchema], + subject: interpretVitestMutantRun, + cover: { + failedTest: [(_input, test) => statusOf(test) === 'failed', 0.1], + skippedTest: [(_input, test) => statusOf(test) === 'skipped', 0.1], + successTest: [(_input, test) => statusOf(test) === 'success', 0.1], + }, + }, (subject, [input, test]) => { const command = commandWith(input, { tests: [test], @@ -167,8 +252,12 @@ describe('interpretVitestMutantRun', (it) => { it.prop( '∀c_MutantRunCommand_≡DryErrorNamesTheExternalError', { - of: [VitestMutantRunCommand, S.String.check(S.isMinLength(1), S.isMaxLength(32))], + of: [VitestMutantRunCommand, errorTextArb], subject: interpretVitestMutantRun, + cover: { + shortExternalErrorText: [(_input, text) => typeof text === 'string' && text.length <= 16, 0.2], + longExternalErrorText: [(_input, text) => typeof text === 'string' && text.length > 16, 0.1], + }, }, (subject, [input, externalErrorText]) => { const command = commandWith(input, { @@ -189,7 +278,15 @@ describe('interpretVitestMutantRun', (it) => { it.prop( '∀c_MutantRunCommand_≡ScorableOutcomesReportEveryExecutedTest', - { of: [VitestMutantRunCommand], subject: interpretVitestMutantRun }, + { + of: [VitestMutantRunCommand], + subject: interpretVitestMutantRun, + cover: { + anyFailedTest: [(input) => anyFailedTest(testsOf(input)), 0.2], + noFailedTest: [(input) => anyFailedTest(testsOf(input)) === false, 0.05], + anySkippedTest: [(input) => anySkippedTest(testsOf(input)), 0.2], + }, + }, (subject, [input]) => { const command = commandWith(input, { tests: input.tests,