evilution 0.8.0 → 0.10.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/.migration-hint-ts +1 -1
- data/.beads/issues.jsonl +17 -7
- data/CHANGELOG.md +31 -0
- data/lib/evilution/cache.rb +85 -0
- data/lib/evilution/cli.rb +2 -10
- data/lib/evilution/config.rb +11 -25
- data/lib/evilution/equivalent/detector.rb +42 -0
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +37 -0
- data/lib/evilution/equivalent/heuristic/dead_code.rb +51 -0
- data/lib/evilution/equivalent/heuristic/method_body_nil.rb +23 -0
- data/lib/evilution/equivalent/heuristic/noop_source.rb +13 -0
- data/lib/evilution/mcp/mutate_tool.rb +16 -1
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +42 -0
- data/lib/evilution/mutator/operator/receiver_replacement.rb +27 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +49 -0
- data/lib/evilution/mutator/registry.rb +4 -1
- data/lib/evilution/reporter/cli.rb +11 -1
- data/lib/evilution/reporter/html.rb +252 -0
- data/lib/evilution/reporter/json.rb +5 -1
- data/lib/evilution/result/mutation_result.rb +5 -1
- data/lib/evilution/result/summary.rb +9 -1
- data/lib/evilution/runner.rb +124 -29
- data/lib/evilution/spec_resolver.rb +28 -6
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +6 -4
- metadata +12 -6
- data/lib/evilution/coverage/collector.rb +0 -47
- data/lib/evilution/coverage/test_map.rb +0 -25
- data/lib/evilution/diff/file_filter.rb +0 -29
- data/lib/evilution/diff/parser.rb +0 -47
data/lib/evilution/runner.rb
CHANGED
|
@@ -9,13 +9,14 @@ require_relative "isolation/in_process"
|
|
|
9
9
|
require_relative "integration/rspec"
|
|
10
10
|
require_relative "reporter/json"
|
|
11
11
|
require_relative "reporter/cli"
|
|
12
|
+
require_relative "reporter/html"
|
|
12
13
|
require_relative "reporter/suggestion"
|
|
13
|
-
require_relative "
|
|
14
|
-
require_relative "diff/file_filter"
|
|
14
|
+
require_relative "equivalent/detector"
|
|
15
15
|
require_relative "git/changed_files"
|
|
16
16
|
require_relative "result/mutation_result"
|
|
17
17
|
require_relative "result/summary"
|
|
18
18
|
require_relative "baseline"
|
|
19
|
+
require_relative "cache"
|
|
19
20
|
require_relative "parallel/pool"
|
|
20
21
|
|
|
21
22
|
module Evilution
|
|
@@ -27,6 +28,7 @@ module Evilution
|
|
|
27
28
|
@parser = AST::Parser.new
|
|
28
29
|
@registry = Mutator::Registry.default
|
|
29
30
|
@isolator = build_isolator
|
|
31
|
+
@cache = config.incremental? ? Cache.new : nil
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def call
|
|
@@ -35,13 +37,18 @@ module Evilution
|
|
|
35
37
|
subjects = parse_subjects
|
|
36
38
|
subjects = filter_by_target(subjects) if config.target?
|
|
37
39
|
subjects = filter_by_line_ranges(subjects) if config.line_ranges?
|
|
38
|
-
subjects = filter_by_diff(subjects) if config.diff?
|
|
39
40
|
log_memory("after parse_subjects", "#{subjects.length} subjects")
|
|
40
41
|
|
|
41
42
|
baseline_result = run_baseline(subjects)
|
|
42
43
|
|
|
43
44
|
mutations = generate_mutations(subjects)
|
|
45
|
+
equivalent_mutations, mutations = filter_equivalent(mutations)
|
|
46
|
+
release_subject_nodes(subjects)
|
|
44
47
|
results, truncated = run_mutations(mutations, baseline_result)
|
|
48
|
+
results += equivalent_mutations.map do |m|
|
|
49
|
+
m.strip_sources!
|
|
50
|
+
equivalent_result(m)
|
|
51
|
+
end
|
|
45
52
|
log_memory("after run_mutations", "#{results.length} results")
|
|
46
53
|
|
|
47
54
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
@@ -54,7 +61,7 @@ module Evilution
|
|
|
54
61
|
|
|
55
62
|
private
|
|
56
63
|
|
|
57
|
-
attr_reader :parser, :registry, :isolator
|
|
64
|
+
attr_reader :parser, :registry, :isolator, :cache
|
|
58
65
|
|
|
59
66
|
def parse_subjects
|
|
60
67
|
files = resolve_target_files
|
|
@@ -68,7 +75,11 @@ module Evilution
|
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
def filter_by_target(subjects)
|
|
71
|
-
matched =
|
|
78
|
+
matched = if config.target.include?("#")
|
|
79
|
+
subjects.select { |s| s.name == config.target }
|
|
80
|
+
else
|
|
81
|
+
subjects.select { |s| s.name.start_with?("#{config.target}#") }
|
|
82
|
+
end
|
|
72
83
|
raise Error, "no method found matching '#{config.target}'" if matched.empty?
|
|
73
84
|
|
|
74
85
|
matched
|
|
@@ -85,20 +96,24 @@ module Evilution
|
|
|
85
96
|
end
|
|
86
97
|
end
|
|
87
98
|
|
|
88
|
-
def filter_by_diff(subjects)
|
|
89
|
-
diff_parser = Diff::Parser.new
|
|
90
|
-
changed_ranges = diff_parser.parse(config.diff_base)
|
|
91
|
-
Diff::FileFilter.new.filter(subjects, changed_ranges)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
99
|
def generate_mutations(subjects)
|
|
95
100
|
subjects.flat_map do |subject|
|
|
96
|
-
|
|
97
|
-
subject.release_node!
|
|
98
|
-
mutations
|
|
101
|
+
registry.mutations_for(subject)
|
|
99
102
|
end
|
|
100
103
|
end
|
|
101
104
|
|
|
105
|
+
def filter_equivalent(mutations)
|
|
106
|
+
Equivalent::Detector.new.call(mutations)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def release_subject_nodes(subjects)
|
|
110
|
+
subjects.each(&:release_node!)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def equivalent_result(mutation)
|
|
114
|
+
Result::MutationResult.new(mutation: mutation, status: :equivalent, duration: 0.0)
|
|
115
|
+
end
|
|
116
|
+
|
|
102
117
|
def run_baseline(subjects)
|
|
103
118
|
return nil unless config.baseline? && subjects.any?
|
|
104
119
|
|
|
@@ -125,12 +140,10 @@ module Evilution
|
|
|
125
140
|
truncated = false
|
|
126
141
|
|
|
127
142
|
mutations.each_with_index do |mutation, index|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
mutation: mutation,
|
|
131
|
-
|
|
132
|
-
timeout: config.timeout
|
|
133
|
-
)
|
|
143
|
+
result = execute_or_fetch(mutation) do
|
|
144
|
+
test_command = ->(m) { integration.call(m) }
|
|
145
|
+
isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
|
|
146
|
+
end
|
|
134
147
|
mutation.strip_sources!
|
|
135
148
|
result = neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
|
|
136
149
|
results << result
|
|
@@ -157,20 +170,34 @@ module Evilution
|
|
|
157
170
|
mutations.each_slice(config.jobs) do |batch|
|
|
158
171
|
break if state[:truncated]
|
|
159
172
|
|
|
160
|
-
|
|
161
|
-
test_command = ->(m) { integration.call(m) }
|
|
162
|
-
result = worker_isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
|
|
163
|
-
compact_result(result)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
batch.each(&:strip_sources!)
|
|
167
|
-
batch_results = rebuild_results(batch, compact_results)
|
|
173
|
+
batch_results = run_parallel_batch(batch, pool, worker_isolator, integration)
|
|
168
174
|
process_batch(batch_results, baseline_result, spec_resolver, state)
|
|
169
175
|
end
|
|
170
176
|
|
|
171
177
|
[state[:results], state[:truncated]]
|
|
172
178
|
end
|
|
173
179
|
|
|
180
|
+
def run_parallel_batch(batch, pool, worker_isolator, integration)
|
|
181
|
+
uncached_indices, cached_results = partition_cached(batch)
|
|
182
|
+
worker_results = run_uncached_workers(batch, uncached_indices, pool, worker_isolator, integration)
|
|
183
|
+
compact_results = merge_parallel_results(batch, uncached_indices, cached_results, worker_results)
|
|
184
|
+
batch.each(&:strip_sources!)
|
|
185
|
+
batch_results = rebuild_results(batch, compact_results)
|
|
186
|
+
batch_results.each { |r| store_cached_result(r.mutation, r) }
|
|
187
|
+
batch_results
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def run_uncached_workers(batch, uncached_indices, pool, worker_isolator, integration)
|
|
191
|
+
return [] if uncached_indices.empty?
|
|
192
|
+
|
|
193
|
+
uncached = uncached_indices.map { |i| batch[i] }
|
|
194
|
+
pool.map(uncached) do |mutation|
|
|
195
|
+
test_command = ->(m) { integration.call(m) }
|
|
196
|
+
result = worker_isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
|
|
197
|
+
compact_result(result)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
174
201
|
def process_batch(batch_results, baseline_result, spec_resolver, state)
|
|
175
202
|
batch_results.each do |result|
|
|
176
203
|
result = neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
|
|
@@ -263,7 +290,15 @@ module Evilution
|
|
|
263
290
|
return unless reporter
|
|
264
291
|
|
|
265
292
|
output = reporter.call(summary)
|
|
266
|
-
|
|
293
|
+
return if config.quiet
|
|
294
|
+
|
|
295
|
+
if config.html?
|
|
296
|
+
path = "evilution-report.html"
|
|
297
|
+
File.write(path, output)
|
|
298
|
+
warn "HTML report written to #{path}"
|
|
299
|
+
else
|
|
300
|
+
$stdout.puts(output)
|
|
301
|
+
end
|
|
267
302
|
end
|
|
268
303
|
|
|
269
304
|
def log_baseline_start
|
|
@@ -332,7 +367,67 @@ module Evilution
|
|
|
332
367
|
Reporter::JSON.new
|
|
333
368
|
when :text
|
|
334
369
|
Reporter::CLI.new
|
|
370
|
+
when :html
|
|
371
|
+
Reporter::HTML.new
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def partition_cached(batch)
|
|
376
|
+
uncached_indices = []
|
|
377
|
+
cached_results = {}
|
|
378
|
+
|
|
379
|
+
batch.each_with_index do |mutation, i|
|
|
380
|
+
cached = fetch_cached_result(mutation)
|
|
381
|
+
if cached
|
|
382
|
+
cached_results[i] = compact_result(cached)
|
|
383
|
+
else
|
|
384
|
+
uncached_indices << i
|
|
385
|
+
end
|
|
335
386
|
end
|
|
387
|
+
|
|
388
|
+
[uncached_indices, cached_results]
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def merge_parallel_results(batch, uncached_indices, cached_results, worker_results)
|
|
392
|
+
result_map = cached_results.dup
|
|
393
|
+
uncached_indices.each_with_index { |batch_idx, worker_idx| result_map[batch_idx] = worker_results[worker_idx] }
|
|
394
|
+
batch.each_index.map { |i| result_map[i] }
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def execute_or_fetch(mutation)
|
|
398
|
+
cached = fetch_cached_result(mutation)
|
|
399
|
+
return cached if cached
|
|
400
|
+
|
|
401
|
+
result = yield
|
|
402
|
+
store_cached_result(mutation, result)
|
|
403
|
+
result
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def fetch_cached_result(mutation)
|
|
407
|
+
return nil unless cache
|
|
408
|
+
|
|
409
|
+
data = cache.fetch(mutation)
|
|
410
|
+
return nil unless data
|
|
411
|
+
return nil unless %i[killed timeout].include?(data[:status])
|
|
412
|
+
|
|
413
|
+
Result::MutationResult.new(
|
|
414
|
+
mutation: mutation,
|
|
415
|
+
status: data[:status],
|
|
416
|
+
duration: data[:duration],
|
|
417
|
+
killing_test: data[:killing_test],
|
|
418
|
+
test_command: data[:test_command]
|
|
419
|
+
)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def store_cached_result(mutation, result)
|
|
423
|
+
return unless cache
|
|
424
|
+
return unless result.killed? || result.timeout?
|
|
425
|
+
|
|
426
|
+
cache.store(mutation,
|
|
427
|
+
status: result.status,
|
|
428
|
+
duration: result.duration,
|
|
429
|
+
killing_test: result.killing_test,
|
|
430
|
+
test_command: result.test_command)
|
|
336
431
|
end
|
|
337
432
|
end
|
|
338
433
|
end
|
|
@@ -28,13 +28,35 @@ module Evilution
|
|
|
28
28
|
base = source_path.sub(/\.rb\z/, "_spec.rb")
|
|
29
29
|
prefix = STRIPPABLE_PREFIXES.find { |p| source_path.start_with?(p) }
|
|
30
30
|
|
|
31
|
-
if prefix
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
candidates = if prefix
|
|
32
|
+
stripped = base.delete_prefix(prefix)
|
|
33
|
+
["spec/#{stripped}", "spec/#{base}"]
|
|
34
|
+
else
|
|
35
|
+
["spec/#{base}"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
fallbacks = candidates.flat_map { |c| parent_fallback_candidates(c) }.uniq
|
|
39
|
+
candidates + fallbacks
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parent_fallback_candidates(spec_path)
|
|
43
|
+
parts = spec_path.split("/")
|
|
44
|
+
# parts: ["spec", "foo", "bar_spec.rb"] — need at least 3 parts for fallback
|
|
45
|
+
return [] if parts.length < 3
|
|
46
|
+
|
|
47
|
+
candidates = []
|
|
48
|
+
# Remove filename, then progressively remove directories
|
|
49
|
+
dir_parts = parts[1..-2] # ["models", "game"]
|
|
50
|
+
(dir_parts.length - 1).downto(0) do |i|
|
|
51
|
+
file = "#{dir_parts[i]}_spec.rb"
|
|
52
|
+
if i.zero?
|
|
53
|
+
candidates << "spec/#{file}"
|
|
54
|
+
else
|
|
55
|
+
parent = dir_parts[0...i].join("/")
|
|
56
|
+
candidates << "spec/#{parent}/#{file}"
|
|
57
|
+
end
|
|
37
58
|
end
|
|
59
|
+
candidates
|
|
38
60
|
end
|
|
39
61
|
end
|
|
40
62
|
end
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -32,12 +32,14 @@ require_relative "evilution/mutator/operator/block_removal"
|
|
|
32
32
|
require_relative "evilution/mutator/operator/conditional_flip"
|
|
33
33
|
require_relative "evilution/mutator/operator/range_replacement"
|
|
34
34
|
require_relative "evilution/mutator/operator/regexp_mutation"
|
|
35
|
+
require_relative "evilution/mutator/operator/receiver_replacement"
|
|
36
|
+
require_relative "evilution/mutator/operator/send_mutation"
|
|
37
|
+
require_relative "evilution/mutator/operator/argument_nil_substitution"
|
|
35
38
|
require_relative "evilution/mutator/registry"
|
|
39
|
+
require_relative "evilution/equivalent/detector"
|
|
36
40
|
require_relative "evilution/isolation/fork"
|
|
37
41
|
require_relative "evilution/isolation/in_process"
|
|
38
42
|
require_relative "evilution/parallel/pool"
|
|
39
|
-
require_relative "evilution/diff/parser"
|
|
40
|
-
require_relative "evilution/diff/file_filter"
|
|
41
43
|
require_relative "evilution/git/changed_files"
|
|
42
44
|
require_relative "evilution/integration/base"
|
|
43
45
|
require_relative "evilution/integration/rspec"
|
|
@@ -45,11 +47,11 @@ require_relative "evilution/result/mutation_result"
|
|
|
45
47
|
require_relative "evilution/result/summary"
|
|
46
48
|
require_relative "evilution/reporter/json"
|
|
47
49
|
require_relative "evilution/reporter/cli"
|
|
50
|
+
require_relative "evilution/reporter/html"
|
|
48
51
|
require_relative "evilution/reporter/suggestion"
|
|
49
|
-
require_relative "evilution/coverage/collector"
|
|
50
|
-
require_relative "evilution/coverage/test_map"
|
|
51
52
|
require_relative "evilution/spec_resolver"
|
|
52
53
|
require_relative "evilution/baseline"
|
|
54
|
+
require_relative "evilution/cache"
|
|
53
55
|
require_relative "evilution/cli"
|
|
54
56
|
require_relative "evilution/runner"
|
|
55
57
|
|
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.10.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-03-
|
|
11
|
+
date: 2026-03-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -81,12 +81,14 @@ files:
|
|
|
81
81
|
- lib/evilution/ast/parser.rb
|
|
82
82
|
- lib/evilution/ast/source_surgeon.rb
|
|
83
83
|
- lib/evilution/baseline.rb
|
|
84
|
+
- lib/evilution/cache.rb
|
|
84
85
|
- lib/evilution/cli.rb
|
|
85
86
|
- lib/evilution/config.rb
|
|
86
|
-
- lib/evilution/
|
|
87
|
-
- lib/evilution/
|
|
88
|
-
- lib/evilution/
|
|
89
|
-
- lib/evilution/
|
|
87
|
+
- lib/evilution/equivalent/detector.rb
|
|
88
|
+
- lib/evilution/equivalent/heuristic/alias_swap.rb
|
|
89
|
+
- lib/evilution/equivalent/heuristic/dead_code.rb
|
|
90
|
+
- lib/evilution/equivalent/heuristic/method_body_nil.rb
|
|
91
|
+
- lib/evilution/equivalent/heuristic/noop_source.rb
|
|
90
92
|
- lib/evilution/git/changed_files.rb
|
|
91
93
|
- lib/evilution/integration/base.rb
|
|
92
94
|
- lib/evilution/integration/rspec.rb
|
|
@@ -98,6 +100,7 @@ files:
|
|
|
98
100
|
- lib/evilution/memory/leak_check.rb
|
|
99
101
|
- lib/evilution/mutation.rb
|
|
100
102
|
- lib/evilution/mutator/base.rb
|
|
103
|
+
- lib/evilution/mutator/operator/argument_nil_substitution.rb
|
|
101
104
|
- lib/evilution/mutator/operator/argument_removal.rb
|
|
102
105
|
- lib/evilution/mutator/operator/arithmetic_replacement.rb
|
|
103
106
|
- lib/evilution/mutator/operator/array_literal.rb
|
|
@@ -117,14 +120,17 @@ files:
|
|
|
117
120
|
- lib/evilution/mutator/operator/negation_insertion.rb
|
|
118
121
|
- lib/evilution/mutator/operator/nil_replacement.rb
|
|
119
122
|
- lib/evilution/mutator/operator/range_replacement.rb
|
|
123
|
+
- lib/evilution/mutator/operator/receiver_replacement.rb
|
|
120
124
|
- lib/evilution/mutator/operator/regexp_mutation.rb
|
|
121
125
|
- lib/evilution/mutator/operator/return_value_removal.rb
|
|
126
|
+
- lib/evilution/mutator/operator/send_mutation.rb
|
|
122
127
|
- lib/evilution/mutator/operator/statement_deletion.rb
|
|
123
128
|
- lib/evilution/mutator/operator/string_literal.rb
|
|
124
129
|
- lib/evilution/mutator/operator/symbol_literal.rb
|
|
125
130
|
- lib/evilution/mutator/registry.rb
|
|
126
131
|
- lib/evilution/parallel/pool.rb
|
|
127
132
|
- lib/evilution/reporter/cli.rb
|
|
133
|
+
- lib/evilution/reporter/html.rb
|
|
128
134
|
- lib/evilution/reporter/json.rb
|
|
129
135
|
- lib/evilution/reporter/suggestion.rb
|
|
130
136
|
- lib/evilution/result/mutation_result.rb
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "coverage"
|
|
4
|
-
require "stringio"
|
|
5
|
-
|
|
6
|
-
module Evilution
|
|
7
|
-
module Coverage
|
|
8
|
-
class Collector
|
|
9
|
-
def call(test_files:)
|
|
10
|
-
read_io, write_io = IO.pipe
|
|
11
|
-
|
|
12
|
-
pid = ::Process.fork do
|
|
13
|
-
read_io.close
|
|
14
|
-
result = collect_coverage(test_files)
|
|
15
|
-
Marshal.dump(result, write_io)
|
|
16
|
-
write_io.close
|
|
17
|
-
exit!(0)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
write_io.close
|
|
21
|
-
data = read_io.read
|
|
22
|
-
read_io.close
|
|
23
|
-
::Process.wait(pid)
|
|
24
|
-
|
|
25
|
-
Marshal.load(data) # rubocop:disable Security/MarshalLoad
|
|
26
|
-
ensure
|
|
27
|
-
read_io&.close
|
|
28
|
-
write_io&.close
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
def collect_coverage(test_files)
|
|
34
|
-
::Coverage.start
|
|
35
|
-
return ::Coverage.result if test_files.empty?
|
|
36
|
-
|
|
37
|
-
require "rspec/core"
|
|
38
|
-
::RSpec.reset
|
|
39
|
-
::RSpec::Core::Runner.run(
|
|
40
|
-
["--format", "progress", "--no-color", "--order", "defined", *test_files],
|
|
41
|
-
StringIO.new, StringIO.new
|
|
42
|
-
)
|
|
43
|
-
::Coverage.result
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Evilution
|
|
4
|
-
module Coverage
|
|
5
|
-
class TestMap
|
|
6
|
-
def initialize(coverage_data)
|
|
7
|
-
@coverage_data = coverage_data
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# Returns true if the given source line was executed during tests.
|
|
11
|
-
# file_path should be an absolute path matching coverage data keys.
|
|
12
|
-
# line is 1-based (editor line numbers).
|
|
13
|
-
def covered?(file_path, line)
|
|
14
|
-
line_data = @coverage_data[file_path]
|
|
15
|
-
return false unless line_data
|
|
16
|
-
|
|
17
|
-
index = line - 1
|
|
18
|
-
return false if index.negative? || index >= line_data.length
|
|
19
|
-
|
|
20
|
-
count = line_data[index]
|
|
21
|
-
!count.nil? && count.positive?
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Evilution
|
|
4
|
-
module Diff
|
|
5
|
-
class FileFilter
|
|
6
|
-
# Filters subjects to only those whose methods overlap with changed lines.
|
|
7
|
-
#
|
|
8
|
-
# @param subjects [Array<Subject>] All extracted subjects
|
|
9
|
-
# @param changed_ranges [Array<Hash>] Output from Diff::Parser#parse
|
|
10
|
-
# @return [Array<Subject>] Subjects overlapping with changes
|
|
11
|
-
def filter(subjects, changed_ranges)
|
|
12
|
-
lookup = build_lookup(changed_ranges)
|
|
13
|
-
|
|
14
|
-
subjects.select do |subject|
|
|
15
|
-
ranges = lookup[subject.file_path]
|
|
16
|
-
next false unless ranges
|
|
17
|
-
|
|
18
|
-
ranges.any? { |range| range.cover?(subject.line_number) }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def build_lookup(changed_ranges)
|
|
25
|
-
changed_ranges.to_h { [_1[:file], _1[:lines]] }
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Evilution
|
|
4
|
-
module Diff
|
|
5
|
-
class Parser
|
|
6
|
-
# Parses git diff output to extract changed file paths and line ranges.
|
|
7
|
-
#
|
|
8
|
-
# @param diff_base [String] Git ref to diff against (e.g., "HEAD~1", "main")
|
|
9
|
-
# @return [Array<Hash>] Array of { file: String, lines: Array<Range> }
|
|
10
|
-
def parse(diff_base)
|
|
11
|
-
output = run_git_diff(diff_base)
|
|
12
|
-
parse_diff_output(output)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def run_git_diff(diff_base)
|
|
18
|
-
`git diff --unified=0 #{diff_base}..HEAD -- '*.rb' 2>/dev/null`
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def parse_diff_output(output)
|
|
22
|
-
result = {}
|
|
23
|
-
current_file = nil
|
|
24
|
-
|
|
25
|
-
output.each_line do |line|
|
|
26
|
-
case line
|
|
27
|
-
when %r{^diff --git a/.+ b/(.+)$}
|
|
28
|
-
current_file = Regexp.last_match(1)
|
|
29
|
-
when /^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@/
|
|
30
|
-
next unless current_file
|
|
31
|
-
|
|
32
|
-
start_line = Regexp.last_match(1).to_i
|
|
33
|
-
count = (Regexp.last_match(2) || "1").to_i
|
|
34
|
-
|
|
35
|
-
next if count.zero? # Pure deletion, no new lines
|
|
36
|
-
|
|
37
|
-
end_line = start_line + count - 1
|
|
38
|
-
result[current_file] ||= []
|
|
39
|
-
result[current_file] << (start_line..end_line)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
result.map { |file, lines| { file: file, lines: lines } }
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|