Repository navigation
Releases: Fortigi/PSMutant
Release list
v0.8.0
For consumers
Killed mutants are much cheaper: the tests that killed a neighbour run first. Once a mutant is
killed, its killing tests are remembered, and each later mutant in the same function, then the same
file, first runs up to three of them alone; only if none fails do its whole covering files run. A test
is trusted for this only after it has passed on its own against unmutated source, checked once on an
idle worker -- so a test that depends on an earlier test's state is never tried alone, and verdicts
are unchanged. On by default; the new killersFirst config key turns it off, and recordAllKillers
does too. The test a report names in KilledBy can change, and with several workers can differ
between two runs of the same config. Measured interleaved on this repo's own runner file, 307
mutants with three workers over two rounds each way: wall clock 931 and 924 seconds off, 703 and 729
on -- about 22% less -- every mutant killed both ways. A first pass gets its own short deadline,
sized from how long its tests took alone, so a mutant that hangs them is not held for the whole
files' deadline before they run.
With more than one worker, each worker is a pwsh process of its own, and runs scale further.
Workers used to be runspaces in PSMutant's process, sharing one GC heap, and stopped gaining at
about four. Measured against PSComplexity's whole self-mutation config, 1424 mutants with identical
verdicts in every run: at workers: 8, 426 and 438 seconds before, 251 and 255 after; at
workers: 3 pinned to four cores, as a CI runner has, 376 and 380 before, 282 and 326 after. One
worker still runs in PSMutant's own process, where it is as fast and pays for no process start.
Two parallel-safety hazards go with the shared process: a test that writes an environment variable
now writes its own worker's, and $PID names one worker. A mutant that ends its worker's
process -- [Environment]::Exit, or a recursion that overflows the stack -- is scored Killed
with no test named, and the worker is replaced; an unmutated covering suite that does so is refused
before the first mutant. A worker process is renewed every 500 mutants rather than every 50: a new
process compiles Pester and your code afresh, which at 50 cost more than the processes saved.
Four new opt-in operators reach the comparisons BinaryOperator never touched.
ComparisonOperator negates -like, -match, -contains, -in and -is, and the -c and -i
spellings of -eq to -le -- -ceq was never mutated at all, because the default swaps are keyed
by the bare spelling. CaseSensitivity turns -ceq into -eq and an explicit -ieq into -ceq,
for every operator with a case-sensitive spelling, -creplace and -csplit included; bare to -c
is not tried, as on a number it changes nothing. BitwiseOperator swaps -band and -bor, -bxor
for -bor, -shl and -shr. StringOperatorRemoval replaces $a -replace 'x', 'y' -- and a
binary -split or -join -- with $a, except a -join that is the whole of a $(...) in a
double-quoted string, where removing it changes only the separator. All four are off unless operators names them, so the
default mutant set, and your score, are unchanged by the upgrade.
On Linux, a backslash in a file name is kept in the paths a report shows. Only the platform's own
separator becomes / now; before, every backslash did, so src/a\b.ps1 -- one file there -- was
reported as src/a/b.ps1.
A config value of the wrong type is named on every PowerShell. On PowerShell 7.6.7 a fault
inside a nested object or a list -- a string in thresholds.break, a number in a tests list -- was
refused with nothing after "does not match the PSMutant config schema:", and beside another fault it
was not mentioned at all: Test-Json there writes no message for it. PSMutant now finds the faults
itself and words them as 7.5's Test-Json did, so the message names the key and what was wrong with
it whatever the version.
-Incremental reuses the previous report's verdicts for every file nothing touched. Each
mutate file is keyed on a hash over the file, its covering test files, and every other source file
its covering suite executed -- measured once per suite with Pester's coverage and recorded in the
report. A file whose key is unchanged reuses its rows; the rest are evaluated and keyed for next time.
Measured on PSComplexity's config, 1424 mutants with verdicts identical to a plain run every time: a
plain run took 401 seconds, the first incremental run 479, a second with nothing changed 24, and one
after touching a single source file 161, reusing 1065 verdicts. The run says how many it reused
before it starts, and the report marks each reused row "Reused": true and adds reused and
incremental. A previous report written by another PSMutant, Pester or PowerShell version, or under
other timeout or recordAllKillers settings, is not reused. The key cannot see a file a suite reads
as data, an installed module, or a file the suite loads but runs nothing from; after changing one,
run without -Incremental. It cannot be combined with -RecheckFrom, -ResumeFrom,
-MergeIntoBaseline, -ChangedFile or -ListOnly.
A mutant that stops the tests from being found is no longer a survivor. When mutated code
reaches exit while a test file is being discovered -- in a function a BeforeDiscovery calls for
its cases, say -- the file's tests are never found, and Pester calls a run of no test Passed.
Such a mutant was scored Survived, as though the tests had looked and missed it. It is now a
RuntimeError: left out of the score, counted in runtimeErrors, and named on the summary line.
For the same reason, an unmutated covering suite that runs no test in a worker -- every test
skipped or never discovered -- now refuses the run before any mutant, naming the suite, where its
mutants used to be scored as survivors over tests that never ran. A mutant reaching exit anywhere
else was already contained: inside a test it fails that test, and at the top of a dot-sourced file
it ends only that file. [Environment]::Exit() ends the process and still cannot be contained; the
README says to leave code that calls it out of mutate.
An equivalent mutant can be declared in the code. A # PSMutant-Equivalent: <original> -> <mutated>: <reason> comment on the line directly above a mutated line declares that mutant
equivalent. It becomes the same file:line:description key a config entry would, so it faces the
same checks -- it must match exactly one mutant, and the run fails if the suite kills it -- and it
moves with the code instead of going stale on an edit above it. A declaration that cannot become a
key (a blank line below it, code beside it, no description or no reason) is refused before anything
is mutated. One mutant declared twice -- in the code and in the config, or under both of a config's
key forms -- is now refused by name, where before the second key was reported as matching no
mutant.
Logging calls are no longer mutated. Mutants inside the arguments of Write-Verbose,
Write-Debug, Write-Information, Write-Host and Write-Progress are skipped, because the text
of a log line rarely tells you anything about your tests. The new ignoreCalls config key replaces
that list, and [] turns it off. Every operator's mutants inside such a call are skipped, and the
number is printed beside the score and written to the report as skippedAsIgnoredCall, with the
list applied as ignoreCalls; neither is counted in total or either score. Mutant ids do not
change, so a recheck still matches. This can lower your mutant count after upgrading -- a run
that mutated inside logging calls before will now skip those mutants; set "ignoreCalls": [] to
keep the old behaviour. Write-Warning, Write-Error and throw are not in the default, because
tests often assert those messages. A -ListOnly result carries the count as Ignored.
Patterns in mutate and tests, and pairing by name. An entry holding * or ? is a pattern,
read as PSComplexity reads its config's exclude: * and ? stay within one path segment, **
spans any number of them, and a pattern also matches everything below what it names. Patterns are
expanded once at start-up, before anything else reads the lists, and the files each matched are
printed, so the run still names exactly what it covers. A pattern matching no file is refused,
naming every such pattern at once. The new testsByConvention switch pairs a mutate file that has no
tests entry with tests/<Name>.Tests.ps1 when that exists; an explicit entry always wins, each
pair is printed, and tests may then be {}. Off by default, so upgrading changes no mapping.
Matched without regard to case on every platform, as every other path here is.
What's Changed
- ci: gate complexity on PSComplexity 0.6.0 by @TaekeK in #280
- test: read Test-Json's verdict from its return value (#281) by @TaekeK in #282
- feat: declare an equivalent mutant with a comment above its line (#262) by @TaekeK in #283
- feat: skip mutants inside logging calls, configurable as ignoreCalls (#263) by @TaekeK in #284
- feat: patterns in mutate and tests, and pairing tests by name (#265) by @TaekeK in #285
- fix: a mutant that stops the tests being found is a RuntimeError, not a survivor (#251) by @TaekeK in #286
- feat: run the tests that killed a neighbour first (#254) by @TaekeK in #287
- ci: bump github/codeql-action/upload-sarif from 4.38.3 to 4.39.0 by @dependabot[bot] in #288
- feat: -Incremental reuses verdicts for files nothing to...
v0.7.0
What's Changed
- ci: gate complexity on PSComplexity 0.5.2 by @TaekeK in #244
- ci: bump github/codeql-action/upload-sarif from 4.38.2 to 4.38.3 by @dependabot[bot] in #245
- fix: a sweep tolerates a concurrent sweep that removed the item first (#267) by @TaekeK in #268
- fix: refuse a suite that is green only in the calling session (#248) by @TaekeK in #269
- fix: judge each mutant on its own code when a suite imports the code under test (#249) by @TaekeK in #271
- docs: the floor is 7.0 in two stale comments, and the README says why (#246) by @TaekeK in #273
- docs: classes are not supported yet; the #249 table said they were by @TaekeK in #276
- feat: report schema v3; a mutant that does not parse is CompileError, not a kill (#252) by @TaekeK in #272
- fix: stop a timed-out mutant without waiting for it (#250) by @TaekeK in #274
- fix: size each mutant's deadline from its own covering suite (#253) by @TaekeK in #275
- feat: report a total score beside the covered-code score (#264) by @TaekeK in #277
- fix: mutation-test code that defines a class or enum (#270) by @TaekeK in #278
- perf: a third off the self-mutation gate -- cheaper covering suites, and a decided job's clock runs by @TaekeK in #279
Full Changelog: v0.6.0...v0.7.0
v0.6.0
What's Changed
- ci: bump github/codeql-action/upload-sarif from 4.37.9 to 4.38.2 by @dependabot[bot] in #223
- ci: take the watcher's first harvest, and stop the issue arguing against itself by @TaekeK in #228
- Cover continuation lines of executed multi-line statements (0.5.1) by @TaekeK in #232
- ci: skip the expensive gates a pull request cannot affect by @TaekeK in #234
- SARIF output and Azure DevOps annotations (0.6.0) by @TaekeK in #233
- pins: declare the Pester exemption list, empty on purpose (#224) by @TaekeK in #236
- ci: check that an exempted PowerShell is still the runner's own (#230) by @TaekeK in #237
- docs: check the estate Pester pin CLAUDE.md states (#226) by @TaekeK in #238
- docs: README names every gate script CI runs (#225) by @TaekeK in #239
- docs: adopt the ships-nothing release rule, README-only included (#227) by @TaekeK in #240
- fix: say where to look when no failing baseline test said why (#156) by @TaekeK in #241
- fix: the stale-sandbox sweep no longer deletes a live run's files on Windows (#235) by @TaekeK in #242
- docs: retire the roadmap and the stale parts of CLAUDE.md by @TaekeK in #243
Full Changelog: v0.5.0...v0.6.0
v0.5.0
What's Changed
- Stop waiting two seconds per mutant on a deliberate timeout by @TaekeK in #186
- feat: report how many files the score was computed over (#185) by @TaekeK in #188
- test: mutation-test the sandbox isolation layer (#22) by @TaekeK in #189
- ci: compare this repo's CI to the paired one's with a shared rule set (#117) by @TaekeK in #190
- ci: watch the compatibility version lists, in three tiers (#182) by @TaekeK in #191
- ci: bump the complexity gate's PSComplexity pin to 0.5.1 by @TaekeK in #192
- feat: write a partial report when a run is interrupted (#39, #187) by @TaekeK in #193
- test: make every Describe and Context pass on its own (#43) by @TaekeK in #195
- ci: fail when main claims a version already on the gallery (#115), and unify the gate decisions by @TaekeK in #196
- feat: record which tests killed each mutant (#9) by @TaekeK in #197
- refactor: make the code every mutant runs real code, not a string (#49) by @TaekeK in #198
- feat: break the mutation score down per file (#6, first of three parts) by @TaekeK in #199
- feat: accept a committed baseline of surviving mutants (#6) by @TaekeK in #201
- docs: write down the process and state stance, and act on it (#57, part of #53) by @TaekeK in #202
- test: map the sandbox sweep suite, now that it cannot delete the live run by @TaekeK in #203
- feat: bound the whole run, so a hang stops instead of looking slow (#124) by @TaekeK in #204
- fix: refuse a recheck seeded from a report about a different run (#110) by @TaekeK in #205
- feat: fold a recheck's verdicts back into its baseline (#4) by @TaekeK in #206
- ci: bump github/codeql-action/upload-sarif from 4.37.8 to 4.37.9 by @dependabot[bot] in #207
- feat: give the output seam more sinks -- verbose and progress (#164) by @TaekeK in #208
- feat: package the prelude, and preview the mutant set with -ListOnly (#63, #10) by @TaekeK in #209
- perf: stop instrumenting a baseline nothing will read (#107) by @TaekeK in #210
- feat: gate a pull request on what it changed, with -ChangedFile (#8) by @TaekeK in #213
- test: gate the one bug class no other gate here can see (#211) by @TaekeK in #214
- feat: let configs pipe in, one independent run each (#163) by @TaekeK in #215
- Evaluate mutants in parallel, one sandbox and one runspace per worker by @TaekeK in #218
- Continue an interrupted run with -ResumeFrom by @TaekeK in #220
Full Changelog: v0.4.0...v0.5.0