kimera 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.kimera.yml.example +59 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +528 -0
- data/exe/kimera +22 -0
- data/lib/kimera/audit/operator_audit.rb +71 -0
- data/lib/kimera/cli/argv.rb +15 -0
- data/lib/kimera/cli/baseline.rb +87 -0
- data/lib/kimera/cli/changed.rb +27 -0
- data/lib/kimera/cli/ci.rb +27 -0
- data/lib/kimera/cli/completion.rb +37 -0
- data/lib/kimera/cli/doctor.rb +115 -0
- data/lib/kimera/cli/flag.rb +55 -0
- data/lib/kimera/cli/help_text.rb +45 -0
- data/lib/kimera/cli/init.rb +105 -0
- data/lib/kimera/cli/mutant.rb +82 -0
- data/lib/kimera/cli/report_file.rb +20 -0
- data/lib/kimera/cli/run/arguments.rb +58 -0
- data/lib/kimera/cli/run/cycle.rb +78 -0
- data/lib/kimera/cli/run/digest.rb +49 -0
- data/lib/kimera/cli/run/emission.rb +45 -0
- data/lib/kimera/cli/run/gate.rb +43 -0
- data/lib/kimera/cli/run/options.rb +85 -0
- data/lib/kimera/cli/run/pass.rb +100 -0
- data/lib/kimera/cli/run/sources.rb +56 -0
- data/lib/kimera/cli/run.rb +52 -0
- data/lib/kimera/cli/skill.rb +23 -0
- data/lib/kimera/cli/suggestion.rb +36 -0
- data/lib/kimera/cli/survivors/panel.rb +81 -0
- data/lib/kimera/cli/survivors.rb +89 -0
- data/lib/kimera/cli/synthesize.rb +66 -0
- data/lib/kimera/cli/workflows.rb +8 -0
- data/lib/kimera/cli.rb +132 -0
- data/lib/kimera/error.rb +3 -0
- data/lib/kimera/execution/baseline_failure.rb +47 -0
- data/lib/kimera/execution/baseline_pass.rb +123 -0
- data/lib/kimera/execution/boot.rb +50 -0
- data/lib/kimera/execution/callback.rb +33 -0
- data/lib/kimera/execution/child_process.rb +48 -0
- data/lib/kimera/execution/composite_isolation.rb +24 -0
- data/lib/kimera/execution/harness.rb +150 -0
- data/lib/kimera/execution/isolated.rb +64 -0
- data/lib/kimera/execution/isolated_child.rb +15 -0
- data/lib/kimera/execution/isolated_child_command.rb +44 -0
- data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
- data/lib/kimera/execution/isolated_plan.rb +54 -0
- data/lib/kimera/execution/isolated_scheduling.rb +54 -0
- data/lib/kimera/execution/isolated_test_selection.rb +22 -0
- data/lib/kimera/execution/isolated_verdict.rb +72 -0
- data/lib/kimera/execution/isolated_watchdog.rb +59 -0
- data/lib/kimera/execution/isolation.rb +17 -0
- data/lib/kimera/execution/null_progress.rb +17 -0
- data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
- data/lib/kimera/execution/overlay_guards.rb +56 -0
- data/lib/kimera/execution/parallel_test_databases.rb +64 -0
- data/lib/kimera/execution/pool_driver.rb +101 -0
- data/lib/kimera/execution/priority.rb +23 -0
- data/lib/kimera/execution/reload.rb +104 -0
- data/lib/kimera/execution/rig.rb +41 -0
- data/lib/kimera/execution/schedule.rb +105 -0
- data/lib/kimera/execution/schemata.rb +88 -0
- data/lib/kimera/execution/shift/attempt.rb +34 -0
- data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
- data/lib/kimera/execution/shift/killer_memory.rb +20 -0
- data/lib/kimera/execution/shift/leak_guard.rb +34 -0
- data/lib/kimera/execution/shift.rb +137 -0
- data/lib/kimera/execution/sweep.rb +30 -0
- data/lib/kimera/execution/transaction_isolation.rb +32 -0
- data/lib/kimera/execution/trial.rb +16 -0
- data/lib/kimera/execution/verdicts.rb +78 -0
- data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
- data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
- data/lib/kimera/execution/worker_pool/worker.rb +32 -0
- data/lib/kimera/execution/worker_pool.rb +101 -0
- data/lib/kimera/frameworks/adapter.rb +82 -0
- data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
- data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
- data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
- data/lib/kimera/incremental/git_diff.rb +98 -0
- data/lib/kimera/incremental/selection.rb +27 -0
- data/lib/kimera/incremental/session.rb +109 -0
- data/lib/kimera/memoization.rb +104 -0
- data/lib/kimera/operators/argument_drop.rb +19 -0
- data/lib/kimera/operators/arithmetic.rb +12 -0
- data/lib/kimera/operators/audit.rb +3 -0
- data/lib/kimera/operators/base.rb +40 -0
- data/lib/kimera/operators/binary_swap.rb +23 -0
- data/lib/kimera/operators/boolean.rb +26 -0
- data/lib/kimera/operators/chain_link_deletion.rb +25 -0
- data/lib/kimera/operators/collection_literal.rb +18 -0
- data/lib/kimera/operators/comparison.rb +13 -0
- data/lib/kimera/operators/conditional.rb +36 -0
- data/lib/kimera/operators/default_argument.rb +17 -0
- data/lib/kimera/operators/element_drop.rb +31 -0
- data/lib/kimera/operators/index_fetch.rb +15 -0
- data/lib/kimera/operators/kernel_coercion.rb +17 -0
- data/lib/kimera/operators/method_unwrap.rb +20 -0
- data/lib/kimera/operators/negation.rb +15 -0
- data/lib/kimera/operators/node_swap.rb +20 -0
- data/lib/kimera/operators/numeric_literal.rb +39 -0
- data/lib/kimera/operators/op_assign.rb +18 -0
- data/lib/kimera/operators/plugins.rb +27 -0
- data/lib/kimera/operators/rails_association.rb +24 -0
- data/lib/kimera/operators/rails_callback.rb +49 -0
- data/lib/kimera/operators/rails_declaration.rb +16 -0
- data/lib/kimera/operators/rails_permit.rb +34 -0
- data/lib/kimera/operators/rails_validation.rb +17 -0
- data/lib/kimera/operators/range.rb +15 -0
- data/lib/kimera/operators/regexp_literal.rb +16 -0
- data/lib/kimera/operators/respond_to_guard.rb +24 -0
- data/lib/kimera/operators/return_value.rb +36 -0
- data/lib/kimera/operators/safe_navigation.rb +15 -0
- data/lib/kimera/operators/selector_swap.rb +24 -0
- data/lib/kimera/operators/statement_deletion.rb +26 -0
- data/lib/kimera/operators/string_literal.rb +22 -0
- data/lib/kimera/operators/symbol_literal.rb +26 -0
- data/lib/kimera/operators/vocabulary.rb +82 -0
- data/lib/kimera/operators.rb +141 -0
- data/lib/kimera/plugins.rb +3 -0
- data/lib/kimera/registry/builder.rb +59 -0
- data/lib/kimera/registry/mutation_point.rb +146 -0
- data/lib/kimera/registry/numbering.rb +15 -0
- data/lib/kimera/registry/registry.rb +120 -0
- data/lib/kimera/registry/source_file.rb +71 -0
- data/lib/kimera/registry/tally.rb +11 -0
- data/lib/kimera/registry/walking.rb +55 -0
- data/lib/kimera/report/actions.rb +38 -0
- data/lib/kimera/report/coloring.rb +25 -0
- data/lib/kimera/report/formats.rb +78 -0
- data/lib/kimera/report/live.rb +56 -0
- data/lib/kimera/report/log.rb +46 -0
- data/lib/kimera/report/progress.rb +55 -0
- data/lib/kimera/report/screen.rb +44 -0
- data/lib/kimera/report/sections.rb +50 -0
- data/lib/kimera/report/tally.rb +84 -0
- data/lib/kimera/report/text.rb +104 -0
- data/lib/kimera/results/result.rb +72 -0
- data/lib/kimera/results/run_report.rb +114 -0
- data/lib/kimera/rewrite/ast_walk.rb +20 -0
- data/lib/kimera/rewrite/directive.rb +61 -0
- data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
- data/lib/kimera/rewrite/directive_handlers.rb +138 -0
- data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
- data/lib/kimera/rewrite/numbered_params.rb +50 -0
- data/lib/kimera/runtime.rb +51 -0
- data/lib/kimera/scope/config.rb +90 -0
- data/lib/kimera/scope/file_set.rb +51 -0
- data/lib/kimera/scope/ignore_list.rb +55 -0
- data/lib/kimera/self_protection.rb +56 -0
- data/lib/kimera/support/duration.rb +13 -0
- data/lib/kimera/support/errors.rb +8 -0
- data/lib/kimera/support/operator_protocol.rb +10 -0
- data/lib/kimera/support/syntax.rb +68 -0
- data/lib/kimera/support/syntax_types.rb +20 -0
- data/lib/kimera/support/unparse.rb +15 -0
- data/lib/kimera/support/version.rb +5 -0
- data/lib/kimera/support/warnings.rb +15 -0
- data/lib/kimera/synthesis/file_weave.rb +78 -0
- data/lib/kimera/synthesis/guard_weaver.rb +77 -0
- data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
- data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
- data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
- data/lib/kimera/synthesis/overlay.rb +52 -0
- data/lib/kimera/synthesis/overlay_splice.rb +101 -0
- data/lib/kimera/synthesis/project.rb +58 -0
- data/lib/kimera/synthesis/source_map.rb +42 -0
- data/lib/kimera/version.rb +3 -0
- data/lib/kimera.rb +13 -0
- data/skills/kimera/SKILL.md +118 -0
- metadata +259 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1a60ca56321e6bfd3e82903b80f1ad75fc16e0647fff5f88be9042424643e3f0
|
|
4
|
+
data.tar.gz: d204852e218e3707f920e9ab5b1efb422395c971b2fa69aecb5a571ac28b7d73
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 63e971e4626da90289ad7b6f8a9483190820b6defa09f12d3e070639d3606fdde01cccaf73da24774de75b212764623747be187f3435b9e8cb7e4e50223e0d1f
|
|
7
|
+
data.tar.gz: be9614ecc3c6d9a89add30e5893a6ccbb5c934b0b06c5df518f19cd0483a9ade6eded8cfd15409c762cb147bf3f6c6803bf75607190e755977bdac0c2b2753a6
|
data/.kimera.yml.example
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Example Kimera configuration. Copy to .kimera.yml and adjust.
|
|
2
|
+
# Every key is optional; command-line flags override these values.
|
|
3
|
+
|
|
4
|
+
# Test framework: rspec (default) or minitest
|
|
5
|
+
framework: rspec
|
|
6
|
+
|
|
7
|
+
# Files to mutate (globs, dirs, or paths)
|
|
8
|
+
paths:
|
|
9
|
+
- "app/**/*.rb"
|
|
10
|
+
- "lib/**/*.rb"
|
|
11
|
+
|
|
12
|
+
# Test files to run
|
|
13
|
+
tests:
|
|
14
|
+
- "spec/**/*_spec.rb"
|
|
15
|
+
|
|
16
|
+
# Files to skip
|
|
17
|
+
exclude:
|
|
18
|
+
- "app/legacy/**/*.rb"
|
|
19
|
+
|
|
20
|
+
# Mutation operators to enable (default: this low-noise set; see the README
|
|
21
|
+
# for the extended and rails families, or use "all")
|
|
22
|
+
operators:
|
|
23
|
+
- comparison
|
|
24
|
+
- boolean_connective
|
|
25
|
+
- boolean_literal
|
|
26
|
+
- statement_deletion
|
|
27
|
+
- negation
|
|
28
|
+
- conditional
|
|
29
|
+
|
|
30
|
+
# Concurrency: number of warm pool workers pulling from one shared queue
|
|
31
|
+
jobs: 4
|
|
32
|
+
|
|
33
|
+
# CI gate: fail only if survivors exceed this count (omit for "any survivor fails")
|
|
34
|
+
max_survivors: 0
|
|
35
|
+
|
|
36
|
+
# Budget for the ignore list below: the gate fails when more than this many
|
|
37
|
+
# mutants are ignore-listed, so growing the list is a deliberate, reviewable act.
|
|
38
|
+
max_ignored: 1
|
|
39
|
+
|
|
40
|
+
# Also fail when any mutant in scope has no covering test. Best on incremental
|
|
41
|
+
# runs (--since), where "no coverage" means the diff adds untested logic.
|
|
42
|
+
fail_on_no_coverage: false
|
|
43
|
+
|
|
44
|
+
# Per-test soft timeout / per-mutant hard watchdog timeout, in seconds
|
|
45
|
+
soft_timeout: 5.0
|
|
46
|
+
# hard_timeout: 16.0
|
|
47
|
+
|
|
48
|
+
# Equivalent-mutant suppression. Equivalence is undecidable, so mark known
|
|
49
|
+
# equivalents by hand; matched mutants are excluded from the score and gate.
|
|
50
|
+
# A rule matches when every field it lists matches (file is a glob). Every
|
|
51
|
+
# entry must carry a reason: — entries without one are rejected at startup.
|
|
52
|
+
ignore:
|
|
53
|
+
- file: "app/models/discount.rb"
|
|
54
|
+
line: 33
|
|
55
|
+
label: "> => >="
|
|
56
|
+
reason: known equivalent at the 50 boundary (spend > 50 vs >= 50 both hit the tier)
|
|
57
|
+
# - file: "app/services/*.rb"
|
|
58
|
+
# original: "a == b"
|
|
59
|
+
# reason: why this mutant is equivalent
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-09-26)
|
|
4
|
+
|
|
5
|
+
Initial public release.
|
|
6
|
+
|
|
7
|
+
- Schemata-based mutation testing for Ruby and Rails. All mutants for a file
|
|
8
|
+
compile into one guarded-dispatch program and are selected at runtime, so the
|
|
9
|
+
suite loads once and mutants flip in a warm process.
|
|
10
|
+
- Warm worker pool (`--jobs N`) pulling from one shared, heaviest-first queue,
|
|
11
|
+
with opt-in per-mutant transaction rollback (`--isolate-db`), cache resets,
|
|
12
|
+
per-worker test databases, and a hard-killable watchdog.
|
|
13
|
+
- Covering tests run most-recent-killer first, cutting test executions without
|
|
14
|
+
narrowing the measured covering set.
|
|
15
|
+
- Diff-driven incremental mode (`--since`) with resumable sessions and CI
|
|
16
|
+
gating (`--max-survivors`, `--max-errors`, `--fail-on-no-coverage`).
|
|
17
|
+
- Isolated mode (`--isolated`): each mutant baked into a fresh tree and judged
|
|
18
|
+
in a clean subprocess.
|
|
19
|
+
- RSpec and Minitest adapters, including `before(:suite)` / `after(:suite)`
|
|
20
|
+
hooks. `--exclude-test GLOB` drops unrunnable test files. A red baseline
|
|
21
|
+
prints how to reproduce it without Kimera.
|
|
22
|
+
- Low-noise default operators, plus extended and Rails-aware families
|
|
23
|
+
(`--operators all`). These include contract operators (`index_fetch`,
|
|
24
|
+
`kernel_coercion`, `respond_to_guard`, `symbol_literal`, `default_argument`)
|
|
25
|
+
and `chain_link_deletion` (`a.where(x).recent.first` => `a.where(x).first`).
|
|
26
|
+
- Harness failures get their own `harness_error` status and never count as
|
|
27
|
+
kills.
|
|
28
|
+
- Equivalent-mutant ignore list: a reason is required, `max_ignored` budgets
|
|
29
|
+
it, `column:` pins ambiguous lines, and a warning fires when an entry no
|
|
30
|
+
longer matches any mutant.
|
|
31
|
+
- `kimera skill` prints a bundled guide for AI coding agents: how to run,
|
|
32
|
+
triage, and gate with Kimera, and the rules against gaming the score.
|
|
33
|
+
`kimera init` points agents to it from `AGENTS.md`.
|
|
34
|
+
- Kimera holds itself to zero surviving mutants: the full warm self-host run
|
|
35
|
+
gates at `max_survivors: 0`, with harness-critical files judged in the
|
|
36
|
+
isolated tier.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kimera contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
|
|
4
|
+
<img src="assets/logo-light.svg" alt="Kimera" width="464">
|
|
5
|
+
</picture>
|
|
6
|
+
</h1>
|
|
7
|
+
|
|
8
|
+
Mutation testing for Ruby and Rails, fast enough to gate a pull request.
|
|
9
|
+
|
|
10
|
+
Kimera plants small bugs ("mutants") in your code and checks that a test fails
|
|
11
|
+
for each one. A mutant no test catches is a **survivor**: a hole in your suite
|
|
12
|
+
that line coverage can't see. Kimera compiles all mutants into your code *once*
|
|
13
|
+
and flips them at runtime, so your app boots once, not once per mutant.
|
|
14
|
+
|
|
15
|
+
- **Fast.** [Stryker](https://stryker-mutator.io)-style mutant schemata
|
|
16
|
+
([Untch, Offutt & Harrold, 1993](https://doi.org/10.1145/154183.154265)): no
|
|
17
|
+
reparse or reload per mutant. On an 8-core Apple M3, dry-inflector's `lib`
|
|
18
|
+
(742 mutants, `--operators all`) runs in 17s with `--jobs 4`. Kimera's own
|
|
19
|
+
codebase (2,552 mutants) takes about 6 minutes with `--jobs 8`, mostly in the
|
|
20
|
+
fresh-process tier for harness-critical files.
|
|
21
|
+
- **Parallel.** Warm workers, forked once, pull mutants from one shared
|
|
22
|
+
heaviest-first queue. A single large file spreads across every core.
|
|
23
|
+
- **Rails-ready.** Opt-in transaction rollback (`--isolate-db`), per-worker
|
|
24
|
+
test databases via Rails' parallel-testing hooks, and 4 Rails operator
|
|
25
|
+
families (strong params, validations, callbacks, associations).
|
|
26
|
+
- **Built for CI.** `kimera changed` mutates only the lines a PR touched.
|
|
27
|
+
`kimera ci` fails the build on any survivor or untested change.
|
|
28
|
+
- **Low noise.** A small default operator set. 19 extended families are one
|
|
29
|
+
flag away, and you can write your own.
|
|
30
|
+
- **Honest verdicts.** Code the warm path can't judge safely is reported
|
|
31
|
+
separately, never scored. `--isolated` re-checks any mutant in a fresh
|
|
32
|
+
process.
|
|
33
|
+
- **Small.** Ruby >= 3.4 (CI covers 3.4 and 4.0), RSpec or Minitest, and two
|
|
34
|
+
dependencies: [Prism](https://github.com/ruby/prism) and
|
|
35
|
+
[unparser](https://github.com/mbj/unparser).
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
$ bundle exec kimera run app --since origin/main --tests 'spec/**/*_spec.rb'
|
|
39
|
+
mutants=28 killed=17 survived=5 timeout=0 error=0 no_coverage=6 score=77.3%
|
|
40
|
+
|
|
41
|
+
survived #1 app/models/discount.rb:18 [<= => <]
|
|
42
|
+
- order_total <= 0
|
|
43
|
+
+ order_total < 0
|
|
44
|
+
covered by 7 test(s): ./spec/discount_spec.rb[1:1:1], …
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This survivor says no test checks an order total of exactly `0`. Add that test
|
|
48
|
+
and the mutant dies.
|
|
49
|
+
|
|
50
|
+
**Contents:** [Getting started](#getting-started) ·
|
|
51
|
+
[Commands](#commands) · [Configuration](#configuration) ·
|
|
52
|
+
[Operators](#operators) ([Rails](#rails), [Custom](#custom-operators)) ·
|
|
53
|
+
[How it works](#how-it-works) · [Design notes](#design-notes--known-limits) ·
|
|
54
|
+
[Development](#development)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Getting started
|
|
59
|
+
|
|
60
|
+
**1. Add Kimera to your Gemfile.** It runs inside your app's bundle so
|
|
61
|
+
dependency versions resolve consistently.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
gem "kimera", group: :test
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
bundle install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**2. Generate a config.** `init` detects RSpec or Minitest, your source roots
|
|
72
|
+
(`app/`, `lib/`), and your test glob. It writes `.kimera.yml`, and adds a line
|
|
73
|
+
to `AGENTS.md` pointing AI coding agents to `kimera skill`: a guide to running
|
|
74
|
+
Kimera and triaging survivors without gaming the score. It matches your
|
|
75
|
+
installed version and works with any agent that can run a shell command.
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
bundle exec kimera init
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**3. Check the setup.** `doctor` confirms source and test discovery, Git, and
|
|
82
|
+
Rails parallel-safety. `--check-baseline` also runs your suite once to confirm
|
|
83
|
+
it is green.
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
bundle exec kimera doctor --check-baseline
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**4. Run it.** Start with the code your branch changed, then widen.
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
bundle exec kimera changed # lines changed since origin/main (or main)
|
|
93
|
+
bundle exec kimera run app/models # one directory
|
|
94
|
+
bundle exec kimera run # every path in .kimera.yml
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**5. Triage survivors.** Save a report so you can dig in without rerunning the
|
|
98
|
+
suite.
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
bundle exec kimera run --report tmp/kimera/report.json
|
|
102
|
+
bundle exec kimera report tmp/kimera/report.json --status survived
|
|
103
|
+
bundle exec kimera mutant 42 --report tmp/kimera/report.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For each survivor, write the test that kills it. If a mutant is truly
|
|
107
|
+
equivalent, add it to `ignore:` in `.kimera.yml` with a reason (see
|
|
108
|
+
[Configuration](#configuration)).
|
|
109
|
+
|
|
110
|
+
**6. Gate pull requests.** `kimera ci` fails on any survivor or uncovered
|
|
111
|
+
mutant. It writes `tmp/kimera/report.json` and prints GitHub annotations. Pass
|
|
112
|
+
`--since` to mutate only the PR's lines (this needs full Git history):
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
# .github/workflows/mutation.yml
|
|
116
|
+
name: Mutation
|
|
117
|
+
on: pull_request
|
|
118
|
+
jobs:
|
|
119
|
+
kimera:
|
|
120
|
+
runs-on: ubuntu-latest
|
|
121
|
+
steps:
|
|
122
|
+
- uses: actions/checkout@v4
|
|
123
|
+
with:
|
|
124
|
+
fetch-depth: 0
|
|
125
|
+
- uses: ruby/setup-ruby@v1
|
|
126
|
+
with:
|
|
127
|
+
bundler-cache: true
|
|
128
|
+
- run: bundle exec kimera ci --since origin/${{ github.base_ref }}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> **Adopting on an existing suite?** Snapshot today's survivors as reviewed
|
|
132
|
+
> debt, so the gate blocks only *new* holes:
|
|
133
|
+
> `bundle exec kimera baseline create tmp/kimera/report.json --reason "adopting Kimera"`.
|
|
134
|
+
> Then add the printed `baseline:` line to `.kimera.yml`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Commands
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
# Summarize what would be mutated
|
|
142
|
+
bundle exec kimera registry app
|
|
143
|
+
|
|
144
|
+
# Write the synthesized schemata mirror (for inspection)
|
|
145
|
+
bundle exec kimera synthesize app --out tmp/schemata
|
|
146
|
+
|
|
147
|
+
# Run the full mutation suite
|
|
148
|
+
bundle exec kimera run app --tests 'spec/**/*_spec.rb'
|
|
149
|
+
|
|
150
|
+
# Minitest
|
|
151
|
+
bundle exec kimera run app --framework minitest --tests 'test/**/*_test.rb'
|
|
152
|
+
|
|
153
|
+
# Incremental: mutate only lines changed since main, gate CI, resume on retry
|
|
154
|
+
bundle exec kimera run app --since origin/main \
|
|
155
|
+
--session tmp/kimera.json --max-survivors 0 --report tmp/report.json
|
|
156
|
+
|
|
157
|
+
# Also fail when a changed line has no covering test
|
|
158
|
+
# (a no_coverage mutant is untested new logic, not a pass)
|
|
159
|
+
bundle exec kimera run app --since origin/main --fail-on-no-coverage
|
|
160
|
+
|
|
161
|
+
# N warm workers pull from one shared queue, so this scales with cores
|
|
162
|
+
# even on a single big file
|
|
163
|
+
bundle exec kimera run app --jobs 4
|
|
164
|
+
|
|
165
|
+
# Drop spec files that can't run this way (order-dependent, need a browser)
|
|
166
|
+
# without rewriting the whole --tests glob
|
|
167
|
+
bundle exec kimera run app --exclude-test 'spec/system/**/*_spec.rb'
|
|
168
|
+
|
|
169
|
+
# Judge every mutant in a fresh subprocess: bake it into a mirror of the tree
|
|
170
|
+
# and run its covering tests under plain rspec. Slower, but trustworthy even
|
|
171
|
+
# for code that can't be instrumented in-process.
|
|
172
|
+
bundle exec kimera run app --isolated
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Daily and CI workflows
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
# Mutate only the PR's changed lines (origin/main, falling back to main)
|
|
179
|
+
bundle exec kimera changed --report tmp/kimera/report.json
|
|
180
|
+
|
|
181
|
+
# Strict gate: zero survivors, no uncovered mutants, a JSON artifact, and
|
|
182
|
+
# GitHub annotations. Override with --format sarif/json.
|
|
183
|
+
bundle exec kimera ci
|
|
184
|
+
|
|
185
|
+
# Continue triage without rerunning the suite
|
|
186
|
+
bundle exec kimera report tmp/kimera/report.json --status no_coverage
|
|
187
|
+
bundle exec kimera mutant 42 --report tmp/kimera/report.json
|
|
188
|
+
|
|
189
|
+
# Snapshot pre-existing survivors while adopting Kimera
|
|
190
|
+
bundle exec kimera baseline create tmp/kimera/report.json --reason "adopting Kimera"
|
|
191
|
+
# Add the printed `baseline:` entry to .kimera.yml, then review it in code review
|
|
192
|
+
bundle exec kimera baseline review .kimera-baseline.yml
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Output formats and exit codes
|
|
196
|
+
|
|
197
|
+
- `--format json`, `ndjson`, `github`, and `sarif` write *only* that format to
|
|
198
|
+
stdout. Progress and diagnostics go to stderr.
|
|
199
|
+
- `--report FILE` always writes the canonical JSON report for later triage.
|
|
200
|
+
- Text output honors `NO_COLOR`. `--no-color` forces it off.
|
|
201
|
+
- `--quiet` suits scripts that only need an artifact. `--verbose` prints the
|
|
202
|
+
resolved scope. `--log FILE` keeps the final text report.
|
|
203
|
+
|
|
204
|
+
Exit codes:
|
|
205
|
+
|
|
206
|
+
- **`0`**: pass.
|
|
207
|
+
- **`2`**: the gate failed. Survivors exceed the threshold (default: any
|
|
208
|
+
survivor), or a mutant in scope has no covering test under
|
|
209
|
+
`--fail-on-no-coverage`, ignored mutants exceed `max_ignored`, or more
|
|
210
|
+
mutants than `--max-errors` (default 0) could not be judged.
|
|
211
|
+
- **`1`**: the baseline is not green, the pool cannot keep a worker alive, or
|
|
212
|
+
the invocation is invalid. Paths that match no source files, `--tests` that
|
|
213
|
+
matches no test files, and an unknown `--since` ref all count. An empty or
|
|
214
|
+
broken scope fails loudly instead of scoring perfectly over nothing.
|
|
215
|
+
|
|
216
|
+
`--isolated` trades speed for trust. It never overlays schemata or shares a
|
|
217
|
+
process with the code under test. On ordinary code it reaches the same verdicts
|
|
218
|
+
as the warm path. It can also test what the warm path can't: the runtime
|
|
219
|
+
selector and Kimera's own harness-critical files (see below).
|
|
220
|
+
|
|
221
|
+
### Progress output
|
|
222
|
+
|
|
223
|
+
During a run, a progress bar tracks each phase on stderr. It shows only when
|
|
224
|
+
stderr is a tty; `--[no-]progress` overrides. Completed phases stay visible:
|
|
225
|
+
|
|
226
|
+
- `baseline` records coverage.
|
|
227
|
+
- `mutants (warm)` uses the shared worker pool.
|
|
228
|
+
- `mutants (isolated)` evaluates the subset routed to fresh mirrors.
|
|
229
|
+
|
|
230
|
+
The last two labels appear only when a run uses both strategies. Output the
|
|
231
|
+
suite prints during mutant evaluation is swallowed, since mutated code warns,
|
|
232
|
+
raises, and logs freely. Verdicts travel back as structured results.
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
baseline [========================] 895/895 100% 0:03
|
|
236
|
+
mutants (warm) [========================] 2104/2104 100% killed=1867 survived=137 no_coverage=100 0:59
|
|
237
|
+
mutants (isolated) [===========> ] 224/445 50% killed=196 survived=0 no_coverage=19 timeout=9 2:01
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Configuration
|
|
243
|
+
|
|
244
|
+
`.kimera.yml` in the project root sets defaults. Command-line flags override
|
|
245
|
+
them.
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
framework: rspec
|
|
249
|
+
paths: ["app/**/*.rb"]
|
|
250
|
+
tests: ["spec/**/*_spec.rb"]
|
|
251
|
+
exclude: ["app/legacy/**/*.rb"]
|
|
252
|
+
operators: [comparison, boolean_connective, boolean_literal, statement_deletion, negation, conditional]
|
|
253
|
+
# Ruby files loaded before the scan, for custom operators.
|
|
254
|
+
require: []
|
|
255
|
+
jobs: 4
|
|
256
|
+
max_survivors: 0
|
|
257
|
+
# Fail when any mutant in scope lacks a covering test. Best on incremental
|
|
258
|
+
# runs, where "no coverage" means the PR adds logic no test executes.
|
|
259
|
+
fail_on_no_coverage: false
|
|
260
|
+
|
|
261
|
+
# The gate fails when more than this many mutants are ignore-listed. Marking
|
|
262
|
+
# one more mutant equivalent means raising this number in the same diff, so
|
|
263
|
+
# the ignore list stays a reviewed decision, not a shortcut past a test.
|
|
264
|
+
max_ignored: 1
|
|
265
|
+
|
|
266
|
+
# Accepted pre-adoption survivors, merged with the ignore rules below.
|
|
267
|
+
# Uncomment only after creating the file with `kimera baseline create`.
|
|
268
|
+
# baseline: .kimera-baseline.yml
|
|
269
|
+
|
|
270
|
+
# Known-equivalent mutants. Equivalence is undecidable, so Kimera doesn't
|
|
271
|
+
# guess: you mark a mutant and it stops being a survivor. An entry without
|
|
272
|
+
# a reason: is rejected at startup.
|
|
273
|
+
ignore:
|
|
274
|
+
- file: app/models/discount.rb
|
|
275
|
+
line: 33
|
|
276
|
+
label: "> => >="
|
|
277
|
+
reason: known equivalent at the 50 boundary (spend > 50 vs >= 50 both hit the tier)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Operators
|
|
283
|
+
|
|
284
|
+
The *default* set is small and low-noise:
|
|
285
|
+
|
|
286
|
+
- comparison operators (boundary and negation)
|
|
287
|
+
- boolean connectives and boolean literals
|
|
288
|
+
- deletion of side-effecting statements
|
|
289
|
+
- `!x => x`
|
|
290
|
+
- forced `if` conditions
|
|
291
|
+
|
|
292
|
+
The extended families are one flag away. Use `--operators all`, or pick from:
|
|
293
|
+
|
|
294
|
+
| family | mutation |
|
|
295
|
+
|---|---|
|
|
296
|
+
| `arithmetic` | `+ => -`, `* => /`, `% => *`, `** => *`, ... |
|
|
297
|
+
| `safe_navigation` | `a&.b => a.b` |
|
|
298
|
+
| `numeric_literal` | `n => n-1, n+1, 0`; floats `f => f+1.0` |
|
|
299
|
+
| `string_literal` | `"text" => ""` |
|
|
300
|
+
| `collection_literal` | `[a, b] => []`, `{k: v} => {}` |
|
|
301
|
+
| `range` | `a..b <=> a...b` |
|
|
302
|
+
| `method_unwrap` | `x.strip => x` (curated argless transformations) |
|
|
303
|
+
| `return_value` | `return x => return nil`; def-body tail `=> nil` |
|
|
304
|
+
| `selector_swap` | `select <=> reject`, `all? <=> any?`, `min <=> max`, `first <=> last`, `detect => first` |
|
|
305
|
+
| `regexp` | `/pat/ => //` (match all) and `=> /(?!)/` (match none) |
|
|
306
|
+
| `element_drop` | `[a, b, c]` / `{k: v, ...}` => drop one element/pair |
|
|
307
|
+
| `argument_drop` | `pay(amount, currency)` => drop one argument |
|
|
308
|
+
| `op_assign` | `total += x => total -= x`, `*= <=> /=` |
|
|
309
|
+
| `index_fetch` | `h[k] => h.fetch(k)` (a tolerated miss becomes a `KeyError`) |
|
|
310
|
+
| `kernel_coercion` | `Array(x) => x`, likewise `String`/`Integer`/`Float` |
|
|
311
|
+
| `respond_to_guard` | `x.respond_to?(:m) => x` |
|
|
312
|
+
| `symbol_literal` | `:total => :total__kimera__` |
|
|
313
|
+
| `default_argument` | `def f(a = 1)` / `f(b: 1)` => the parameter becomes required |
|
|
314
|
+
| `chain_link_deletion` | `a.where(x).recent.first => a.where(x).first`: drop one link, keep both ends |
|
|
315
|
+
|
|
316
|
+
Turning them all on typically triples the mutant count. Adopt them through the
|
|
317
|
+
baseline burn-down workflow, not by flipping them on in a mature gate.
|
|
318
|
+
|
|
319
|
+
**Contract operators.** The five from `index_fetch` through `default_argument`
|
|
320
|
+
each remove an affordance the code grants its callers. A survivor means no test
|
|
321
|
+
exercises the case the affordance exists for. `index_fetch` and
|
|
322
|
+
`kernel_coercion` are equivalent wherever the key is always present or the
|
|
323
|
+
value already has the target shape. That is the normal case for an options hash
|
|
324
|
+
built from a defaults literal. Scope them to data that crosses a boundary
|
|
325
|
+
(params, parsed JSON, ENV); on internal bookkeeping they are noise.
|
|
326
|
+
|
|
327
|
+
**`chain_link_deletion`** casts the widest net and matters most for Rails. Most
|
|
328
|
+
Rails business rules live mid-chain: the scope, the filter, the decorator.
|
|
329
|
+
Deleting a link asks whether any test depends on it narrowing anything. It
|
|
330
|
+
generates roughly one mutant per chained call, so scope it to the code you are
|
|
331
|
+
burning down, not a whole app.
|
|
332
|
+
|
|
333
|
+
### Rails
|
|
334
|
+
|
|
335
|
+
Four Rails-aware families ship as the `rails` group. `--operators
|
|
336
|
+
comparison,rails` composes, and `all` includes them.
|
|
337
|
+
|
|
338
|
+
| family | mutation | what a survivor means |
|
|
339
|
+
|---|---|---|
|
|
340
|
+
| `rails_permit` | `permit(:name, :admin)` => drop one key | over-permissive strong params no test notices |
|
|
341
|
+
| `rails_validation` | delete `validates ...` | a validation no test ever violates |
|
|
342
|
+
| `rails_callback` | delete `before_action :authenticate_user!` etc.; drop one action from an `only:`/`except:` list | an unguarded path (or a guarded action) no test exercises |
|
|
343
|
+
| `rails_association` | `has_many :posts, dependent: :destroy` => drop `dependent:` | orphaned-record cleanup no test observes |
|
|
344
|
+
|
|
345
|
+
- **Scope lambdas** are mutated on the *warm* path. A `-> { ... }` body at class
|
|
346
|
+
level re-executes per call, so ordinary operators mutate it safely
|
|
347
|
+
(`scope :active, -> { where(active: true) }`). Run-once `do ... end` DSL
|
|
348
|
+
blocks (`included do`, `class_eval`) stay excluded.
|
|
349
|
+
- **`rails_permit`** runs on the normal warm path.
|
|
350
|
+
- **Validations and callbacks** are class-body DSL. They execute once at load,
|
|
351
|
+
so no in-process scheme can judge them. The warm run reports them as
|
|
352
|
+
`isolated_only` (excluded from the score, never gating). `--isolated` gives
|
|
353
|
+
the verdict by baking each deletion into a fresh tree.
|
|
354
|
+
- **Zeitwerk.** Kimera calls `Rails.application.eager_load!` before overlaying,
|
|
355
|
+
so lazily autoloaded constants can't escape mutation.
|
|
356
|
+
|
|
357
|
+
### Custom operators
|
|
358
|
+
|
|
359
|
+
The built-in operators cover what every Ruby app shares. The mutants that find
|
|
360
|
+
the most in *your* app are often ones only you can write: the call whose
|
|
361
|
+
deletion should break a test, the keyword whose absence should be caught.
|
|
362
|
+
Register them the way you add a RuboCop cop: a Ruby file named in `require:`,
|
|
363
|
+
loaded before the scan.
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
# lib/kimera/operators/authorization.rb
|
|
367
|
+
require "kimera/operators"
|
|
368
|
+
|
|
369
|
+
class Authorization < Kimera::Operators::Base
|
|
370
|
+
NAMES = %i[authorize authorize! policy_scope].freeze
|
|
371
|
+
|
|
372
|
+
class << self
|
|
373
|
+
def key = "authorization"
|
|
374
|
+
|
|
375
|
+
def statement? = true
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def variants(node, **)
|
|
379
|
+
return unless matches?(node, NAMES)
|
|
380
|
+
deletion(node)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
Kimera::Operators.register(Authorization)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```yaml
|
|
388
|
+
# .kimera.yml
|
|
389
|
+
require: [lib/kimera/operators/authorization.rb]
|
|
390
|
+
operators: [comparison, custom] # `custom` is every operator you registered
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
`variants` receives a [Prism](https://github.com/ruby/prism) node and returns
|
|
394
|
+
`Variant`s. Each has a label for the report and a *directive* naming a rewrite
|
|
395
|
+
Kimera already knows: `selector_swap`, `drop_argument`, `kwarg_pair_drop`,
|
|
396
|
+
`statement_deletion`, and the rest of the built-in vocabulary. A directive type
|
|
397
|
+
with no handler is an error, not a silent pass. Custom rewrite handlers are not
|
|
398
|
+
supported yet.
|
|
399
|
+
|
|
400
|
+
Operators load once, in the CLI parent. The warm pool forks its workers and the
|
|
401
|
+
isolated tier bakes each mutant before spawning. So a registered operator
|
|
402
|
+
reaches every process that builds a mutant, with no per-worker setup.
|
|
403
|
+
|
|
404
|
+
Seven worked examples ship in [`examples/operators`](examples/operators), each
|
|
405
|
+
with a note on what a survivor means:
|
|
406
|
+
|
|
407
|
+
- authorization deletion
|
|
408
|
+
- bang-call stripping
|
|
409
|
+
- `perform_later => perform_now`
|
|
410
|
+
- HTTP status dropping
|
|
411
|
+
- cache-expiry dropping
|
|
412
|
+
- money rounding
|
|
413
|
+
- encrypted attribute deletion
|
|
414
|
+
|
|
415
|
+
**Audit your operator before it guards anything.** A run can't show two
|
|
416
|
+
failures: a directive that won't render, and one that rebuilds the original
|
|
417
|
+
source. The second is an equivalent mutant that survives every run and ends up
|
|
418
|
+
hand-added to the ignore list. Check it against a representative snippet:
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
require "kimera/operators/audit"
|
|
422
|
+
|
|
423
|
+
faults = Kimera::Operators::Audit.faults([Authorization.new], File.read("app/controllers/orders_controller.rb"))
|
|
424
|
+
expect(faults).to(be_empty)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## How it works
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
source ──Prism──▶ registry (mutation points, JSON) ──┬─▶ synthesis (unparser) ─▶ schemata
|
|
433
|
+
└─▶ reporting
|
|
434
|
+
│
|
|
435
|
+
suite ─load once─▶ overlay schemata ─▶ baseline+coverage ─▶ warm worker-pool kill loop
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
1. **Registry.** Walk the Prism AST and record every mutation point: operator,
|
|
439
|
+
byte/line span, original node, and variant directives. Each gets a globally
|
|
440
|
+
unique integer id. The JSON registry is the single source of truth.
|
|
441
|
+
2. **Synthesis.** Replace each schema-safe point with a nested guarded dispatch
|
|
442
|
+
(`if Kimera::Runtime.active?(102) … else …end`) and unparse. Memoized
|
|
443
|
+
expressions (`@x ||= …`) and load-time-only code go to a reload fallback, so
|
|
444
|
+
they can never read as a false "survived".
|
|
445
|
+
3. **Execution.** Load the suite once and overlay the schemata. Check the
|
|
446
|
+
baseline is green while recording per-test coverage. With `--jobs N`, the
|
|
447
|
+
coverage pass also fans out across the warm pool (each worker runs a slice
|
|
448
|
+
of the examples). Then fork the warm pool and stream mutants to it from one
|
|
449
|
+
shared, heaviest-first queue. For each mutant, flip `Runtime.active` and run
|
|
450
|
+
only its covering tests. The queue is shared, not split by file, so
|
|
451
|
+
`--jobs N` scales with cores even when mutants sit in one large file.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Design notes & known limits
|
|
456
|
+
|
|
457
|
+
- **Runs under your bundle.** `unparser` pulls in a `diff-lcs` version that can
|
|
458
|
+
conflict with `rspec-expectations` outside a resolved bundle. Use
|
|
459
|
+
`bundle exec`. A source checkout's `exe/kimera` activates the current
|
|
460
|
+
directory's Gemfile before booting Kimera.
|
|
461
|
+
- **Schema-unsafe mutants.** Code that runs once at load (class/module bodies,
|
|
462
|
+
constants) or is memoized can't be toggled in a warm process. Kimera mutates
|
|
463
|
+
only inside method bodies and routes memoized points to a fork-per-mutant
|
|
464
|
+
reload fallback.
|
|
465
|
+
- **An un-emittable node costs one method, not a file.** `unparser` refuses to
|
|
466
|
+
re-emit some valid Ruby (an interpolated `%r{}x` pattern, for example). When
|
|
467
|
+
a whole file won't round-trip, Kimera guards and re-emits each method that
|
|
468
|
+
carries points and splices those back into the original text. The offending
|
|
469
|
+
constant stays verbatim and the rest of the file stays mutatable.
|
|
470
|
+
- **Standing risks and mitigations:**
|
|
471
|
+
- Global-state leakage: opt-in transaction rollback (`--isolate-db`), cache
|
|
472
|
+
resets, periodic kill-confirmation, one mutant per warm worker at a time.
|
|
473
|
+
- Selector-induced hangs: a soft per-test timeout, plus a watchdog that
|
|
474
|
+
SIGKILLs a wedged worker so a replacement pulls from the shared queue.
|
|
475
|
+
- Equivalent mutants: a small operator set plus human suppression hooks, not
|
|
476
|
+
a solver.
|
|
477
|
+
- **`.rspec` and suite hooks are honored.** The RSpec adapter applies the
|
|
478
|
+
project's `.rspec` (`--require spec_helper`/`rails_helper`, the `spec` load
|
|
479
|
+
path) as the `rspec` executable does. It fires `config.before(:suite)` once
|
|
480
|
+
before the baseline and `after(:suite)` at the end. Libraries install global
|
|
481
|
+
switches there: `webmock/rspec` calls `WebMock.enable!` only in that hook.
|
|
482
|
+
Without it, every stubbed HTTP request would reach the real network.
|
|
483
|
+
- **Parallel runs need parallel state.** With `--jobs N` on a Rails app, Kimera
|
|
484
|
+
runs Rails' parallel-testing fork hooks. Each worker gets its own database
|
|
485
|
+
(`app_test_0`, `app_test_1`, …), as with `rails test -j`, and the suite's
|
|
486
|
+
`before(:suite)` setup replays against it. Anything *else* your suite shares
|
|
487
|
+
(Redis, Elasticsearch, a temp directory) needs the same treatment. Register
|
|
488
|
+
a hook and Kimera runs it:
|
|
489
|
+
`ActiveSupport::Testing::Parallelization.after_fork_hook { |i| ... }`.
|
|
490
|
+
Without one, concurrent examples fight over one keyspace. That reads as a red
|
|
491
|
+
baseline, or worse, as kills that are really collisions.
|
|
492
|
+
- **Score definition.** `score = killed / evaluable`.
|
|
493
|
+
- Killed includes timeouts and errors (observable misbehaviour).
|
|
494
|
+
- Evaluable excludes `no_coverage`, `ignored`, `isolated_only`, and
|
|
495
|
+
`harness_error` mutants. Each is reported separately.
|
|
496
|
+
- A `harness_error` is a mutant Kimera could not judge: its worker died, or
|
|
497
|
+
its reply was unreadable. It is not a kill. It gates via `--max-errors`
|
|
498
|
+
(default 0). A pool that never produces a result aborts the run.
|
|
499
|
+
- Uncovered mutants don't lower the score, unlike some tools.
|
|
500
|
+
`--fail-on-no-coverage` closes that gap.
|
|
501
|
+
- **One top-level constant.** `require "kimera"` defines `::MutantRuntime`,
|
|
502
|
+
aliased to `Kimera::Runtime` unless already bound. Every synthesized guard
|
|
503
|
+
dispatches through it. It stays top-level so an outer measurer can pre-bind
|
|
504
|
+
it. It is the only constant Kimera adds outside its namespace.
|
|
505
|
+
|
|
506
|
+
## Mutation-testing Kimera with Kimera
|
|
507
|
+
|
|
508
|
+
Kimera gates itself at zero survivors (`bundle exec kimera run`, configured by
|
|
509
|
+
the repo's `.kimera.yml`). The warm path can't judge its own harness: the
|
|
510
|
+
runtime selector would recurse, and a mutant in the RSpec adapter can disable
|
|
511
|
+
the runner judging it. Those files are excluded or reported as
|
|
512
|
+
`isolated_only`. `bundle exec kimera run --isolated` judges them in fresh
|
|
513
|
+
subprocesses that share nothing with Kimera's runtime.
|
|
514
|
+
|
|
515
|
+
## Development
|
|
516
|
+
|
|
517
|
+
```sh
|
|
518
|
+
bundle install
|
|
519
|
+
bin/spec # run the test suite
|
|
520
|
+
COVERAGE=1 bin/spec # run with line+branch coverage (SimpleCov)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
`examples/` holds the runnable fixtures the integration tests drive
|
|
524
|
+
(`sample_app` for RSpec, `minitest_app` for Minitest).
|
|
525
|
+
|
|
526
|
+
## License
|
|
527
|
+
|
|
528
|
+
MIT
|
data/exe/kimera
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
if File.file?(File.join(Dir.pwd, "Gemfile")) && !defined?(Bundler)
|
|
6
|
+
require "bundler/setup"
|
|
7
|
+
raise(LoadError, "kimera is not in this project's Gemfile") if Gem.find_files("unparser.rb").empty?
|
|
8
|
+
end
|
|
9
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
10
|
+
require "kimera"
|
|
11
|
+
rescue LoadError, StandardError => error
|
|
12
|
+
raise unless error.is_a?(LoadError) || (defined?(Bundler::BundlerError) && error.is_a?(Bundler::BundlerError))
|
|
13
|
+
warn(
|
|
14
|
+
"kimera: could not load its dependencies (#{error.message.lines.first.strip})",
|
|
15
|
+
"Kimera runs inside your project's bundle: add `gem \"kimera\", group: :test` to the Gemfile, " \
|
|
16
|
+
"run `bundle install`, then `bundle exec kimera`."
|
|
17
|
+
)
|
|
18
|
+
exit(1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
status = Kimera::CLI.start(ARGV)
|
|
22
|
+
exit(status.is_a?(Integer) ? status : 0)
|