evilution 0.30.4 → 0.32.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +22 -0
  3. data/.rubocop_todo.yml +6 -0
  4. data/CHANGELOG.md +22 -0
  5. data/README.md +9 -7
  6. data/docs/integrations.md +126 -0
  7. data/docs/isolation.md +28 -0
  8. data/lib/evilution/cli/parser/options_builder.rb +6 -1
  9. data/lib/evilution/config/validators/integration.rb +5 -1
  10. data/lib/evilution/config.rb +14 -4
  11. data/lib/evilution/integration/loading/mutation_applier.rb +16 -8
  12. data/lib/evilution/integration/loading/source_evaluator.rb +4 -1
  13. data/lib/evilution/integration/minitest.rb +1 -1
  14. data/lib/evilution/integration/rspec/baseline_runner.rb +3 -1
  15. data/lib/evilution/integration/rspec/framework_loader.rb +5 -1
  16. data/lib/evilution/integration/rspec.rb +38 -1
  17. data/lib/evilution/integration/test_unit/dispatcher.rb +26 -0
  18. data/lib/evilution/integration/test_unit/framework_loader.rb +33 -0
  19. data/lib/evilution/integration/test_unit/result_builder.rb +53 -0
  20. data/lib/evilution/integration/test_unit/subject_class_registry.rb +26 -0
  21. data/lib/evilution/integration/test_unit/test_file_resolver.rb +48 -0
  22. data/lib/evilution/integration/test_unit.rb +124 -0
  23. data/lib/evilution/integration/test_unit_crash_detector.rb +61 -0
  24. data/lib/evilution/isolation/fork.rb +26 -1
  25. data/lib/evilution/isolation/in_process.rb +20 -3
  26. data/lib/evilution/mcp/info_tool.rb +2 -2
  27. data/lib/evilution/mcp/mutate_tool.rb +3 -2
  28. data/lib/evilution/runner/baseline_runner.rb +3 -1
  29. data/lib/evilution/runner/canary.rb +130 -0
  30. data/lib/evilution/runner.rb +24 -1
  31. data/lib/evilution/spec_ast_cache.rb +20 -3
  32. data/lib/evilution/spec_resolver.rb +16 -2
  33. data/lib/evilution/spec_selector.rb +14 -2
  34. data/lib/evilution/version.rb +1 -1
  35. data/lib/evilution.rb +39 -0
  36. data/script/run_self_baseline +2 -2
  37. metadata +11 -2
@@ -59,13 +59,30 @@ class Evilution::SpecAstCache
59
59
  end
60
60
 
61
61
  def parse(path)
62
- raise Evilution::ParseError.new("file not found: #{path}", file: path) unless File.exist?(path)
62
+ resolved = resolve_path(path)
63
+ raise Evilution::ParseError.new("file not found: #{path}", file: path) unless resolved
63
64
 
64
- source = read_source(path)
65
- result = parse_source(path, source)
65
+ source = read_source(resolved)
66
+ result = parse_source(resolved, source)
66
67
  collect_blocks(source, result, extract_comment_ranges(result))
67
68
  end
68
69
 
70
+ # Accept either a CWD-relative path (historical) or one resolvable against
71
+ # Evilution::PROJECT_ROOT — needed for isolators chdir'd into a per-mutation
72
+ # sandbox (EV-wqxu / GH #1278). The PROJECT_ROOT fallback is gated on the
73
+ # isolated-worker flag so unrelated callers that chdir intentionally (e.g.
74
+ # tests using a fixture project layout) do not accidentally resolve into
75
+ # the evilution dev tree.
76
+ def resolve_path(path)
77
+ return path if File.exist?(path)
78
+ return nil unless Evilution.in_isolated_worker?
79
+
80
+ expanded = File.expand_path(path, Evilution::PROJECT_ROOT)
81
+ return expanded if File.exist?(expanded)
82
+
83
+ nil
84
+ end
85
+
69
86
  def parse_source(path, source)
70
87
  result = Prism.parse(source)
71
88
  return result unless result.failure?
@@ -16,7 +16,7 @@ class Evilution::SpecResolver
16
16
  normalized = normalize_path(source_path)
17
17
  candidates = candidate_test_paths(normalized)
18
18
  candidates = filter_by_pattern(candidates, spec_pattern) if spec_pattern
