evilution 0.27.0 → 0.29.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +65 -0
  3. data/.rubocop_todo.yml +0 -1
  4. data/CHANGELOG.md +39 -0
  5. data/README.md +19 -0
  6. data/lib/evilution/ast/constant_names.rb +28 -11
  7. data/lib/evilution/ast/pattern/parser.rb +29 -17
  8. data/lib/evilution/baseline.rb +5 -4
  9. data/lib/evilution/cli/commands/session_diff.rb +6 -4
  10. data/lib/evilution/cli/commands/subjects.rb +6 -3
  11. data/lib/evilution/cli/commands/util_mutation.rb +24 -19
  12. data/lib/evilution/cli/parser/command_extractor.rb +9 -11
  13. data/lib/evilution/cli/parser/file_args.rb +3 -1
  14. data/lib/evilution/cli/parser/options_builder.rb +36 -1
  15. data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
  16. data/lib/evilution/cli/parser.rb +18 -20
  17. data/lib/evilution/cli/printers/environment.rb +19 -19
  18. data/lib/evilution/cli/printers/session_diff.rb +8 -8
  19. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  20. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  21. data/lib/evilution/compare/diff_extractor.rb +6 -0
  22. data/lib/evilution/compare/fingerprint.rb +15 -72
  23. data/lib/evilution/compare/line_normalizer.rb +72 -0
  24. data/lib/evilution/compare/normalizer.rb +27 -9
  25. data/lib/evilution/config/validators/profile.rb +11 -0
  26. data/lib/evilution/config.rb +49 -32
  27. data/lib/evilution/disable_comment.rb +21 -12
  28. data/lib/evilution/integration/crash_detector.rb +2 -2
  29. data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
  30. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  31. data/lib/evilution/integration/minitest.rb +25 -16
  32. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  33. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +11 -3
  34. data/lib/evilution/integration/rspec.rb +4 -0
  35. data/lib/evilution/isolation/fork.rb +43 -28
  36. data/lib/evilution/isolation/in_process.rb +10 -6
  37. data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
  38. data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
  39. data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
  40. data/lib/evilution/mcp/info_tool.rb +7 -3
  41. data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
  42. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  43. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
  44. data/lib/evilution/mcp/mutate_tool.rb +27 -14
  45. data/lib/evilution/mcp/session_tool.rb +27 -20
  46. data/lib/evilution/mutation.rb +60 -42
  47. data/lib/evilution/mutator/base.rb +23 -21
  48. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
  49. data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
  50. data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
  51. data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
  52. data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
  53. data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
  54. data/lib/evilution/mutator/operator/case_when.rb +7 -5
  55. data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
  56. data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
  57. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
  58. data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
  59. data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
  60. data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
  61. data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
  62. data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
  63. data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
  64. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  65. data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
  66. data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
  67. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
  68. data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
  69. data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
  70. data/lib/evilution/mutator/registry.rb +20 -0
  71. data/lib/evilution/parallel/work_queue/channel/frame.rb +5 -1
  72. data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
  73. data/lib/evilution/parallel/work_queue/worker/loop.rb +1 -1
  74. data/lib/evilution/parallel/work_queue/worker.rb +10 -7
  75. data/lib/evilution/parallel/work_queue.rb +35 -18
  76. data/lib/evilution/process_cleanup.rb +19 -0
  77. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
  78. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
  79. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  80. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  81. data/lib/evilution/reporter/html/escape.rb +1 -1
  82. data/lib/evilution/reporter/html/section.rb +1 -1
  83. data/lib/evilution/reporter/html/sections.rb +4 -2
  84. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  85. data/lib/evilution/reporter/html.rb +8 -3
  86. data/lib/evilution/reporter/json.rb +52 -18
  87. data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
  88. data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
  89. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  90. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  91. data/lib/evilution/reporter/suggestion/templates/minitest.rb +361 -649
  92. data/lib/evilution/reporter/suggestion/templates/rspec.rb +362 -603
  93. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  94. data/lib/evilution/result/error_info.rb +20 -0
  95. data/lib/evilution/result/memory_stats.rb +20 -0
  96. data/lib/evilution/result/mutation_result.rb +30 -14
  97. data/lib/evilution/runner/baseline_runner.rb +16 -10
  98. data/lib/evilution/runner/diagnostics.rb +14 -11
  99. data/lib/evilution/runner/isolation_resolver.rb +12 -11
  100. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +1 -3
  101. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +1 -2
  102. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +3 -10
  103. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +3 -10
  104. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  105. data/lib/evilution/runner/mutation_executor/result_cache.rb +4 -4
  106. data/lib/evilution/runner/mutation_executor/result_notifier.rb +1 -3
  107. data/lib/evilution/runner/mutation_executor/result_packer.rb +11 -9
  108. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +33 -13
  109. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +2 -4
  110. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  111. data/lib/evilution/runner/mutation_executor.rb +14 -20
  112. data/lib/evilution/runner/mutation_planner.rb +38 -19
  113. data/lib/evilution/runner/report_publisher.rb +1 -2
  114. data/lib/evilution/runner/subject_pipeline.rb +22 -13
  115. data/lib/evilution/runner.rb +36 -34
  116. data/lib/evilution/session/diff.rb +15 -6
  117. data/lib/evilution/spec_ast_cache.rb +26 -12
  118. data/lib/evilution/version.rb +1 -1
  119. data/lib/evilution.rb +1 -0
  120. data/script/memory_check +14 -6
  121. data/scripts/benchmark_density +10 -9
  122. data/scripts/compare_mutations +38 -21
  123. data/scripts/mutant_json_adapter +7 -4
  124. metadata +15 -3
  125. data/lib/evilution/reporter/html/namespace.rb +0 -11
