evilution 0.28.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +52 -0
  3. data/CHANGELOG.md +7 -0
  4. data/lib/evilution/ast/constant_names.rb +28 -11
  5. data/lib/evilution/ast/pattern/parser.rb +29 -17
  6. data/lib/evilution/cli/commands/session_diff.rb +6 -4
  7. data/lib/evilution/cli/commands/subjects.rb +6 -3
  8. data/lib/evilution/cli/commands/util_mutation.rb +24 -19
  9. data/lib/evilution/cli/parser/command_extractor.rb +9 -11
  10. data/lib/evilution/cli/parser/file_args.rb +3 -1
  11. data/lib/evilution/cli/parser/options_builder.rb +29 -1
  12. data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
  13. data/lib/evilution/cli/parser.rb +18 -20
  14. data/lib/evilution/cli/printers/environment.rb +19 -19
  15. data/lib/evilution/cli/printers/session_diff.rb +8 -8
  16. data/lib/evilution/compare/normalizer.rb +10 -5
  17. data/lib/evilution/config.rb +10 -10
  18. data/lib/evilution/disable_comment.rb +21 -12
  19. data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
  20. data/lib/evilution/integration/minitest.rb +25 -16
  21. data/lib/evilution/integration/rspec.rb +4 -0
  22. data/lib/evilution/isolation/fork.rb +27 -17
  23. data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
  24. data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
  25. data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
  26. data/lib/evilution/mcp/info_tool.rb +7 -1
  27. data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
  28. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
  29. data/lib/evilution/mcp/mutate_tool.rb +27 -14
  30. data/lib/evilution/mcp/session_tool.rb +27 -18
  31. data/lib/evilution/mutation.rb +13 -15
  32. data/lib/evilution/mutator/base.rb +17 -15
  33. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
  34. data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
  35. data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
  36. data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
  37. data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
  38. data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
  39. data/lib/evilution/mutator/operator/case_when.rb +7 -5
  40. data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
  41. data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
  42. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
  43. data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
  44. data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
  45. data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
  46. data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
  47. data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
  48. data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
  49. data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
  50. data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
  51. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
  52. data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
  53. data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
  54. data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
  55. data/lib/evilution/parallel/work_queue/worker.rb +10 -7
  56. data/lib/evilution/parallel/work_queue.rb +35 -18
  57. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
  58. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
  59. data/lib/evilution/reporter/json.rb +52 -18
  60. data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
  61. data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
  62. data/lib/evilution/reporter/suggestion/templates/minitest.rb +20 -14
  63. data/lib/evilution/reporter/suggestion/templates/rspec.rb +19 -13
  64. data/lib/evilution/runner/baseline_runner.rb +15 -8
  65. data/lib/evilution/runner/diagnostics.rb +13 -9
  66. data/lib/evilution/runner/isolation_resolver.rb +11 -9
  67. data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -1
  68. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +32 -10
  69. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +1 -1
  70. data/lib/evilution/runner/mutation_executor.rb +2 -0
  71. data/lib/evilution/runner/mutation_planner.rb +37 -17
  72. data/lib/evilution/runner/subject_pipeline.rb +21 -11
  73. data/lib/evilution/runner.rb +3 -3
  74. data/lib/evilution/session/diff.rb +15 -6
  75. data/lib/evilution/spec_ast_cache.rb +26 -12
  76. data/lib/evilution/version.rb +1 -1
  77. data/script/memory_check +11 -5
  78. data/scripts/benchmark_density +10 -9
  79. data/scripts/compare_mutations +38 -21
  80. data/scripts/mutant_json_adapter +7 -4
  81. metadata +3 -2
@@ -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.28.0"
4
+ VERSION = "0.29.0"
5
5
  end
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
@@ -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.28.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-05-03 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
@@ -423,6 +423,7 @@ files:
423
423
  - lib/evilution/reporter/progress_bar.rb
424
424
  - lib/evilution/reporter/suggestion.rb
425
425
  - lib/evilution/reporter/suggestion/diff_helpers.rb
426
+ - lib/evilution/reporter/suggestion/diff_lines.rb
426
427
  - lib/evilution/reporter/suggestion/registry.rb
427
428
  - lib/evilution/reporter/suggestion/templates.rb
428
429
  - lib/evilution/reporter/suggestion/templates/generic.rb