19
- candidates.find { |path| File.exist?(path) }
19
+ candidates.find { |path| project_relative_exists?(path) }
20
20
  end
21
21
 
22
22
  def resolve_all(source_paths)
@@ -25,13 +25,27 @@ class Evilution::SpecResolver
25
25
 
26
26
  private
27
27
 
28
+ # Existence check that succeeds against the current CWD. When the caller
29
+ # is an isolated worker that chdir'd into a per-mutation sandbox (Evilution
30
+ # signals this via in_isolated_worker?), also try PROJECT_ROOT so the
31
+ # sandbox CWD does not break spec resolution (EV-wqxu / GH #1278).
32
+ def project_relative_exists?(path)
33
+ return true if File.exist?(path)
34
+ return false unless Evilution.in_isolated_worker?
35
+
36
+ File.exist?(File.expand_path(path, Evilution::PROJECT_ROOT))
37
+ end
38
+
28
39
  def filter_by_pattern(candidates, pattern)
29
40
  candidates.select { |path| File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
30
41
  end
31
42
 
32
43
  def normalize_path(path)
33
44
  path = path.delete_prefix("./")
34
- path = path.delete_prefix("#{Dir.pwd}/") if path.start_with?("/")
45
+ if path.start_with?("/")
46
+ path = path.delete_prefix("#{Dir.pwd}/")
47
+ path = path.delete_prefix("#{Evilution::PROJECT_ROOT}/") if Evilution.in_isolated_worker?
48
+ end
35
49
  path
36
50
  end
37
51
 
@@ -15,7 +15,7 @@ class Evilution::SpecSelector
15
15
 
16
16
  mapped = mapping_for(source_path)
17
17
  if mapped
18
- existing = mapped.select { |path| File.exist?(path) }
18
+ existing = mapped.select { |path| project_relative_exists?(path) }
19
19
  return existing unless existing.empty?
20
20
  end
21
21
 
@@ -33,7 +33,19 @@ class Evilution::SpecSelector
33
33
  return path if path.nil?
34
34
 
35
35
  normalized = path.to_s
36
- normalized = normalized.delete_prefix("#{Dir.pwd}/") if normalized.start_with?("/")
36
+ if normalized.start_with?("/")
37
+ normalized = normalized.delete_prefix("#{Dir.pwd}/")
38
+ normalized = normalized.delete_prefix("#{Evilution::PROJECT_ROOT}/") if Evilution.in_isolated_worker?
39
+ end
37
40
  normalized.delete_prefix("./")
38
41
  end
42
+
43
+ # Same semantics as Evilution::SpecResolver#project_relative_exists? — see
44
+ # that method for the EV-wqxu / GH #1278 rationale.
45
+ def project_relative_exists?(path)
46
+ return true if File.exist?(path)
47
+ return false unless Evilution.in_isolated_worker?
48
+
49
+ File.exist?(File.expand_path(path, Evilution::PROJECT_ROOT))
50
+ end
39
51
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.30.4"
4
+ VERSION = "0.32.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -129,6 +129,45 @@ require_relative "evilution/disable_comment"
129
129
  require_relative "evilution/runner"
130
130
 
131
131
  module Evilution
132
+ # Captured at load time, before any isolator can chdir into a per-mutation
133
+ # sandbox. Used as the anchor for resolving project-relative paths (spec
134
+ # files, source files for eval) from inside a chdir'd child so the CWD
135
+ # sandbox (EV-wqxu / GH #1278) cannot break spec resolution or eval __FILE__.
136
+ PROJECT_ROOT = Dir.pwd.freeze unless defined?(PROJECT_ROOT)
137
+
138
+ # Flag set by isolators (Evilution::Isolation::Fork in the forked child,
139
+ # Evilution::Isolation::InProcess around the test_command) so spec
140
+ # resolution and source eval anchor relative paths to PROJECT_ROOT instead
141
+ # of Dir.pwd. Without this gate, a caller that intentionally chdirs to a
142
+ # different project (e.g. a fixture layout in tests) would have its lookups
143
+ # inadvertently fall back to the evilution dev tree.
144
+ def self.in_isolated_worker!
145
+ @in_isolated_worker = true
146
+ end
147
+
148
+ def self.in_isolated_worker?
149
+ @in_isolated_worker == true
150
+ end
151
+
152
+ def self.with_isolated_worker
153
+ previous = @in_isolated_worker
154
+ @in_isolated_worker = true
155
+ yield
156
+ ensure
157
+ @in_isolated_worker = previous
158
+ end
159
+
160
+ # Base directory for resolving project-relative paths. An isolated worker
161
+ # has chdir'd into a per-mutation sandbox (EV-wqxu / GH #1278), so callers
162
+ # in that context must anchor against PROJECT_ROOT rather than Dir.pwd —
163
+ # otherwise spec files, source eval __FILE__, and $LOAD_PATH entries
164
+ # resolve into the sandbox and break the run. In any other context (normal
165
+ # use, tests that intentionally chdir into a fixture project layout, etc.)
166
+ # the caller's Dir.pwd remains the truth.
167
+ def self.project_base_dir
168
+ in_isolated_worker? ? PROJECT_ROOT : Dir.pwd
169
+ end
170
+
132
171
  class Error < StandardError
133
172
  attr_reader :file
134
173
 
@@ -38,7 +38,7 @@ dirs.each do |dir|
38
38
  end
39
39
 
40
40
  log = File.join(LOG_DIR, "#{dir}.self.log")
41
- cmd = [WRAPPER, "--strict", "--jobs=4", "--timeout=15", "--quiet-children", *files]
41
+ cmd = [WRAPPER, "--strict", "--jobs=4", "--timeout=15", "--quiet-children", "--isolation=fork", *files]
42
42
  puts "==> #{dir} (#{files.length} files)"
43
43
  pid = spawn(*cmd, out: log, err: %i[child out])
44
44
  Process.wait(pid)
@@ -53,7 +53,7 @@ toplevel_files = Dir.glob(File.join(ROOT, "lib", "evilution", "*.rb"))
53
53
  toplevel_files.reject! { |f| SKIP_FILES.include?(f) }
54
54
  unless toplevel_files.empty?
55
55
  log = File.join(LOG_DIR, "toplevel.self.log")
56
- cmd = [WRAPPER, "--strict", "--jobs=4", "--timeout=15", "--quiet-children", *toplevel_files]
56
+ cmd = [WRAPPER, "--strict", "--jobs=4", "--timeout=15", "--quiet-children", "--isolation=fork", *toplevel_files]
57
57
  puts "==> toplevel (#{toplevel_files.length} files)"
58
58
  pid = spawn(*cmd, out: log, err: %i[child out])
59
59
  Process.wait(pid)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evilution
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.4
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Kiselev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-16 00:00:00.000000000 Z
11
+ date: 2026-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -104,6 +104,7 @@ files:
104
104
  - comparison_results/operator_classification.md
105
105
  - comparison_results/operator_prioritization.md
106
106
  - docs/ast_pattern_syntax.md
107
+ - docs/integrations.md
107
108
  - docs/isolation.md
108
109
  - docs/mutation_density_benchmark.md
109
110
  - docs/versioning.md
@@ -240,6 +241,13 @@ files:
240
241
  - lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb
241
242
  - lib/evilution/integration/rspec/test_file_resolver.rb
242
243
  - lib/evilution/integration/rspec/unresolved_spec_warner.rb
244
+ - lib/evilution/integration/test_unit.rb
245
+ - lib/evilution/integration/test_unit/dispatcher.rb
246
+ - lib/evilution/integration/test_unit/framework_loader.rb
247
+ - lib/evilution/integration/test_unit/result_builder.rb
248
+ - lib/evilution/integration/test_unit/subject_class_registry.rb
249
+ - lib/evilution/integration/test_unit/test_file_resolver.rb
250
+ - lib/evilution/integration/test_unit_crash_detector.rb
243
251
  - lib/evilution/isolation.rb
244
252
  - lib/evilution/isolation/fork.rb
245
253
  - lib/evilution/isolation/in_process.rb
@@ -446,6 +454,7 @@ files:
446
454
  - lib/evilution/result/summary.rb
447
455
  - lib/evilution/runner.rb
448
456
  - lib/evilution/runner/baseline_runner.rb
457
+ - lib/evilution/runner/canary.rb
449
458
  - lib/evilution/runner/diagnostics.rb
450
459
  - lib/evilution/runner/isolation_resolver.rb
451
460
  - lib/evilution/runner/mutation_executor.rb