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.
- checksums.yaml +4 -4
- data/.beads/interactions.jsonl +52 -0
- data/CHANGELOG.md +7 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/cli/commands/session_diff.rb +6 -4
- data/lib/evilution/cli/commands/subjects.rb +6 -3
- data/lib/evilution/cli/commands/util_mutation.rb +24 -19
- data/lib/evilution/cli/parser/command_extractor.rb +9 -11
- data/lib/evilution/cli/parser/file_args.rb +3 -1
- data/lib/evilution/cli/parser/options_builder.rb +29 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
- data/lib/evilution/cli/parser.rb +18 -20
- data/lib/evilution/cli/printers/environment.rb +19 -19
- data/lib/evilution/cli/printers/session_diff.rb +8 -8
- data/lib/evilution/compare/normalizer.rb +10 -5
- data/lib/evilution/config.rb +10 -10
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
- data/lib/evilution/integration/minitest.rb +25 -16
- data/lib/evilution/integration/rspec.rb +4 -0
- data/lib/evilution/isolation/fork.rb +27 -17
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
- data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
- data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
- data/lib/evilution/mcp/info_tool.rb +7 -1
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
- data/lib/evilution/mcp/mutate_tool.rb +27 -14
- data/lib/evilution/mcp/session_tool.rb +27 -18
- data/lib/evilution/mutation.rb +13 -15
- data/lib/evilution/mutator/base.rb +17 -15
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
- data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
- data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
- data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
- data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
- data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
- data/lib/evilution/mutator/operator/case_when.rb +7 -5
- data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
- data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
- data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
- data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
- data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
- data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
- data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
- data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
- data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
- data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
- data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
- data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
- data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
- data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
- data/lib/evilution/parallel/work_queue/worker.rb +10 -7
- data/lib/evilution/parallel/work_queue.rb +35 -18
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
- data/lib/evilution/reporter/json.rb +52 -18
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
- data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +20 -14
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +19 -13
- data/lib/evilution/runner/baseline_runner.rb +15 -8
- data/lib/evilution/runner/diagnostics.rb +13 -9
- data/lib/evilution/runner/isolation_resolver.rb +11 -9
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -1
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +32 -10
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +1 -1
- data/lib/evilution/runner/mutation_executor.rb +2 -0
- data/lib/evilution/runner/mutation_planner.rb +37 -17
- data/lib/evilution/runner/subject_pipeline.rb +21 -11
- data/lib/evilution/runner.rb +3 -3
- data/lib/evilution/session/diff.rb +15 -6
- data/lib/evilution/spec_ast_cache.rb +26 -12
- data/lib/evilution/version.rb +1 -1
- data/script/memory_check +11 -5
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- 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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
159
|
+
result << @source.byteslice(cursor, end_off - cursor)
|
|
146
160
|
result
|
|
147
161
|
end
|
|
148
162
|
|
data/lib/evilution/version.rb
CHANGED
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
|
-
|
|
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
|
data/scripts/benchmark_density
CHANGED
|
@@ -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)
|
data/scripts/compare_mutations
CHANGED
|
@@ -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
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: #{
|
|
371
|
-
puts " Reference: #{
|
|
372
|
-
puts " Ratio: #{ratio}x"
|
|
373
|
-
puts " Extra in reference: #{
|
|
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
|
data/scripts/mutant_json_adapter
CHANGED
|
@@ -128,14 +128,17 @@ module MutantJsonAdapter
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def infer_operator(diff_lines)
|
|
131
|
-
removed = diff_lines
|
|
132
|
-
|
|
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.
|
|
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-
|
|
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
|