Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
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,
MutantSetPolicyCommand,
MutantSuppressed,
} from '../mutant-set-policy.workflow.js'

const holds = (conditions: readonly boolean[]): boolean => conditions.every((condition) => condition)

const outcomeOf = (
decided: Result.Result<readonly MutantSetOutcome[], never>,
): MutantSetOutcome | undefined =>
Expand All @@ -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<MutantSetFacts> = Arbitrary.schema(MutantSetFactsSchema)

const literalFactsArb: Arbitrary.Arbitrary<MutantSetFacts> = 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<MutantSetFacts> = Arbitrary.schema(S.Boolean).pipe(
Arbitrary.flatMap((literal) => (literal ? literalFactsArb : plainFactsArb)),
)

const relationalInsufficientFactsArb: Arbitrary.Arbitrary<MutantSetFacts> = plainFactsArb.pipe(
Arbitrary.map((facts) => MutantSetFactsSchema.make({ ...facts, relationalSufficient: false })),
)

const candidateArbs = {
plain: plainFactsArb,
literal: literalFactsArb,
relational: relationalInsufficientFactsArb,
} as const

const candidateArb: Arbitrary.Arbitrary<MutantSetFacts> = 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<readonly MutantSetFacts[]> = Arbitrary.schema(S.Boolean).pipe(
Arbitrary.flatMap((duplicated) => (duplicated ? duplicatedListArb : candidateListArb)),
)

const fullPolicyCommandArb: Arbitrary.Arbitrary<MutantSetPolicyCommand> = 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') {
Expand All @@ -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] })),
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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<string> = 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: {
Expand Down Expand Up @@ -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
Expand All @@ -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, {
Expand All @@ -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, {
Expand All @@ -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, {
Expand All @@ -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, {
Expand All @@ -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],
Expand All @@ -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, {
Expand All @@ -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,
Expand Down
Loading