@@ -1,15 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../runner"
3
4
  require_relative "../disable_comment"
4
5
  require_relative "../ast/sorbet_sig_detector"
5
6
  require_relative "../ast/pattern/filter"
6
7
  require_relative "../equivalent/detector"
7
8
 
8
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
9
-
10
9
  class Evilution::Runner::MutationPlanner
11
10
  Plan = Struct.new(:enabled, :equivalent, :skipped_count, :disabled_mutations, keyword_init: true)
12
11
 
12
+ GenerationResult = Data.define(:mutations, :skipped)
13
+ DisabledFilterResult = Data.define(:enabled, :disabled)
14
+ SigFilterResult = Data.define(:enabled, :skipped)
15
+ EquivalentFilterResult = Data.define(:equivalent, :enabled)
16
+ private_constant :GenerationResult, :DisabledFilterResult, :SigFilterResult, :EquivalentFilterResult
17
+
13
18
  def initialize(config, registry:, disable_detector: Evilution::DisableComment.new,
14
19
  sig_detector: Evilution::AST::SorbetSigDetector.new)
15
20
  @config = config
@@ -21,26 +26,39 @@ class Evilution::Runner::MutationPlanner
21
26
  end
22
27
 
23
28
  def call(subjects)
24
- mutations, generation_skipped = generate(subjects)
25
- mutations, disabled = filter_disabled(mutations)
26
- disabled.each(&:strip_sources!) if config.show_disabled?
27
- disabled_mutations = config.show_disabled? ? disabled : []
29
+ generation = generate(subjects)
30
+ disabled_filter = filter_disabled(generation.mutations)
31
+ disabled_mutations = compute_disabled_mutations(disabled_filter)
32
+ sig_filter = filter_sig_blocks(disabled_filter.enabled)
33
+ equivalent_filter = filter_equivalent(sig_filter.enabled)
28
34
 
29
- mutations, sig_skipped = filter_sig_blocks(mutations)
30
- equivalent, enabled = filter_equivalent(mutations)
31
-
32
- Plan.new(
33
- enabled: enabled,
34
- equivalent: equivalent,
35
- skipped_count: generation_skipped + disabled.length + sig_skipped,
36
- disabled_mutations: disabled_mutations
37
- )
35
+ build_plan(equivalent_filter, disabled_mutations, total_skipped(generation, disabled_filter, sig_filter))
38
36
  end
39
37
 
40
38
  private
41
39
 
42
40
  attr_reader :config, :registry
43
41
 
42
+ def compute_disabled_mutations(disabled_filter)
43
+ return [] unless config.show_disabled?
44
+
45
+ disabled_filter.disabled.each(&:strip_sources!)
46
+ disabled_filter.disabled
47
+ end
48
+
49
+ def total_skipped(generation, disabled_filter, sig_filter)
50
+ generation.skipped + disabled_filter.disabled.length + sig_filter.skipped
51
+ end
52
+
53
+ def build_plan(equivalent_filter, disabled_mutations, skipped_count)
54
+ Plan.new(
55
+ enabled: equivalent_filter.enabled,
56
+ equivalent: equivalent_filter.equivalent,
57
+ skipped_count: skipped_count,
58
+ disabled_mutations: disabled_mutations
59
+ )
60
+ end
61
+
44
62
  def generate(subjects)
