Skip to content

v1.6.1 — Safer reports, faster Xcode runs, and fixes from the October review

Latest

Choose a tag to compare

@ericodx ericodx released this 08 Oct 21:51
· 7 commits to main since this release
2726548

This release closes every issue the October 2026 code review opened. The HTML report no longer breaks on a < mutation or lets a string literal inject markup, simulator clones no longer leak when pool setup fails, and the twelve smaller risks the review listed are each confirmed and fixed. Incompatible Xcode mutants now run in warm sandboxes, one per worker, rebuilt incrementally instead of cold for every mutant, and xcodebuild no longer collects test diagnostics nobody reads, which doubled every failing run on the simulator and sometimes hung it. On the benchmark fixture the incompatible phase is 55% faster on macOS and 57% faster on the iOS Simulator. Nothing in the command line, the configuration keys or the report formats is added or removed.


Bug fixes

Reports

  • Every value the HTML report interpolates is escaped (&, <, >, ", '). A < or && mutation no longer breaks the table, and a string literal can no longer inject markup (#139)
  • HTML and Sonar paths are project-relative and stay right behind a symlink or under /private. They no longer start with / (see Upgrading)
  • The end columns of the JSON and Sonar mutant ranges count UTF-8 bytes, the unit of the start column. They were off after a non-ASCII character (#143)

Simulators

  • Each simulator clone is recorded as soon as simctl clone returns, and a pool whose setup fails part-way deletes the clones it made before rethrowing (#141)
  • Clones are named XMR-<pid>-<session>-<n>. Before cloning, a pool deletes the clones of runs whose process is gone, and those in the old name form. Clones of a run still in progress are left alone

Schematization and execution

  • ImportStyle sees the imports inside the active clauses of #if blocks, nested ones included, so a schema no longer adds a second, conflicting Foundation import (#153)
  • When SchemaNarrower gives up after taking mutants out of the schema, the fallback no longer tests those mutants again. They already run on the incompatible path, and each of them got a second verdict
  • Schema narrowing fails when it cannot restore a sandbox link or write a narrowed schema, instead of ignoring it and failing every later build
  • A sandbox file that cannot be linked back after an incompatible mutant stops the run with IntegrityError.sourceNotRestored. Before, every later mutant of that worker was cached as unviable
  • An incompatible mutant whose file lies outside the project is unviable. Before, the computed sandbox path was the sandbox root, and writing the mutant there deleted the sandbox

Processes and signals (#143)

  • The SIGKILL that follows an Xcode timeout is cancelled when the process exits. It used to fire five seconds later whatever happened
  • A task that is already cancelled no longer starts its process, and one cancelled while its process starts stops it
  • The process table is read with headroom and retried on ENOMEM, instead of coming back empty
  • setpgid always failed with EACCES once the child had exec'd. It is replaced by a check that the process leads its own group, which warns once
  • The cleanup after a signal runs on a dispatch signal source. The C handler does nothing that is not async-signal-safe

Configuration (#143)

  • An invalid timeout, build-timeout or concurrency in .swift-mutation-testing.yml is a usage error that names the file. Unknown keys are warned about
  • Inline # comments are stripped, outside quotes, and flags accept yes, no, on and off as well as true and false
  • Disabling every operator of the tier, with --disable-mutator, disabled-mutators or active: false, is a usage error. The empty list it left meant every operator, so the run tested all seven
  • The --concurrency help said every SPM run uses one worker. Only an Xcode run on macOS or with XCTest does

Cache, plans and test files (#143)

  • A cache or plan journal that cannot be written warns once, instead of dropping its entries silently
  • A plan file that exists but cannot be read is reported as unreadable, not as gone
  • An unreadable cache forgets its activation records too
  • Only the Tests directories inside the project count as test files. A project inside a directory named like a test target no longer hashes its sources as tests

Performance

Warm sandboxes for incompatible Xcode mutants (#144)

  • Each worker holds one pool slot and one clean sandbox, built once. Per mutant it writes the mutated file, rebuilds incrementally, runs the tests and links the original back. That last step is what makes reuse safe, and an integration test on CalcApp pins it
  • Up to a quarter of --concurrency workers run at once, capped by the pool size. A reproduction keeps a cold sandbox per attempt

No test diagnostics

  • Every xcodebuild test-without-building passes -collect-test-diagnostics never. By default xcodebuild collects a diagnostics report whenever a test fails. On the iOS Simulator that doubled every failing run (20.7 s against 8.5 s) and sometimes hung it, so killed mutants came back as timeouts after the full limit

Incompatible SPM mutants (#146)

  • They run their file's own suite first and stop at the first failing test, as schematized mutants do. A reproduction still runs the whole suite

Discovery, schematization, plans and sandboxes (#145, #147–#152)

  • A parsed file builds its SourceLocationConverter and function body scopes once, and the seven operators, the suppression filter, indexing and schematization share them. Before, each operator built its own
  • The test files are listed and read once per run (they were listed twice and read three times), and the killer test file comes from an index of func declarations and exact @Test titles instead of rereading every file per killed mutant
  • Schematization works on one byte buffer decoded once and finds earlier edits by binary search
  • Each file is hashed once instead of once per incompatible mutant, and a plan copies each file's bytes once and looks hashes up by path
  • Sandboxes are built off the cooperative pool, and only symlinked files are resolved
  • Scope lookups use binary search, mutation points are sorted once, and the project root is resolved once per report

Upgrading

  • HTML and Sonar paths are relative without a leading / (Sources/Calc.swift, not /Sources/Calc.swift). SonarQube's generic issue import expects exactly that form. A script that matched the old form needs updating
  • The killer test file is matched exactly. A test file that only mentions a test's name no longer counts as the file that killed the mutant. The cache keys on that file, so a cached kill is invalidated by the right change
  • .swift-mutation-testing.yml is checked more strictly. A value that was silently ignored, such as timeout: abc, now ends the run with 1 and names the file. An unknown key is a warning, not an error
  • Disabling the whole tier is an error. If a configuration disables all three default operators, choose --operator-tier experimental or name the operators with --operator
  • Simulator clones from older versions are deleted by the first pool that sets up after upgrading. They used the old name form

Measurements

The warm sandboxes, on a copy of CalcApp with 300 generated files and 11 incompatible mutants. These are medians of three end-to-end runs per version, measured against main just before the change, all with the diagnostics flag and all reaching the same verdicts:

Destination Version Incompatible phase Whole run
macOS (concurrency resolves to 1) cold sandbox per mutant 129.3 s 140.2 s
macOS v1.6.1 57.7 s (−55%) 66.8 s (−52%)
iOS Simulator, --concurrency 8 cold sandbox per mutant 177.6 s 229.7 s
iOS Simulator v1.6.1 76.8 s (−57%) 100.5 s (−56%)

The fixture's cold build is short (6.2 s, against 2.8 s incremental), so a project whose build takes minutes should gain more. Scripts/xcode-incompatible-benchmark/benchmark.swift reproduces the table.

This repository's own score on the default tier is 99.8%, from the self-run of 2026-10-08. That run had 1645 mutants and scored 99.2% over every operator. That is the README badge.


Test coverage

  • 1462 tests in 176 suites
  • 8266 of 8267 source lines covered
  • 3705 of 3711 regions covered (99.84%)

The documented region coverage is measured again with llvm-cov report and matches it (#154). The six regions left are listed in Docs/CodeBase/README.md, with the reason each cannot be reached and the commands that measure the figure. The one line left is the _exit in SandboxCleaner.SignalTarget.process, which ends the process before coverage is written.


Documentation

  • Every page of Docs/CodeBase and Docs/Architecture was checked against the sources: signatures corrected, and the types that had no section given one. All 31 diagrams render
  • Docs/USAGE.MD, Docs/INSTALLATION.MD, Docs/OPERATORS.md and the plugin skill: the default tier's three operators, the SPM worker count and the Xcode requirement are stated correctly
  • Docs/BUILDING.md gives the plain swift test commands for tests and coverage

Known issues

  • The Xcode path does not validate a baseline; a failing suite there still makes every mutant look killed
  • Signed-Releases and reproducible build provenance are not in place
  • A mutant that hangs pays the targeted timeout and then the full one

Requirements

  • macOS 15+
  • Swift 6.2+ (Xcode 26)
  • Xcode project or workspace with a valid scheme and test target, or an SPM package with a test target

Installation

See the Installation Guide for Homebrew, pre-built binary, and build from source instructions.


What's Changed

  • Fix every open fix-labelled issue and what they left behind by @ericodx in #159
  • Implement every open performance issue: warm Xcode sandboxes, no test diagnostics, and one pass where there were many by @ericodx in #161
  • Region coverage, documentation review and two execution fixes by @ericodx in #162

Full Changelog: v1.6.0...v1.6.1