active_mutator 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c401b5edbafa0a072b6672e088a9016f918d9e76ce6b264a9e2a054b1b538cc6
4
- data.tar.gz: d2a1908e950d8687e68524184efc2bce200d595be3b25f31e3a0e92a95325b3e
3
+ metadata.gz: cfe7e211472b45836efb059eb37472307becf74e85b8441664e8cec836c10c26
4
+ data.tar.gz: 6e0ed299bc84b8c912e73314f58ea88cc2fbb783f6949aea14285a27ea2e5bd9
5
5
  SHA512:
6
- metadata.gz: 9d42e2e4a207ab1c6ced606eeeee342389b503da5055e207d282df220f112537f90b5f967079dfd76060d740f710d5441c8422565e7bdad314fa4bc9cf5d07c6
7
- data.tar.gz: 3e226d88dc71b5b0b698f5e2a1148a38c7f6b12e0cd47411033368a0bf7d18a90d0ca8243fa3d2841ebd1b1be4cfb554c41732b4e1eacc2961f6477ddb692cc7
6
+ metadata.gz: 6153450b921e1647a13d14f4e473cc123c045ff738f2cea1b6786797e265b05bc7bc00233bdf66f192296be1c372f0c458f6910992d01716320033b86a0a00e9
7
+ data.tar.gz: b1d9c1133f21dbe6370651c2d00e7e4173620ac13986ce24e8b91931f5f5793985d41cf2f8a7be626b73c948b6a473dec99f6e3560f52630cdad83c8ab3d5d72
data/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # active_mutator
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/active_mutator.svg)](https://rubygems.org/gems/active_mutator)
4
+
3
5
  Mutation testing for Ruby, built on [Prism](https://github.com/ruby/prism).
4
- Open source, RSpec-integrated, Rails-first.
6
+ Open source, RSpec-integrated, Rails-first. Available on
7
+ [RubyGems](https://rubygems.org/gems/active_mutator).
5
8
 
6
- active_mutator mutates your code one small change at a time (`>` `>=`,
7
- `&&` `||`, delete a statement, force a condition…), runs exactly the
8
- examples that cover the mutated line, and reports every mutant your suite
9
- fails to kill. A surviving mutant is a behavior change no test notices —
10
- a precise, machine-verified test gap.
9
+ active_mutator mutates your code one small change at a time (`>` becomes `>=`,
10
+ `&&` becomes `||`, a statement gets deleted, a condition gets forced, and so
11
+ on). It runs exactly the examples that cover the mutated line, and reports
12
+ every mutant your suite fails to kill. A surviving mutant is a behavior
13
+ change no test notices: a precise, machine-verified test gap.
11
14
 
12
15
  ## A surviving mutant, in one example
13
16
 
@@ -35,31 +38,31 @@ Surviving mutants:
35
38
  + total <= 100
36
39
  ```
37
40
 
38
- Nothing in the test suite calls `discount(100)` the one input where `<`
39
- and `<=` disagree. The tests pass, coverage is green, and the boundary is
40
- still unverified. That gap is invisible to coverage and obvious to
41
+ Nothing in the test suite calls `discount(100)`, the one input where `<`
42
+ and `<=` disagree. The tests pass, and coverage is green. But the boundary
43
+ is still unverified. That gap is invisible to coverage and obvious to
41
44
  mutation testing. Add `it { expect(calc.discount(100)).to eq(0) }` and the
42
45
  mutant is killed.
43
46
 
44
47
  ## What is mutation testing?
45
48
 
46
49
  Coverage answers "did a test run this line?" Mutation testing answers "would
47
- a test *notice* if this line were wrong?" a materially different, and
48
- usually more useful, question.
50
+ a test *notice* if this line were wrong?" That is a different, and usually
51
+ more useful, question.
49
52
 
50
53
  active_mutator applies one small, syntactically valid change to your code
51
54
  (a "mutant") and re-runs only the examples that cover it. If a test fails,
52
55
  the mutant is **killed**: your tests correctly reject that wrong behavior.
53
56
  If every covering test still passes, the mutant **survived**: something
54
- changed and nothing noticed. A survivor is not a hypothetical it's the
55
- exact line, the exact before/after diff, and proof that no assertion
57
+ changed and nothing noticed. A survivor is not a hypothetical. It is the
58
+ exact line, the exact before and after diff, and proof that no assertion
56
59
  depends on the difference.
57
60
 
58
61
  Mutation score is `(killed + timeout) / (killed + timeout + survived)`.
59
- 100% is usually not the right target some mutants are behaviorally
60
- *equivalent* to the original and can never be killed by any test which is
61
- why active_mutator has a committed acceptance ledger for closing survivors
62
- out with a stated reason instead of chasing an unreachable score.
62
+ 100% is usually not the right target. Some mutants are behaviorally
63
+ *equivalent* to the original and can never be killed by any test. That is
64
+ why active_mutator has a committed acceptance ledger. It lets you close
65
+ survivors out with a stated reason instead of chasing an unreachable score.
63
66
 
64
67
  Full primer, including the origin of the technique and further reading:
65
68
  **[`docs/guides/what-is-mutation-testing.md`](docs/guides/what-is-mutation-testing.md)**.
@@ -73,7 +76,7 @@ group :development, :test do
73
76
  end
74
77
  ```
75
78
 
76
- Requires Ruby 3.2, RSpec, and a green suite. Linux/macOS (MRI fork).
79
+ Requires Ruby 3.2 or later, RSpec, and a green suite. Linux/macOS (MRI fork).
77
80
 
78
81
  ## Quick start
79
82
 
@@ -82,9 +85,10 @@ bundle install
82
85
  bundle exec active_mutator app/models/calculator.rb
83
86
  ```
84
87
 
85
- First run performs an instrumented baseline of your suite to build the
86
- coverage map (cached in `.active_mutator/`, refreshed incrementally after
87
- that see [`docs/guides/how-it-works.md`](docs/guides/how-it-works.md)).
88
+ The first run performs an instrumented baseline of your suite to build the
89
+ coverage map. The map is cached in `.active_mutator/` and refreshed
90
+ incrementally after that (see
91
+ [`docs/guides/how-it-works.md`](docs/guides/how-it-works.md)).
88
92
  Then each mutant runs in its own fork against only its covering examples.
89
93
 
90
94
  ### Reading the output
@@ -115,36 +119,36 @@ Each character on the progress line is one mutant, printed as it finishes:
115
119
 
116
120
  | Char | Status | Meaning |
117
121
  |---|---|---|
118
- | `.` | `killed` | a covering test failed — good, the mutant is dead |
119
- | `S` | `survived` | every covering test passed a test gap |
120
- | `T` | `timeout` | ran past its time budget counted as detected (likely an infinite loop) |
122
+ | `.` | `killed` | a covering test failed. Good, the mutant is dead |
123
+ | `S` | `survived` | every covering test passed. This is a test gap |
124
+ | `T` | `timeout` | ran past its time budget. Counted as detected (likely an infinite loop) |
121
125
  | `E` | `error` | the worker crashed, or the mutated code raised outside a test assertion |
122
- | `U` | `uncovered` | no test executes the mutated line at all coverage debt, worse than a survivor |
123
- | `A` | `accepted` | matches a known-equivalent entry in the acceptance ledger excluded from the score |
126
+ | `U` | `uncovered` | no test executes the mutated line at all. This is coverage debt, worse than a survivor |
127
+ | `A` | `accepted` | matches a known-equivalent entry in the acceptance ledger. Excluded from the score |
124
128
 
125
129
  `invalid` mutants (edits that don't even re-parse as valid Ruby) are
126
130
  discarded before scheduling and reported as a count only. Exit code is `1`
127
- iff unaccepted survivors exist `0` otherwise, including when there are
128
- only `uncovered`/`accepted`/`error` results.
131
+ if unaccepted survivors exist, `0` otherwise, including when there are
132
+ only `uncovered`, `accepted`, or `error` results.
129
133
 
130
134
  ## How it works, compactly
131
135
 
132
- 1. **Subject discovery** a Prism visitor finds every method (`def`) in
136
+ 1. **Subject discovery**: a Prism visitor finds every method (`def`) in
133
137
  your target files.
134
- 2. **Source-span edits** each operator emits byte-range text edits
135
- against the original file, not a rewritten AST; every mutant is
138
+ 2. **Source-span edits**: each operator emits byte-range text edits
139
+ against the original file, not a rewritten AST. Every mutant is
136
140
  re-parsed with Prism and discarded (`invalid`) if the edit produced
137
141
  something that doesn't parse. No unparser is ever built or maintained.
138
- 3. **Coverage-mapped test selection** one instrumented baseline run maps
139
- every source line to the examples that cover it; incremental runs
142
+ 3. **Coverage-mapped test selection**: one instrumented baseline run maps
143
+ every source line to the examples that cover it. Incremental runs
140
144
  refresh only what changed instead of re-running the whole suite.
141
- 4. **Fork-per-mutant kill runs** the parent preloads your app and spec
142
- helper once; each mutant is inserted and exercised in its own fork
145
+ 4. **Fork-per-mutant kill runs**: the parent preloads your app and spec
146
+ helper once. Each mutant is inserted and exercised in its own fork
143
147
  against just its covering examples, so results can't bleed state
144
148
  between mutants.
145
149
 
146
150
  Full architecture, including the coverage-cache format, the fork pipeline,
147
- the serial lane for browser specs, timeout budgets, and every status
151
+ the serial lane for browser specs, timeout budgets, and every status, is in
148
152
  **[`docs/guides/how-it-works.md`](docs/guides/how-it-works.md)**.
149
153
 
150
154
  ## Usage
@@ -157,10 +161,11 @@ active_mutator --since origin/main # PR scope (CI)
157
161
  active_mutator --subject 'Foo::Bar#baz' # one method
158
162
  ```
159
163
 
160
- Statuses: `killed` (test failed good), `survived` (test gap), `timeout`
161
- (counts as detected), `uncovered` (no covering example coverage debt),
162
- `accepted` (known-equivalent, see ledger), `error`, `invalid` (discarded).
163
- Exit code 1 iff unaccepted survivors exist.
164
+ Statuses: `killed` (test failed, this is good), `survived` (test gap),
165
+ `timeout` (counts as detected), `uncovered` (no covering example, this is
166
+ coverage debt), `accepted` (known-equivalent, see ledger), `error`,
167
+ `invalid` (discarded).
168
+ Exit code 1 if unaccepted survivors exist.
164
169
 
165
170
  Score = (killed + timeout) / (killed + timeout + survived).
166
171
 
@@ -179,7 +184,7 @@ bundle exec active_mutator --changed --accept-survivors # records to ledger
179
184
  git add .active_mutator_accepted.json # committed state
180
185
  ```
181
186
 
182
- Acceptance takes effect on the NEXT run (the accepting run still exits 1).
187
+ Acceptance takes effect on the next run. The accepting run still exits 1.
183
188
  Agent workflow: see [`docs/skills/mutation-check.md`](docs/skills/mutation-check.md).
184
189
 
185
190
  ## CI recipe
@@ -193,9 +198,9 @@ Agent workflow: see [`docs/skills/mutation-check.md`](docs/skills/mutation-check
193
198
  | Flag | Default | Meaning |
194
199
  |---|---|---|
195
200
  | `--jobs N` | half the cores | fork-pool width |
196
- | `--changed` | | mutate uncommitted + untracked work |
197
- | `--since REF` | | mutate methods changed since REF |
198
- | `--subject NAME` | | one subject, e.g. `Foo#bar` |
201
+ | `--changed` | none | mutate uncommitted + untracked work |
202
+ | `--since REF` | none | mutate methods changed since REF |
203
+ | `--subject NAME` | none | one subject, e.g. `Foo#bar` |
199
204
  | `--format terminal\|json` | terminal | report format |
200
205
  | `--accept-survivors` | off | record survivors to the acceptance ledger |
201
206
  | `--force-baseline` | off | ignore cached coverage map |
@@ -203,9 +208,9 @@ Agent workflow: see [`docs/skills/mutation-check.md`](docs/skills/mutation-check
203
208
  | `--serial-pattern PAT` | `spec/system/`, `spec/features/` | covering-path prefixes forced serial |
204
209
  | `--browser-boot-seconds S` | 15 | serial-lane timeout bump |
205
210
  | `--timeout-factor F` / `--timeout-floor S` | 8 / 10 | mutation timeout budget |
206
- | `--require FILE` | | preload files (repeatable) |
211
+ | `--require FILE` | none | preload files (repeatable) |
207
212
 
208
- Every active_mutator process sets `ENV["ACTIVE_MUTATOR"] = "1"` use it to
213
+ Every active_mutator process sets `ENV["ACTIVE_MUTATOR"] = "1"`. Use it to
209
214
  guard SimpleCov or other tooling in your spec helper:
210
215
 
211
216
  ```ruby
@@ -214,30 +219,30 @@ SimpleCov.start "rails" unless ENV["ACTIVE_MUTATOR"]
214
219
 
215
220
  ## Known limits (v1.1)
216
221
 
217
- Method bodies only (no class-macro/constant mutation) · RSpec only ·
218
- heredoc strings not mutated · `class << self` bodies and nested defs
219
- skipped · incremental baseline can miss examples that only cover changed
220
- code after the change (nightly `--force-baseline` recovers).
222
+ Method bodies only (no class-macro/constant mutation). RSpec only.
223
+ Heredoc strings are not mutated. `class << self` bodies and nested defs
224
+ are skipped. The incremental baseline can miss examples that only cover
225
+ changed code after the change (nightly `--force-baseline` recovers).
221
226
 
222
227
  ## Guides
223
228
 
224
- - [What is mutation testing?](docs/guides/what-is-mutation-testing.md)
225
- the concepts: kill/survive, score, equivalent mutants, further reading.
226
- - [How it works](docs/guides/how-it-works.md) architecture: subject
229
+ - [What is mutation testing?](docs/guides/what-is-mutation-testing.md):
230
+ the concepts. Kill/survive, score, equivalent mutants, further reading.
231
+ - [How it works](docs/guides/how-it-works.md): architecture. Subject
227
232
  discovery, source-span edits, the coverage map, the fork pipeline, and
228
233
  honest limits.
229
- - [Operator reference](docs/guides/operators.md) every mutation
234
+ - [Operator reference](docs/guides/operators.md): every mutation
230
235
  active_mutator can generate, with before/after examples and what a
231
236
  survivor of each one means.
232
- - [Mutation-check skill](docs/skills/mutation-check.md) the agent-facing
233
- workflow: run, read survivors, strengthen tests, or accept with a reason.
237
+ - [Mutation-check skill](docs/skills/mutation-check.md): the agent-facing
238
+ workflow. Run, read survivors, strengthen tests, or accept with a reason.
234
239
 
235
240
  ## Contributing
236
241
 
237
242
  Issues and pull requests welcome. Run `bundle exec rspec` before sending a
238
- change; `bundle exec active_mutator --changed` on your own diff before
239
- sending a change that touches `lib/` is a good idea for the same reason
240
- you'd want it run on any other codebase.
243
+ change. Also run `bundle exec active_mutator --changed` on your own diff
244
+ before sending a change that touches `lib/`. This is a good idea for the
245
+ same reason you'd want it run on any other codebase.
241
246
 
242
247
  ## License
243
248
 
@@ -3,7 +3,7 @@ require "set"
3
3
 
4
4
  module ActiveMutator
5
5
  # Committed, repo-root ledger of accepted (equivalent) survivors.
6
- # Deliberately NOT inside .active_mutator/ that dir is gitignored and
6
+ # Deliberately NOT inside .active_mutator/: that dir is gitignored and
7
7
  # disposable, while acceptance decisions are durable team/CI state.
8
8
  class AcceptedLedger
9
9
  FILENAME = ".active_mutator_accepted.json"
@@ -1,6 +1,6 @@
1
1
  module ActiveMutator
2
2
  # flock-guarded write-to-temp + rename. Concurrent runs in one repo (an
3
- # agent plus a human the dev-loop case) must not corrupt cache or ledger.
3
+ # agent plus a human, the dev-loop case) must not corrupt cache or ledger.
4
4
  module AtomicFile
5
5
  def self.write(path, content)
6
6
  File.open("#{path}.lock", File::CREAT | File::RDWR, 0o644) do |lock|
@@ -42,13 +42,22 @@ module ActiveMutator
42
42
 
43
43
  private
44
44
 
45
- def run_baseline!
45
+ # The cache is disposable and must never be committed. Host projects
46
+ # rarely gitignore it themselves, so the directory ignores its own
47
+ # contents (the node_modules trick).
48
+ def prepare_cache_dir
46
49
  FileUtils.mkdir_p(@cache_dir)
50
+ ignore = File.join(@cache_dir, ".gitignore")
51
+ File.write(ignore, "*\n") unless File.exist?(ignore)
52
+ end
53
+
54
+ def run_baseline!
55
+ prepare_cache_dir
47
56
  env = baseline_env(@out_path)
48
- # out: :err the subprocess suite's progress output must not pollute
57
+ # out: :err: the subprocess suite's progress output must not pollute
49
58
  # our stdout (breaks `--format json` consumers).
50
59
  ok = system(env, "bundle", "exec", "rspec", chdir: @root, out: :err)
51
- raise BaselineFailed, "baseline suite failed fix the suite before mutating" unless ok
60
+ raise BaselineFailed, "baseline suite failed, fix the suite before mutating" unless ok
52
61
  raise BaselineFailed, "baseline produced no coverage output" unless File.exist?(@out_path)
53
62
  end
54
63
 
@@ -80,7 +89,7 @@ module ActiveMutator
80
89
  if targets.any?
81
90
  env = baseline_env(partial_out)
82
91
  ok = system(env, "bundle", "exec", "rspec", *targets, chdir: @root, out: :err)
83
- raise BaselineFailed, "partial baseline run failed fix the suite before mutating" unless ok
92
+ raise BaselineFailed, "partial baseline run failed, fix the suite before mutating" unless ok
84
93
  raise BaselineFailed, "partial baseline produced no output" unless File.exist?(partial_out)
85
94
  end
86
95
  merge_partial!(partial_out, delta)
@@ -1,5 +1,5 @@
1
1
  # Loaded standalone via RUBYOPT=-ractive_mutator/baseline_hooks in the host
2
- # project's suite before rspec boots, so Coverage instruments everything
2
+ # project's suite, before rspec boots, so Coverage instruments everything
3
3
  # the suite loads (including code loaded by spec_helper). Records per-example
4
4
  # coverage diffs and writes the inverted map to ACTIVE_MUTATOR_BASELINE_OUT.
5
5
  require "json"
@@ -53,7 +53,7 @@ if ENV["ACTIVE_MUTATOR_BASELINE_OUT"]
53
53
  root = ENV.fetch("ACTIVE_MUTATOR_ROOT")
54
54
  ActiveMutator::BaselineHooks::RECORDS[example.id] =
55
55
  ActiveMutator::BaselineHooks.diff_coverage(before, after, root)
56
- # NOT example.execution_result.run_time that is nil until after
56
+ # NOT example.execution_result.run_time: that is nil until after
57
57
  # around hooks complete.
58
58
  ActiveMutator::BaselineHooks::TIMES[example.id] = elapsed
59
59
  end
@@ -3,7 +3,7 @@ require "json"
3
3
  module ActiveMutator
4
4
  # Cache format v2: primary data is per-example `records`
5
5
  # ({example_id => [[abs_path, line], ...]}); the inverted index is derived
6
- # in memory at load. A missing/old version is simply stale the cache is
6
+ # in memory at load. A missing/old version is simply stale: the cache is
7
7
  # disposable, so there is no migration path, only regeneration.
8
8
  class CoverageMap
9
9
  def self.load(path) = new(JSON.parse(File.read(path)))
@@ -1,7 +1,7 @@
1
1
  module ActiveMutator
2
2
  # Line-number-independent identity for a mutant, used by the acceptance
3
3
  # ledger. `ordinal` disambiguates byte-identical mutants within one subject
4
- # (e.g. the two `>` in `a > 0 && b > 0`) by source order without it,
4
+ # (e.g. the two `>` in `a > 0 && b > 0`) by source order. Without it,
5
5
  # accepting one would silently accept both.
6
6
  Fingerprint = Data.define(:file, :subject, :description, :original_snippet, :ordinal) do
7
7
  def self.for_mutations(mutations, root:)
@@ -121,7 +121,7 @@ module ActiveMutator
121
121
 
122
122
  # A preloaded helper commonly starts SimpleCov. Its at_exit would fire in
123
123
  # THIS parent process at the end of the mutation run, clobbering the
124
- # project's real coverage data and minimum_coverage would exit(1) for a
124
+ # project's real coverage data, and minimum_coverage would exit(1) for a
125
125
  # bogus reason. Neutralize it.
126
126
  def disarm_simplecov
127
127
  SimpleCov.at_exit {} if defined?(SimpleCov)
@@ -136,7 +136,7 @@ module ActiveMutator
136
136
 
137
137
  def warn_stale(ledger, all_fingerprints)
138
138
  ledger.stale_entries(all_fingerprints).each do |entry|
139
- warn "active_mutator: stale accepted fingerprint (no matching mutant): #{entry.subject} #{entry.description}"
139
+ warn "active_mutator: stale accepted fingerprint (no matching mutant): #{entry.subject}, #{entry.description}"
140
140
  end
141
141
  end
142
142
  end
@@ -5,10 +5,14 @@ module ActiveMutator
5
5
  # Parent enforces per-item deadlines with SIGKILL (worker-side timeouts
6
6
  # cannot interrupt all infinite loops).
7
7
  class Scheduler
8
- def initialize(jobs:, worker: Worker.method(:run), on_result: nil)
8
+ OrphanedError = Class.new(Error)
9
+
10
+ def initialize(jobs:, worker: Worker.method(:run), on_result: nil,
11
+ orphaned: -> { Process.ppid == 1 })
9
12
  @jobs = jobs
10
13
  @worker = worker
11
14
  @on_result = on_result
15
+ @orphaned = orphaned
12
16
  end
13
17
 
14
18
  def run(items)
@@ -32,6 +36,7 @@ module ActiveMutator
32
36
  queue = items.dup
33
37
  results = []
34
38
  until queue.empty? && running.empty?
39
+ abort_if_orphaned!(running)
35
40
  spawn(queue.shift, running) while running.size < width && !queue.empty?
36
41
  reap(running, results)
37
42
  sleep 0.02 unless running.empty?
@@ -39,6 +44,22 @@ module ActiveMutator
39
44
  results
40
45
  end
41
46
 
47
+ # SIGKILL on the parent (or a closed terminal, or CI teardown) cannot be
48
+ # trapped, so a killed run would otherwise keep forking through the whole
49
+ # queue with nobody supervising it. Orphaned processes get reparented to
50
+ # init/launchd (ppid 1); when that happens, stop everything and bail.
51
+ def abort_if_orphaned!(running)
52
+ return unless @orphaned.call
53
+
54
+ running.each_key do |pid|
55
+ kill(pid)
56
+ rescue StandardError
57
+ nil
58
+ end
59
+ running.clear
60
+ raise OrphanedError, "parent process died; aborting mutation run"
61
+ end
62
+
42
63
  def spawn(item, running)
43
64
  reader, writer = IO.pipe
44
65
  pid = fork do
@@ -86,7 +107,7 @@ module ActiveMutator
86
107
  def kill(pid)
87
108
  Process.kill("KILL", -pid) # negative pid = whole process group
88
109
  rescue Errno::ESRCH, Errno::EPERM
89
- # Group not established yet (setpgid race) or already gone direct kill.
110
+ # Group not established yet (setpgid race) or already gone: direct kill.
90
111
  begin
91
112
  Process.kill("KILL", pid)
92
113
  rescue Errno::ESRCH
@@ -100,7 +121,7 @@ module ActiveMutator
100
121
  end
101
122
  end
102
123
 
103
- # Returns {sig => previous_handler} so #run can restore on exit
124
+ # Returns {sig => previous_handler} so #run can restore on exit,
104
125
  # otherwise our traps permanently replace the host's (e.g. RSpec's Ctrl-C).
105
126
  def install_signal_handlers(running)
106
127
  %w[INT TERM].to_h do |sig|
@@ -1,3 +1,3 @@
1
1
  module ActiveMutator
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -3,7 +3,7 @@ require "set"
3
3
 
4
4
  module ActiveMutator
5
5
  # Runs INSIDE a fork. Order is critical: RSpec's setup phase loads the spec
6
- # files, whose spec_helper/rails_helper loads the application only THEN
6
+ # files, whose spec_helper/rails_helper loads the application. Only THEN
7
7
  # can the mutation be inserted over the loaded original. Insert-first would
8
8
  # NameError on any project not preloaded in the parent (all non-Rails
9
9
  # projects), and loading app code after insertion would silently restore
@@ -47,7 +47,7 @@ module ActiveMutator
47
47
  @writer.flush if @writer.respond_to?(:flush)
48
48
  end
49
49
 
50
- # RSpec.world holds every group registered in the process including any
50
+ # RSpec.world holds every group registered in the process, including any
51
51
  # top-level groups evaluated while the PARENT preloaded the spec helper
52
52
  # (spec/support files with RSpec.describe at load time are common). Those
53
53
  # leak into the fork; running them would report their failures as false
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_mutator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel John
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-10 00:00:00.000000000 Z
11
+ date: 2026-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prism
@@ -52,10 +52,10 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.13'
55
- description: 'Mutation testing for Ruby and Rails: Prism-based source-span mutations
55
+ description: Mutation testing for Ruby and Rails. Uses Prism-based source-span mutations
56
56
  (no unparser), coverage-mapped test selection, and a fork-per-mutant kill pipeline.
57
- Scopes to changed methods for a fast dev loop, or a diff for CI, with an incremental
58
- coverage baseline and a committed acceptance ledger for equivalent mutants.'
57
+ Scopes to changed methods for a fast dev loop, or to a diff for CI. Includes an
58
+ incremental coverage baseline and a committed acceptance ledger for equivalent mutants.
59
59
  email:
60
60
  executables:
61
61
  - active_mutator