45
63
  filter = build_ignore_filter
46
64
  operator_options = build_operator_options
@@ -48,7 +66,7 @@ class Evilution::Runner::MutationPlanner
48
66
  registry.mutations_for(subject, filter: filter, operator_options: operator_options)
49
67
  end
50
68
  skipped = filter ? filter.skipped_count : 0
51
- [mutations, skipped]
69
+ GenerationResult.new(mutations: mutations, skipped: skipped)
52
70
  end
53
71
 
54
72
  def build_operator_options
@@ -74,7 +92,7 @@ class Evilution::Runner::MutationPlanner
74
92
  end
75
93
  end
76
94
 
77
- [enabled, disabled]
95
+ DisabledFilterResult.new(enabled: enabled, disabled: disabled)
78
96
  end
79
97
 
80
98
  def mutation_disabled?(mutation)
@@ -103,7 +121,7 @@ class Evilution::Runner::MutationPlanner
103
121
  end
104
122
  end
105
123
 
106
- [enabled, skipped]
124
+ SigFilterResult.new(enabled: enabled, skipped: skipped)
107
125
  end
108
126
 
109
127
  def mutation_in_sig_block?(mutation)
@@ -121,6 +139,7 @@ class Evilution::Runner::MutationPlanner
121
139
  end
122
140
 
123
141
  def filter_equivalent(mutations)
124
- Evilution::Equivalent::Detector.new.call(mutations)
142
+ equivalent, enabled = Evilution::Equivalent::Detector.new.call(mutations)
143
+ EquivalentFilterResult.new(equivalent: equivalent, enabled: enabled)
125
144
  end
126
145
  end
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../runner"
3
4
  require_relative "../reporter/json"
4
5
  require_relative "../reporter/cli"
5
6
  require_relative "../reporter/html"
6
7
  require_relative "../session/store"
7
8
 
8
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
9
-
10
9
  class Evilution::Runner::ReportPublisher
11
10
  def initialize(config)
12
11
  @config = config
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../runner"
3
4
  require_relative "../ast/inheritance_scanner"
4
5
  require_relative "../git/changed_files"
5
6
 
6
- class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
7
-
8
7
  class Evilution::Runner::SubjectPipeline
9
8
  def initialize(config, parser:)
10
9
  @config = config
@@ -105,17 +104,27 @@ class Evilution::Runner::SubjectPipeline
105
104
 
106
105
  def target_matcher
107
106
  target = config.target
