active_mutator 0.3.0 → 0.4.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 +4 -4
- data/README.md +14 -0
- data/lib/active_mutator/baseline.rb +44 -6
- data/lib/active_mutator/baseline_delta.rb +15 -6
- data/lib/active_mutator/baseline_hooks.rb +15 -6
- data/lib/active_mutator/cli.rb +19 -0
- data/lib/active_mutator/config.rb +2 -1
- data/lib/active_mutator/config_file.rb +7 -0
- data/lib/active_mutator/runner.rb +13 -7
- data/lib/active_mutator/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c97d4e4b28fb22f07d54f258e65162b4a4e9c731ab89caa851610e1ff3d1a83
|
|
4
|
+
data.tar.gz: dfd2fdddc29f209281e45001ae7a21bd6d75fce6833a5f4086f3592d36378843
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 983e510da62ef0a31abefd1342485e17a365013ce13eb16b9ad3a1b929ca5ff13c643c15ae9529c6b0def30215268b1a2c424d102489f4351d27b2c9cddd2ca1
|
|
7
|
+
data.tar.gz: 272643ac14e46cfba0e0ea95d4b9fe4cedc7ace3b7359eaabdf7bed60a8caf2451f84ba3986828f9528dfe402e9fe1fe4d9b20d7daa63f842ec728ebfb4ef2af
|
data/README.md
CHANGED
|
@@ -258,6 +258,7 @@ survivors show inline on the PR diff. Pairs with the CI recipe:
|
|
|
258
258
|
| `--force-baseline` | off | ignore cached coverage map |
|
|
259
259
|
| `--preload-helper FILE` / `--no-preload-helper` | auto-detect | parent spec-helper preload |
|
|
260
260
|
| `--serial-pattern PAT` | `spec/system/`, `spec/features/` | covering-path prefixes forced serial |
|
|
261
|
+
| `--spec-path DIR` | `spec/` | where spec files live, relative to the project root (repeatable; the first use replaces the default `spec/`), e.g. `--spec-path engines/billing/spec --spec-path spec` |
|
|
261
262
|
| `--browser-boot-seconds S` | 15 | serial-lane timeout bump |
|
|
262
263
|
| `--timeout-factor F` / `--timeout-floor S` | 8 / 10 | mutation timeout budget |
|
|
263
264
|
| `--[no-]adaptive-timeout` | on | scale timeout budgets from observed worker wall times (median utilization, grow-only, clamped 1x–4x; `--timeout-factor`/`--timeout-floor` set the starting budget) |
|
|
@@ -266,6 +267,14 @@ survivors show inline on the PR diff. Pairs with the CI recipe:
|
|
|
266
267
|
| `--[no-]class-level` | on | mutate class-level code (macros, constants, DSL/scope lambdas) via class-body subjects |
|
|
267
268
|
| `--fail-at SCORE` | none (strict) | exit 0 if score >= SCORE even with survivors (opt-in relaxation for gradual adoption; 0 = report-only) |
|
|
268
269
|
|
|
270
|
+
`--spec-path` tells active_mutator where spec files live (coverage
|
|
271
|
+
classification, digests, escalation); RSpec's own discovery is still the
|
|
272
|
+
project's job — a project with specs under `test/` also needs
|
|
273
|
+
`--default-path test` in its `.rspec`. The serial-lane defaults stay
|
|
274
|
+
`spec/system/` and `spec/features/` regardless of `--spec-path`; a
|
|
275
|
+
custom spec root with browser specs should set `--serial-pattern`
|
|
276
|
+
(e.g. `--serial-pattern test/system/`) itself.
|
|
277
|
+
|
|
269
278
|
`--debug-plan` prints the planned mutant list as one JSON document
|
|
270
279
|
(`{"planned": [...], "pre_resolved": {...}}`) and exits without running
|
|
271
280
|
anything. A coverage baseline is still built or loaded, since timeouts
|
|
@@ -285,6 +294,8 @@ flags override file values (`--require` and `--exclude` add to the file's
|
|
|
285
294
|
lists; the first `--serial-pattern` replaces them). Recognized keys:
|
|
286
295
|
`jobs`, `format`, `timeout_factor`, `timeout_floor`,
|
|
287
296
|
`browser_boot_seconds`, `fail_at`, `exclude`, `serial_patterns`,
|
|
297
|
+
`spec_paths` (where spec files live, relative to the project root;
|
|
298
|
+
replaces the default `spec`),
|
|
288
299
|
`requires`, `operators` (custom operator files, loaded before analysis; see
|
|
289
300
|
[Custom operators](docs/guides/custom-operators.md)),
|
|
290
301
|
`preload_helper` (a path, or `false` to skip preload),
|
|
@@ -301,6 +312,9 @@ exclude:
|
|
|
301
312
|
- lib/generated
|
|
302
313
|
serial_patterns:
|
|
303
314
|
- spec/system/
|
|
315
|
+
spec_paths:
|
|
316
|
+
- engines/billing/spec
|
|
317
|
+
- spec
|
|
304
318
|
fail_at: 90 # legacy suite: gate on score instead of zero-survivors
|
|
305
319
|
```
|
|
306
320
|
|
|
@@ -5,10 +5,11 @@ require "json"
|
|
|
5
5
|
module ActiveMutator
|
|
6
6
|
# Runs the host suite once, instrumented, in a subprocess. Produces and
|
|
7
7
|
# caches the CoverageMap. Invalidation is coarse: any digest change in
|
|
8
|
-
# {app,lib
|
|
8
|
+
# {app,lib}/**/*.rb or the configured spec paths triggers a full re-run.
|
|
9
9
|
class Baseline
|
|
10
|
-
def initialize(root:, cache_dir: File.join(root, ".active_mutator"))
|
|
10
|
+
def initialize(root:, spec_paths: ["spec"], cache_dir: File.join(root, ".active_mutator"))
|
|
11
11
|
@root = root
|
|
12
|
+
@spec_paths = spec_paths
|
|
12
13
|
@cache_dir = cache_dir
|
|
13
14
|
@out_path = File.join(cache_dir, "coverage.json")
|
|
14
15
|
end
|
|
@@ -19,13 +20,19 @@ module ActiveMutator
|
|
|
19
20
|
digests = current_digests
|
|
20
21
|
if !force && File.exist?(@out_path)
|
|
21
22
|
map = CoverageMap.load(@out_path)
|
|
22
|
-
|
|
23
|
+
# A spec_paths change silently degrades the delta classifier: files
|
|
24
|
+
# under a removed spec path just vanish from the digest scan, so
|
|
25
|
+
# BaselineDelta treats their stale example records as untouched
|
|
26
|
+
# source coverage instead of dropping them. Force a full rebuild
|
|
27
|
+
# whenever the configured spec_paths differ from what the cache was
|
|
28
|
+
# stamped with.
|
|
29
|
+
if stored_spec_paths(map) == @spec_paths && map.fresh?(digests)
|
|
23
30
|
@last_refresh = :cached
|
|
24
31
|
return map
|
|
25
32
|
end
|
|
26
|
-
if map.version == 2
|
|
33
|
+
if stored_spec_paths(map) == @spec_paths && map.version == 2
|
|
27
34
|
delta = BaselineDelta.compute(old_digests: stored_digests(map), new_digests: digests,
|
|
28
|
-
coverage_map: map, root: @root)
|
|
35
|
+
coverage_map: map, root: @root, spec_paths: @spec_paths)
|
|
29
36
|
unless delta.full?
|
|
30
37
|
run_partial!(delta)
|
|
31
38
|
stamp_digests(digests)
|
|
@@ -59,6 +66,25 @@ module ActiveMutator
|
|
|
59
66
|
ok = system(env, "bundle", "exec", "rspec", chdir: @root, out: :err)
|
|
60
67
|
raise BaselineFailed, "baseline suite failed, fix the suite before mutating" unless ok
|
|
61
68
|
raise BaselineFailed, "baseline produced no coverage output" unless File.exist?(@out_path)
|
|
69
|
+
|
|
70
|
+
verify_complete!(@out_path)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# An aborted subprocess can still exit 0 with a partial map (RSpec
|
|
74
|
+
# rescues Errno::EPIPE and runs after(:suite)); stamping that as fresh
|
|
75
|
+
# silently reports every mutant uncovered. Payloads without the count
|
|
76
|
+
# predate this check and are accepted as-is.
|
|
77
|
+
def verify_complete!(out_path)
|
|
78
|
+
payload = JSON.parse(File.read(out_path))
|
|
79
|
+
expected = payload["expected_examples"]
|
|
80
|
+
return unless expected
|
|
81
|
+
|
|
82
|
+
recorded = payload.fetch("records", {}).size
|
|
83
|
+
return if recorded >= expected
|
|
84
|
+
|
|
85
|
+
raise BaselineFailed,
|
|
86
|
+
"baseline aborted early: #{recorded} of #{expected} examples recorded — " \
|
|
87
|
+
"re-run without interrupting the suite"
|
|
62
88
|
end
|
|
63
89
|
|
|
64
90
|
def baseline_env(out_path)
|
|
@@ -66,6 +92,7 @@ module ActiveMutator
|
|
|
66
92
|
"ACTIVE_MUTATOR" => "1",
|
|
67
93
|
"ACTIVE_MUTATOR_ROOT" => @root,
|
|
68
94
|
"ACTIVE_MUTATOR_BASELINE_OUT" => out_path,
|
|
95
|
+
"ACTIVE_MUTATOR_SPEC_PATHS" => @spec_paths.join(":"),
|
|
69
96
|
# RUBYOPT, not `rspec --require`: project .rspec requires (spec_helper
|
|
70
97
|
# → app code) run before command-line requires, and Coverage misses
|
|
71
98
|
# everything loaded before Coverage.start. -r fires before rspec boots.
|
|
@@ -83,6 +110,12 @@ module ActiveMutator
|
|
|
83
110
|
JSON.parse(File.read(@out_path)).fetch("digests", {})
|
|
84
111
|
end
|
|
85
112
|
|
|
113
|
+
# A pre-0.4.0 cache predates spec_paths and has no key; treat that as the
|
|
114
|
+
# old implicit default so existing default-config caches stay valid.
|
|
115
|
+
def stored_spec_paths(map)
|
|
116
|
+
JSON.parse(File.read(@out_path)).fetch("spec_paths", ["spec"])
|
|
117
|
+
end
|
|
118
|
+
|
|
86
119
|
def run_partial!(delta)
|
|
87
120
|
targets = delta.rerun_spec_files + delta.rerun_example_ids
|
|
88
121
|
partial_out = File.join(@cache_dir, "partial.json")
|
|
@@ -91,6 +124,8 @@ module ActiveMutator
|
|
|
91
124
|
ok = system(env, "bundle", "exec", "rspec", *targets, chdir: @root, out: :err)
|
|
92
125
|
raise BaselineFailed, "partial baseline run failed, fix the suite before mutating" unless ok
|
|
93
126
|
raise BaselineFailed, "partial baseline produced no output" unless File.exist?(partial_out)
|
|
127
|
+
|
|
128
|
+
verify_complete!(partial_out)
|
|
94
129
|
end
|
|
95
130
|
merge_partial!(partial_out, delta)
|
|
96
131
|
ensure
|
|
@@ -122,11 +157,14 @@ module ActiveMutator
|
|
|
122
157
|
def stamp_digests(digests)
|
|
123
158
|
data = JSON.parse(File.read(@out_path))
|
|
124
159
|
data["digests"] = digests
|
|
160
|
+
data["spec_paths"] = @spec_paths
|
|
125
161
|
AtomicFile.write(@out_path, JSON.generate(data))
|
|
126
162
|
end
|
|
127
163
|
|
|
128
164
|
def current_digests
|
|
129
|
-
files = Dir[File.join(@root, "{app,lib
|
|
165
|
+
files = Dir[File.join(@root, "{app,lib}/**/*.rb")]
|
|
166
|
+
files += @spec_paths.flat_map { |sp| Dir[File.join(@root, sp, "**", "*.rb")] }
|
|
167
|
+
files = files.uniq.sort
|
|
130
168
|
files += [File.join(@root, "Gemfile.lock"), File.join(@root, ".rspec")].select { |f| File.exist?(f) }
|
|
131
169
|
files.to_h { |f| [f.delete_prefix("#{@root}/"), Digest::SHA256.file(f).hexdigest] }
|
|
132
170
|
end
|
|
@@ -15,10 +15,10 @@ module ActiveMutator
|
|
|
15
15
|
# files, a full re-run is cheaper and simpler than a giant partial one.
|
|
16
16
|
REFERENCE_FULL_RATIO = 0.5
|
|
17
17
|
|
|
18
|
-
def self.compute(old_digests:, new_digests:, coverage_map:, root:)
|
|
18
|
+
def self.compute(old_digests:, new_digests:, coverage_map:, root:, spec_paths: ["spec"])
|
|
19
19
|
changed = (old_digests.keys | new_digests.keys)
|
|
20
20
|
.reject { |k| old_digests[k] == new_digests[k] }
|
|
21
|
-
return FULL if changed.any? { |k| full_trigger?(k) }
|
|
21
|
+
return FULL if changed.any? { |k| full_trigger?(k, spec_paths: spec_paths) }
|
|
22
22
|
|
|
23
23
|
rerun_spec_files = []
|
|
24
24
|
rerun_example_ids = []
|
|
@@ -35,7 +35,7 @@ module ActiveMutator
|
|
|
35
35
|
changed.each do |rel|
|
|
36
36
|
added = !old_digests.key?(rel)
|
|
37
37
|
deleted = !new_digests.key?(rel)
|
|
38
|
-
if rel.start_with?("
|
|
38
|
+
if spec_paths.any? { |sp| rel.start_with?("#{sp}/") }
|
|
39
39
|
owned = coverage_map.examples_for_spec_file(rel)
|
|
40
40
|
if deleted
|
|
41
41
|
# A deleted spec file with no records is support-like: other spec
|
|
@@ -54,7 +54,7 @@ module ActiveMutator
|
|
|
54
54
|
rerun_example_ids.concat(coverage_map.examples_covering_file(abs))
|
|
55
55
|
end
|
|
56
56
|
unless deleted
|
|
57
|
-
spec_contents ||=
|
|
57
|
+
spec_contents ||= spec_file_contents(root: root, spec_paths: spec_paths)
|
|
58
58
|
candidates = newly_covering_candidates(root: root, rel: rel, coverage_map: coverage_map,
|
|
59
59
|
spec_contents: spec_contents)
|
|
60
60
|
return FULL if candidates == :full
|
|
@@ -71,8 +71,17 @@ module ActiveMutator
|
|
|
71
71
|
drop_source_files: drop_source_files.uniq.sort)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def self.full_trigger?(rel)
|
|
75
|
-
rel.start_with?("
|
|
74
|
+
def self.full_trigger?(rel, spec_paths: ["spec"])
|
|
75
|
+
spec_paths.any? { |sp| rel.start_with?("#{sp}/support/") } || !rel.end_with?(".rb")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# All *_spec.rb files under the configured spec paths, absolute path =>
|
|
79
|
+
# file content. Shared by the delta scan and Runner's phase-2 escalation
|
|
80
|
+
# so "where do specs live" is answered in exactly one place (#35).
|
|
81
|
+
def self.spec_file_contents(root:, spec_paths:)
|
|
82
|
+
spec_paths
|
|
83
|
+
.flat_map { |sp| Dir[File.join(root, sp, "**", "*_spec.rb")] }
|
|
84
|
+
.uniq.to_h { |f| [f, File.read(f)] }
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
# #11: an unchanged spec file can START covering a changed source file
|
|
@@ -9,7 +9,8 @@ module ActiveMutator
|
|
|
9
9
|
RECORDS = {}
|
|
10
10
|
TIMES = {}
|
|
11
11
|
|
|
12
|
-
def self.diff_coverage(before, after, root)
|
|
12
|
+
def self.diff_coverage(before, after, root, spec_paths: ["spec"])
|
|
13
|
+
prefixes = spec_paths.map { |sp| "/#{sp}/" }
|
|
13
14
|
hits = []
|
|
14
15
|
after.each do |path, data|
|
|
15
16
|
next unless path.start_with?(root)
|
|
@@ -19,7 +20,7 @@ module ActiveMutator
|
|
|
19
20
|
# spec/fixtures/ tree), which would otherwise falsely exclude every
|
|
20
21
|
# file under it.
|
|
21
22
|
relative = path.delete_prefix(root)
|
|
22
|
-
next if relative.start_with?(
|
|
23
|
+
next if prefixes.any? { |p| relative.start_with?(p) }
|
|
23
24
|
|
|
24
25
|
before_lines = before.dig(path, :lines)
|
|
25
26
|
data[:lines].each_with_index do |count, idx|
|
|
@@ -32,8 +33,10 @@ module ActiveMutator
|
|
|
32
33
|
hits
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
def self.build_payload(records, times)
|
|
36
|
-
{ "version" => 2, "records" => records, "times" => times }
|
|
36
|
+
def self.build_payload(records, times, expected_examples: nil)
|
|
37
|
+
payload = { "version" => 2, "records" => records, "times" => times }
|
|
38
|
+
payload["expected_examples"] = expected_examples if expected_examples
|
|
39
|
+
payload
|
|
37
40
|
end
|
|
38
41
|
end
|
|
39
42
|
end
|
|
@@ -51,16 +54,22 @@ if ENV["ACTIVE_MUTATOR_BASELINE_OUT"]
|
|
|
51
54
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
52
55
|
after = Coverage.peek_result
|
|
53
56
|
root = ENV.fetch("ACTIVE_MUTATOR_ROOT")
|
|
57
|
+
spec_paths = ENV.fetch("ACTIVE_MUTATOR_SPEC_PATHS", "spec").split(":")
|
|
54
58
|
ActiveMutator::BaselineHooks::RECORDS[example.id] =
|
|
55
|
-
ActiveMutator::BaselineHooks.diff_coverage(before, after, root)
|
|
59
|
+
ActiveMutator::BaselineHooks.diff_coverage(before, after, root, spec_paths: spec_paths)
|
|
56
60
|
# NOT example.execution_result.run_time: that is nil until after
|
|
57
61
|
# around hooks complete.
|
|
58
62
|
ActiveMutator::BaselineHooks::TIMES[example.id] = elapsed
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
config.after(:suite) do
|
|
66
|
+
# The expected count lets the parent detect an aborted run: RSpec
|
|
67
|
+
# swallows some mid-suite failures (e.g. Errno::EPIPE on a closed
|
|
68
|
+
# stdout) yet still runs after(:suite) and exits 0, which would stamp
|
|
69
|
+
# a partial coverage map as a fresh, trusted baseline.
|
|
62
70
|
payload = ActiveMutator::BaselineHooks.build_payload(
|
|
63
|
-
ActiveMutator::BaselineHooks::RECORDS, ActiveMutator::BaselineHooks::TIMES
|
|
71
|
+
ActiveMutator::BaselineHooks::RECORDS, ActiveMutator::BaselineHooks::TIMES,
|
|
72
|
+
expected_examples: RSpec.world.example_count
|
|
64
73
|
)
|
|
65
74
|
File.write(ENV.fetch("ACTIVE_MUTATOR_BASELINE_OUT"), JSON.generate(payload))
|
|
66
75
|
end
|
data/lib/active_mutator/cli.rb
CHANGED
|
@@ -3,6 +3,13 @@ require "optparse"
|
|
|
3
3
|
module ActiveMutator
|
|
4
4
|
module CLI
|
|
5
5
|
def self.run(argv)
|
|
6
|
+
# Handled before OptionParser: its built-in --version handler prints
|
|
7
|
+
# "version unknown" and exits the process.
|
|
8
|
+
if argv.include?("--version")
|
|
9
|
+
puts "active_mutator #{VERSION}"
|
|
10
|
+
return 0
|
|
11
|
+
end
|
|
12
|
+
|
|
6
13
|
Runner.new(parse(argv)).call
|
|
7
14
|
rescue OptionParser::ParseError, Error => e
|
|
8
15
|
warn "active_mutator: #{e.message}"
|
|
@@ -22,6 +29,7 @@ module ActiveMutator
|
|
|
22
29
|
# boot cost (RSpec setup + spec file loading).
|
|
23
30
|
requires: [], timeout_factor: 8.0, timeout_floor: 10.0, force_baseline: false,
|
|
24
31
|
preload_helper: nil, serial_patterns: ["spec/system/", "spec/features/"],
|
|
32
|
+
spec_paths: ["spec"],
|
|
25
33
|
browser_boot_seconds: 15.0, accept_survivors: false, exclude: [],
|
|
26
34
|
max_mutants: nil, debug_plan: false, fail_at: nil, adaptive_timeout: true,
|
|
27
35
|
operators: [], class_level: true, class_level_closure_cap: 10
|
|
@@ -47,6 +55,11 @@ module ActiveMutator
|
|
|
47
55
|
options[:serial_patterns_replaced] = true
|
|
48
56
|
options[:serial_patterns] << v
|
|
49
57
|
end
|
|
58
|
+
o.on("--spec-path DIR", "Directory holding spec files, relative to root (repeatable; replaces the default spec/ on first use)") do |v|
|
|
59
|
+
options[:spec_paths] = [] unless options[:spec_paths_replaced]
|
|
60
|
+
options[:spec_paths_replaced] = true
|
|
61
|
+
options[:spec_paths] << v
|
|
62
|
+
end
|
|
50
63
|
o.on("--browser-boot-seconds S", Float, "Extra timeout budget for serial-lane mutants") { |v| options[:browser_boot_seconds] = v }
|
|
51
64
|
o.on("--[no-]adaptive-timeout", "Scale timeout budgets from observed worker wall times (default: on)") { |v| options[:adaptive_timeout] = v }
|
|
52
65
|
o.on("--accept-survivors", "Record surviving mutants into the acceptance ledger") { options[:accept_survivors] = true }
|
|
@@ -59,6 +72,12 @@ module ActiveMutator
|
|
|
59
72
|
end
|
|
60
73
|
end.parse(argv)
|
|
61
74
|
options.delete(:serial_patterns_replaced)
|
|
75
|
+
options.delete(:spec_paths_replaced)
|
|
76
|
+
options[:spec_paths] = options[:spec_paths].map { |sp| sp.sub(%r{/+\z}, "") }
|
|
77
|
+
raise OptionParser::InvalidArgument, "--spec-path list must not be empty" if options[:spec_paths].empty?
|
|
78
|
+
if options[:spec_paths].any? { |sp| sp.strip.empty? }
|
|
79
|
+
raise OptionParser::InvalidArgument, "--spec-path entries must not be blank"
|
|
80
|
+
end
|
|
62
81
|
|
|
63
82
|
Config.new(paths: paths, root: Dir.pwd, **options)
|
|
64
83
|
end
|
|
@@ -3,7 +3,8 @@ require "etc"
|
|
|
3
3
|
module ActiveMutator
|
|
4
4
|
Config = Data.define(:paths, :since, :subject_filter, :jobs, :format, :requires,
|
|
5
5
|
:timeout_factor, :timeout_floor, :force_baseline, :root,
|
|
6
|
-
:preload_helper, :serial_patterns, :
|
|
6
|
+
:preload_helper, :serial_patterns, :spec_paths,
|
|
7
|
+
:browser_boot_seconds,
|
|
7
8
|
:accept_survivors, :exclude, :max_mutants, :debug_plan,
|
|
8
9
|
:fail_at, :adaptive_timeout, :operators,
|
|
9
10
|
:class_level, :class_level_closure_cap)
|
|
@@ -19,6 +19,7 @@ module ActiveMutator
|
|
|
19
19
|
"fail_at" => :score,
|
|
20
20
|
"exclude" => :string_list,
|
|
21
21
|
"serial_patterns" => :string_list,
|
|
22
|
+
"spec_paths" => :nonempty_string_list,
|
|
22
23
|
"requires" => :string_list,
|
|
23
24
|
"operators" => :string_list,
|
|
24
25
|
"preload_helper" => :preload_helper,
|
|
@@ -75,6 +76,12 @@ module ActiveMutator
|
|
|
75
76
|
raise Error, "#{FILENAME}: #{key} must be a list of strings"
|
|
76
77
|
end
|
|
77
78
|
value
|
|
79
|
+
when :nonempty_string_list
|
|
80
|
+
unless value.is_a?(Array) && value.all?(String)
|
|
81
|
+
raise Error, "#{FILENAME}: #{key} must be a list of strings"
|
|
82
|
+
end
|
|
83
|
+
raise Error, "#{FILENAME}: #{key} must not be empty" if value.empty?
|
|
84
|
+
value
|
|
78
85
|
when :boolean
|
|
79
86
|
unless [true, false].include?(value)
|
|
80
87
|
raise Error, "#{FILENAME}: #{key} must be true or false"
|
|
@@ -13,7 +13,8 @@ module ActiveMutator
|
|
|
13
13
|
ClosureReload.cap = @config.class_level_closure_cap
|
|
14
14
|
preload!
|
|
15
15
|
preload_spec_helper!
|
|
16
|
-
map = Baseline.new(root: @config.root
|
|
16
|
+
map = Baseline.new(root: @config.root, spec_paths: @config.spec_paths)
|
|
17
|
+
.coverage_map(force: @config.force_baseline)
|
|
17
18
|
@reporter.coverage_map = map if @reporter.respond_to?(:coverage_map=)
|
|
18
19
|
subjects = discover_subjects
|
|
19
20
|
analyses = subjects.map { |s| Engine.new.analyze(s) }
|
|
@@ -86,7 +87,7 @@ module ActiveMutator
|
|
|
86
87
|
# its mutant is a known equivalent.)
|
|
87
88
|
return results if candidates.empty?
|
|
88
89
|
|
|
89
|
-
spec_contents =
|
|
90
|
+
spec_contents = BaselineDelta.spec_file_contents(root: @config.root, spec_paths: @config.spec_paths)
|
|
90
91
|
patterns = {} # subject file => constant-reference pattern (parsed once per file)
|
|
91
92
|
items = {}
|
|
92
93
|
candidates.each do |r|
|
|
@@ -200,14 +201,15 @@ module ActiveMutator
|
|
|
200
201
|
def examples_for_mutation(mutation, map)
|
|
201
202
|
return map.examples_for(mutation.subject.file, coverage_lines(mutation)) unless mutation.subject.class_body?
|
|
202
203
|
|
|
203
|
-
(
|
|
204
|
-
|
|
204
|
+
convention_examples = convention_spec_rels(mutation.subject.file)
|
|
205
|
+
.flat_map { |rel| map.examples_for_spec_file(rel) }
|
|
206
|
+
(map.examples_covering_file(mutation.subject.file) | convention_examples).sort
|
|
205
207
|
end
|
|
206
208
|
|
|
207
|
-
def
|
|
209
|
+
def convention_spec_rels(file)
|
|
208
210
|
rel = file.delete_prefix(@config.root.chomp("/") + "/").delete_suffix(".rb")
|
|
209
211
|
rest = rel.sub(%r{\A[^/]+/}, "")
|
|
210
|
-
"
|
|
212
|
+
@config.spec_paths.map { |sp| "#{sp}/#{rest}_spec.rb" }
|
|
211
213
|
end
|
|
212
214
|
|
|
213
215
|
def build_reporter
|
|
@@ -293,7 +295,11 @@ module ActiveMutator
|
|
|
293
295
|
helper = if @config.preload_helper
|
|
294
296
|
File.expand_path(@config.preload_helper, @config.root)
|
|
295
297
|
else
|
|
296
|
-
|
|
298
|
+
# Precedence: within each spec path rails_helper wins over
|
|
299
|
+
# spec_helper; earlier spec paths win over later ones — same
|
|
300
|
+
# as today for the default ["spec"].
|
|
301
|
+
@config.spec_paths
|
|
302
|
+
.flat_map { |sp| ["#{sp}/rails_helper.rb", "#{sp}/spec_helper.rb"] }
|
|
297
303
|
.map { |p| File.join(@config.root, p) }
|
|
298
304
|
.find { |p| File.exist?(p) }
|
|
299
305
|
end
|
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.
|
|
4
|
+
version: 0.4.0
|
|
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-
|
|
11
|
+
date: 2026-08-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|