108
- if target.end_with?("*")
109
- prefix = target.chomp("*")
110
- ->(s) { s.name.split(/[#.]/).first.start_with?(prefix) }
111
- elsif target.end_with?("#", ".")
112
- prefix = target
113
- ->(s) { s.name.start_with?(prefix) }
114
- elsif target.include?("#") || target.include?(".")
115
- ->(s) { s.name == target }
116
- else
117
- ->(s) { s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
118
- end
107
+ return wildcard_matcher(target.chomp("*")) if target.end_with?("*")
108
+ return prefix_matcher(target) if target.end_with?("#", ".")
109
+ return exact_matcher(target) if target.include?("#") || target.include?(".")
110
+
111
+ class_matcher(target)
112
+ end
113
+
114
+ def wildcard_matcher(prefix)
115
+ ->(s) { s.name.split(/[#.]/).first.start_with?(prefix) }
116
+ end
117
+
118
+ def prefix_matcher(prefix)
119
+ ->(s) { s.name.start_with?(prefix) }
120
+ end
121
+
122
+ def exact_matcher(target)
123
+ ->(s) { s.name == target }
124
+ end
125
+
126
+ def class_matcher(target)
127
+ ->(s) { s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
119
128
  end
120
129
 
121
130
  def filter_by_line_ranges(subjects)
@@ -1,36 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
- require_relative "config"
5
- require_relative "ast/parser"
6
- require_relative "memory"
7
- require_relative "mutator/registry"
8
- require_relative "isolation/fork"
9
- require_relative "isolation/in_process"
10
- require_relative "integration/rspec"
11
- require_relative "integration/minitest"
12
- require_relative "reporter/json"
13
- require_relative "reporter/cli"
14
- require_relative "reporter/html"
15
- require_relative "reporter/suggestion"
16
- require_relative "git/changed_files"
17
- require_relative "result/mutation_result"
18
- require_relative "result/summary"
19
- require_relative "baseline"
20
- require_relative "cache"
21
- require_relative "parallel/pool"
22
- require_relative "session/store"
23
- require_relative "temp_dir_tracker"
24
- require_relative "rails_detector"
25
- require_relative "parallel_db_warning"
26
- require_relative "child_output"
27
- require_relative "runner/subject_pipeline"
28
- require_relative "runner/mutation_planner"
29
- require_relative "runner/isolation_resolver"
30
- require_relative "runner/baseline_runner"
31
- require_relative "runner/diagnostics"
32
- require_relative "runner/mutation_executor"
33
- require_relative "runner/report_publisher"
4
+ require_relative "../evilution"
34
5
 
35
6
  class Evilution::Runner
36
7
  attr_reader :config
@@ -40,7 +11,7 @@ class Evilution::Runner
40
11
  @on_result = on_result
41
12
  @hooks = hooks
42
13
  @parser = Evilution::AST::Parser.new
43
- @registry = Evilution::Mutator::Registry.default
14
+ @registry = Evilution::Mutator::Registry.for_profile(config.profile)
44
15
  @cache = config.incremental? ? Evilution::Cache.new : nil
45
16
  end
46
17
 
@@ -61,13 +32,13 @@ class Evilution::Runner
61
32
  plan = mutation_planner.call(subjects)
62
33
  release_subject_nodes(subjects)
63
34
  clear_operator_caches
64
- results, truncated = run_mutations(plan.enabled, baseline_result)
65
- results += equivalent_results(plan.equivalent)
35
+ execution = run_mutations(plan.enabled, baseline_result)
36
+ results = execution.results + equivalent_results(plan.equivalent)
66
37
  log_memory("after run_mutations", "#{results.length} results")
67
38
 
68
39
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
69
40
 
70
- summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: truncated,
41
+ summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: execution.truncated,
71
42
  skipped: plan.skipped_count,
72
43
  disabled_mutations: plan.disabled_mutations)
73
44
  output_report(summary)
@@ -213,3 +184,34 @@ class Evilution::Runner
213
184
  diagnostics.log_memory(phase, context)
214
185
  end
215
186
  end
187
+
188
+ require_relative "config"
189
+ require_relative "ast/parser"
190
+ require_relative "memory"
191
+ require_relative "mutator/registry"
192
+ require_relative "isolation/fork"
193
+ require_relative "isolation/in_process"
194
+ require_relative "integration/rspec"
195
+ require_relative "integration/minitest"
196
+ require_relative "reporter/json"
197
+ require_relative "reporter/cli"
198
+ require_relative "reporter/html"
199
+ require_relative "reporter/suggestion"
200
+ require_relative "git/changed_files"
201
+ require_relative "result/mutation_result"
202
+ require_relative "result/summary"
203
+ require_relative "baseline"
204
+ require_relative "cache"
205
+ require_relative "parallel/pool"
206
+ require_relative "session/store"
207
+ require_relative "temp_dir_tracker"
208
+ require_relative "rails_detector"
209
+ require_relative "parallel_db_warning"
210
+ require_relative "child_output"
211
+ require_relative "runner/subject_pipeline"
212
+ require_relative "runner/mutation_planner"
213
+ require_relative "runner/isolation_resolver"
214
+ require_relative "runner/baseline_runner"
215
+ require_relative "runner/diagnostics"
216
+ require_relative "runner/mutation_executor"
217
+ require_relative "runner/report_publisher"
@@ -38,20 +38,29 @@ class Evilution::Session::Diff
38
38
  def call(base_data, head_data)
39
39
  base_survivors = base_data["survived"] || []
40
40
  head_survivors = head_data["survived"] || []
41
-
42
- base_keys = base_survivors.to_set { |m| mutation_key(m) }
43
- head_keys = head_survivors.to_set { |m| mutation_key(m) }
41
+ fixed, new_survivors, persistent = partition_survivors(base_survivors, head_survivors)
44
42
 
45
43
  Result.new(
46
44
  summary: build_summary_diff(base_data, head_data),
47
- fixed: base_survivors.reject { |m| head_keys.include?(mutation_key(m)) },
48
- new_survivors: head_survivors.reject { |m| base_keys.include?(mutation_key(m)) },
49
- persistent: head_survivors.select { |m| base_keys.include?(mutation_key(m)) }
45
+ fixed: fixed,
46
+ new_survivors: new_survivors,
47
+ persistent: persistent
50
48
  )
51
49
  end
52
50
 
53
51
  private
54
52
 
53
+ def partition_survivors(base_survivors, head_survivors)
54
+ base_keys = base_survivors.to_set { |m| mutation_key(m) }
55
+ head_keys = head_survivors.to_set { |m| mutation_key(m) }
56
+
57
+ [
58
+ base_survivors.reject { |m| head_keys.include?(mutation_key(m)) },
59
+ head_survivors.reject { |m| base_keys.include?(mutation_key(m)) },
60
+ head_survivors.select { |m| base_keys.include?(mutation_key(m)) }
61
+ ]
62
+ end
63
+
55
64
  def build_summary_diff(base_data, head_data)
56
65
  base = extract_summary_values(base_data)
57
66
  head = extract_summary_values(head_data)
@@ -62,18 +62,27 @@ class Evilution::SpecAstCache
62
62
  raise Evilution::ParseError.new("file not found: #{path}", file: path) unless File.exist?(path)
63
63
 
64
64
  source = read_source(path)
65
+ result = parse_source(path, source)
66
+ collect_blocks(source, result, extract_comment_ranges(result))
67
+ end
68
+
69
+ def parse_source(path, source)
65
70
  result = Prism.parse(source)
71
+ return result unless result.failure?
66
72
 
67
- if result.failure?
68
- raise Evilution::ParseError.new(
69
- "failed to parse #{path}: #{result.errors.map(&:message).join(", ")}",
70
- file: path
71
- )
72
- end
73
+ raise Evilution::ParseError.new(
74
+ "failed to parse #{path}: #{result.errors.map(&:message).join(", ")}",
75
+ file: path
76
+ )
77
+ end
73
78
 
74
- comment_ranges = result.comments
75
- .map { |c| c.location.start_offset...c.location.end_offset }
76
- .sort_by(&:begin)
79
+ def extract_comment_ranges(result)
80
+ result.comments
81
+ .map { |c| c.location.start_offset...c.location.end_offset }
82
+ .sort_by(&:begin)
83
+ end
84
+
85
+ def collect_blocks(source, result, comment_ranges)
77
86
  collector = BlockCollector.new(source, comment_ranges)
78
87
  collector.visit(result.value)
79
88
  collector.blocks
@@ -133,16 +142,21 @@ class Evilution::SpecAstCache
133
142
  def strip_comments(slice, base_offset)
134
143
  return slice if @comment_ranges.empty?
135
144
 
136
- ranges = comment_ranges_within(base_offset, base_offset + slice.bytesize)
145
+ end_offset = base_offset + slice.bytesize
146
+ ranges = comment_ranges_within(base_offset, end_offset)
137
147
  return slice if ranges.empty?
138
148
 
149
+ splice_excluding_ranges(base_offset, end_offset, ranges)
150
+ end
151
+
152
+ def splice_excluding_ranges(start_off, end_off, ranges)
139
153
  result = +""
140
- cursor = base_offset
154
+ cursor = start_off
141
155
  ranges.each do |range|
142
156
  result << @source.byteslice(cursor, range.begin - cursor)
143
157
  cursor = range.end
144
158
  end
145
- result << @source.byteslice(cursor, base_offset + slice.bytesize - cursor)
159
+ result << @source.byteslice(cursor, end_off - cursor)
146
160
  result
147
161
  end
148
162
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.27.0"
4
+ VERSION = "0.29.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -89,6 +89,7 @@ require_relative "evilution/mutator/operator/string_interpolation"
89
89
  require_relative "evilution/mutator/operator/retry_removal"
90
90
  require_relative "evilution/mutator/operator/case_when"
91
91
  require_relative "evilution/mutator/operator/predicate_replacement"
92
+ require_relative "evilution/mutator/operator/predicate_to_nil"
92
93
  require_relative "evilution/mutator/operator/equality_to_identity"
93
94
  require_relative "evilution/mutator/operator/lambda_body"
94
95
  require_relative "evilution/mutator/operator/begin_unwrap"
data/script/memory_check CHANGED
@@ -30,16 +30,22 @@ end
30
30
 
31
31
  def report(name, result)
32
32
  status = result[:passed] ? "PASS" : "FAIL"
33
- growth = result[:growth_mb] ? format("%.1f MB", result[:growth_mb]) : "N/A"
34
- max = format("%.1f MB", result[:max_growth_kb] / 1024.0)
35
- samples = result[:samples].map { |s| s ? format("%.1f", s / 1024.0) : "N/A" }.join(" -> ")
33
+ metrics = format_metrics(result)
36
34
 
37
35
  puts "[#{status}] #{name}"
38
- puts " Growth: #{growth} (max: #{max})"
39
- puts " Samples (MB): #{samples}"
36
+ puts " Growth: #{metrics[:growth]} (max: #{metrics[:max]})"
37
+ puts " Samples (MB): #{metrics[:samples]}"
40
38
  puts
41
39
  end
42
40
 
41
+ def format_metrics(result)
42
+ {
43
+ growth: result[:growth_mb] ? format("%.1f MB", result[:growth_mb]) : "N/A",
44
+ max: format("%.1f MB", result[:max_growth_kb] / 1024.0),
45
+ samples: result[:samples].map { |s| s ? format("%.1f", s / 1024.0) : "N/A" }.join(" -> ")
46
+ }
47
+ end
48
+
43
49
  abort("RSS measurement unavailable (requires /proc filesystem)") unless Evilution::Memory.rss_kb
44
50
 
45
51
  mutations = setup_workload
@@ -88,7 +94,9 @@ if mutations.size >= 2
88
94
  batch.zip(compact_results).map do |mutation, data|
89
95
  Evilution::Result::MutationResult.new(
90
96
  mutation: mutation, status: data[:status], duration: data[:duration],
91
- child_rss_kb: data[:child_rss_kb], memory_delta_kb: data[:memory_delta_kb]
97
+ memory: Evilution::Result::MemoryStats.new(
98
+ child_rss_kb: data[:child_rss_kb], memory_delta_kb: data[:memory_delta_kb]
99
+ )
92
100
  )
93
101
  end
94
102
  end
@@ -155,18 +155,19 @@ module BenchmarkDensity
155
155
  def print_table(results)
156
156
  puts format(HEADER_FMT, file: "File", evilution: "Evilution", reference: "Reference", ratio: "Ratio")
157
157
  puts "-" * 75
158
-
159
- results.each do |r|
160
- ratio = compute_ratio(r[:evilution], r[:reference])
161
- ratio_str = ratio ? format("%.2fx", ratio) : "N/A"
162
- ev_str = r[:evilution]&.to_s || "ERR"
163
- ref_str = r[:reference]&.to_s || "ERR"
164
- puts format(ROW_FMT, file: r[:path], evilution: ev_str, reference: ref_str, ratio: ratio_str)
165
- end
166
-
158
+ results.each { |r| puts format_result_row(r) }
167
159
  puts "-" * 75
168
160
  end
169
161
 
162
+ def format_result_row(result)
163
+ ratio = compute_ratio(result[:evilution], result[:reference])
164
+ format(ROW_FMT,
165
+ file: result[:path],
166
+ evilution: result[:evilution]&.to_s || "ERR",
167
+ reference: result[:reference]&.to_s || "ERR",
168
+ ratio: ratio ? format("%.2fx", ratio) : "N/A")
169
+ end
170
+
170
171
  def print_summary(results)
171
172
  totals = compute_totals(results)
172
173
  print_total_line(totals)
@@ -221,17 +221,25 @@ module CompareMutations
221
221
  lines = ["## Extra in reference (#{extra.size})", ""]
222
222
 
223
223
  catalog.summary.each do |entry|
224
- lines << "### #{entry[:operator]} (#{entry[:count]})"
225
- catalog.by_operator[entry[:operator]].each do |m|
226
- lines << " Line #{m["line"]}:"
227
- m["diff"].to_s.each_line { |l| lines << " #{l.chomp}" }
228
- lines << ""
229
- end
224
+ lines.concat(format_operator_group(entry, catalog.by_operator[entry[:operator]]))
230
225
  end
231
226
 
232
227
  lines
233
228
  end
234
229
 
230
+ def format_operator_group(entry, mutations)
231
+ lines = ["### #{entry[:operator]} (#{entry[:count]})"]
232
+ mutations.each { |m| lines.concat(format_extra_mutation(m)) }
233
+ lines
234
+ end
235
+
236
+ def format_extra_mutation(mutation)
237
+ lines = [" Line #{mutation["line"]}:"]
238
+ mutation["diff"].to_s.each_line { |l| lines << " #{l.chomp}" }
239
+ lines << ""
240
+ lines
241
+ end
242
+
235
243
  def build_extra_evilution_section(comparison)
236
244
  ev_extra = comparison.extra_in_evilution
237
245
  return [] if ev_extra.empty?
@@ -343,34 +351,43 @@ module CompareMutations
343
351
 
344
352
  def compare_file(path, _reference_target, reporter)
345
353
  full_path = File.join(@config.project_root, path)
346
- ev_data = @evilution.collect(path)
347
- ref_data = @reference.collect(full_path)
348
-
349
- ev_set = MutationSet.from_json(ev_data)
350
- ref_set = MutationSet.from_json(ref_data)
354
+ ev_set = MutationSet.from_json(@evilution.collect(path))
355
+ ref_set = MutationSet.from_json(@reference.collect(full_path))
351
356
  comparison = Comparison.new(evilution: ev_set, reference: ref_set)
352
357
  catalog = Catalog.new(comparison.extra_in_reference)
353
358
 
354
359
  reporter.write_file_report(path, comparison, catalog)
360
+ build_file_result(path, comparison, catalog)
361
+ end
355
362
 
356
- { file: path, evilution_count: ev_set.size, reference_count: ref_set.size,
363
+ def build_file_result(path, comparison, catalog)
364
+ {
365
+ file: path,
366
+ evilution_count: comparison.evilution_set.size,
367
+ reference_count: comparison.reference_set.size,
357
368
  density_ratio: comparison.density_ratio.round(2),
358
369
  extra_count: comparison.extra_in_reference.size,
359
- operator_summary: catalog.summary }
370
+ operator_summary: catalog.summary
371
+ }
360
372
  end
361
373
 
362
374
  def print_summary(file_results)
363
- ev_total = file_results.sum { |r| r[:evilution_count] }
364
- ref_total = file_results.sum { |r| r[:reference_count] }
365
- extra_total = file_results.sum { |r| r[:extra_count] }
366
- ratio = ev_total.positive? ? (ref_total.to_f / ev_total).round(2) : 0.0
375
+ totals = compute_totals(file_results)
367
376
 
368
377
  puts "Comparison complete. Results in #{@config.output_dir}/"
369
378
  puts " Files: #{file_results.size}"
370
- puts " Evilution: #{ev_total} mutations"
371
- puts " Reference: #{ref_total} mutations"
372
- puts " Ratio: #{ratio}x"
373
- puts " Extra in reference: #{extra_total}"
379
+ puts " Evilution: #{totals[:ev]} mutations"
380
+ puts " Reference: #{totals[:ref]} mutations"
381
+ puts " Ratio: #{totals[:ratio]}x"
382
+ puts " Extra in reference: #{totals[:extra]}"
383
+ end
384
+
385
+ def compute_totals(file_results)
386
+ ev = file_results.sum { |r| r[:evilution_count] }
387
+ ref = file_results.sum { |r| r[:reference_count] }
388
+ extra = file_results.sum { |r| r[:extra_count] }
389
+ { ev: ev, ref: ref, extra: extra,
390
+ ratio: ev.positive? ? (ref.to_f / ev).round(2) : 0.0 }
374
391
  end
375
392
  end
376
393
  end
@@ -128,14 +128,17 @@ module MutantJsonAdapter
128
128
  end
129
129
 
130
130
  def infer_operator(diff_lines)
131
- removed = diff_lines.select { |l| l.start_with?("-") && !l.start_with?("---") }
132
- .map { |l| l[1..].strip }
133
- added = diff_lines.select { |l| l.start_with?("+") && !l.start_with?("+++") }
134
- .map { |l| l[1..].strip }
131
+ removed = extract_diff_side(diff_lines, "-", "---")
132
+ added = extract_diff_side(diff_lines, "+", "+++")
135
133
 
136
134
  categorize_mutation(removed, added)
137
135
  end
138
136
 
137
+ def extract_diff_side(diff_lines, prefix, header)
138
+ diff_lines.select { |l| l.start_with?(prefix) && !l.start_with?(header) }
139
+ .map { |l| l[1..].strip }
140
+ end
141
+
139
142
  def categorize_mutation(removed, added)
140
143
  return "replacement" if removed.empty?
141
144
 
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.27.0
4
+ version: 0.29.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-04-26 00:00:00.000000000 Z
11
+ date: 2026-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -158,8 +158,12 @@ files:
158
158
  - lib/evilution/compare.rb
159
159
  - lib/evilution/compare/categorizer.rb
160
160
  - lib/evilution/compare/detector.rb
161
+ - lib/evilution/compare/diff_extractor.rb
162
+ - lib/evilution/compare/diff_extractor/evilution.rb
163
+ - lib/evilution/compare/diff_extractor/mutant.rb
161
164
  - lib/evilution/compare/fingerprint.rb
162
165
  - lib/evilution/compare/invalid_input.rb
166
+ - lib/evilution/compare/line_normalizer.rb
163
167
  - lib/evilution/compare/normalizer.rb
164
168
  - lib/evilution/compare/record.rb
165
169
  - lib/evilution/config.rb
@@ -180,6 +184,7 @@ files:
180
184
  - lib/evilution/config/validators/isolation.rb
181
185
  - lib/evilution/config/validators/jobs.rb
182
186
  - lib/evilution/config/validators/preload.rb
187
+ - lib/evilution/config/validators/profile.rb
183
188
  - lib/evilution/config/validators/spec_mappings.rb
184
189
  - lib/evilution/config/validators/spec_pattern.rb
185
190
  - lib/evilution/disable_comment.rb
@@ -316,6 +321,7 @@ files:
316
321
  - lib/evilution/mutator/operator/pattern_matching_array.rb
317
322
  - lib/evilution/mutator/operator/pattern_matching_guard.rb
318
323
  - lib/evilution/mutator/operator/predicate_replacement.rb
324
+ - lib/evilution/mutator/operator/predicate_to_nil.rb
319
325
  - lib/evilution/mutator/operator/range_replacement.rb
320
326
  - lib/evilution/mutator/operator/receiver_replacement.rb
321
327
  - lib/evilution/mutator/operator/redo_statement.rb
@@ -352,6 +358,7 @@ files:
352
358
  - lib/evilution/parallel/work_queue/worker/loop.rb
353
359
  - lib/evilution/parallel/work_queue/worker_stat.rb
354
360
  - lib/evilution/parallel_db_warning.rb
361
+ - lib/evilution/process_cleanup.rb
355
362
  - lib/evilution/rails_detector.rb
356
363
  - lib/evilution/related_spec_heuristic.rb
357
364
  - lib/evilution/reporter.rb
@@ -381,7 +388,6 @@ files:
381
388
  - lib/evilution/reporter/html/baseline_keys.rb
382
389
  - lib/evilution/reporter/html/diff_formatter.rb
383
390
  - lib/evilution/reporter/html/escape.rb
384
- - lib/evilution/reporter/html/namespace.rb
385
391
  - lib/evilution/reporter/html/report.rb
386
392
  - lib/evilution/reporter/html/section.rb
387
393
  - lib/evilution/reporter/html/sections.rb
@@ -417,13 +423,17 @@ files:
417
423
  - lib/evilution/reporter/progress_bar.rb
418
424
  - lib/evilution/reporter/suggestion.rb
419
425
  - lib/evilution/reporter/suggestion/diff_helpers.rb
426
+ - lib/evilution/reporter/suggestion/diff_lines.rb
420
427
  - lib/evilution/reporter/suggestion/registry.rb
428
+ - lib/evilution/reporter/suggestion/templates.rb
421
429
  - lib/evilution/reporter/suggestion/templates/generic.rb
422
430
  - lib/evilution/reporter/suggestion/templates/minitest.rb
423
431
  - lib/evilution/reporter/suggestion/templates/rspec.rb
424
432
  - lib/evilution/result.rb
425
433
  - lib/evilution/result/coverage_gap.rb
426
434
  - lib/evilution/result/coverage_gap_grouper.rb
435
+ - lib/evilution/result/error_info.rb
436
+ - lib/evilution/result/memory_stats.rb
427
437
  - lib/evilution/result/mutation_result.rb
428
438
  - lib/evilution/result/summary.rb
429
439
  - lib/evilution/runner.rb
@@ -433,11 +443,13 @@ files:
433
443
  - lib/evilution/runner/mutation_executor.rb
434
444
  - lib/evilution/runner/mutation_executor/mutation_runner.rb
435
445
  - lib/evilution/runner/mutation_executor/neutralization_pipeline.rb
446
+ - lib/evilution/runner/mutation_executor/neutralizer.rb
436
447
  - lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb
437
448
  - lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb
438
449
  - lib/evilution/runner/mutation_executor/result_cache.rb
439
450
  - lib/evilution/runner/mutation_executor/result_notifier.rb
440
451
  - lib/evilution/runner/mutation_executor/result_packer.rb
452
+ - lib/evilution/runner/mutation_executor/strategy.rb
441
453
  - lib/evilution/runner/mutation_executor/strategy/parallel.rb
442
454
  - lib/evilution/runner/mutation_executor/strategy/sequential.rb
443
455
  - lib/evilution/runner/mutation_planner.rb