evilution 0.9.0 → 0.11.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 +15 -14
- data/CHANGELOG.md +34 -0
- data/README.md +12 -4
- data/lib/evilution/cli.rb +1 -10
- data/lib/evilution/config.rb +4 -24
- 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 +37 -6
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +42 -0
- data/lib/evilution/mutator/operator/arithmetic_replacement.rb +3 -1
- data/lib/evilution/mutator/operator/array_literal.rb +7 -0
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +15 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +9 -1
- data/lib/evilution/mutator/operator/comparison_replacement.rb +4 -4
- data/lib/evilution/mutator/operator/float_literal.rb +7 -0
- data/lib/evilution/mutator/operator/hash_literal.rb +7 -0
- data/lib/evilution/mutator/operator/integer_literal.rb +7 -0
- data/lib/evilution/mutator/operator/nil_replacement.rb +10 -6
- data/lib/evilution/mutator/operator/regexp_mutation.rb +11 -6
- data/lib/evilution/mutator/operator/send_mutation.rb +49 -0
- data/lib/evilution/mutator/operator/string_literal.rb +7 -0
- data/lib/evilution/mutator/operator/symbol_literal.rb +7 -0
- data/lib/evilution/mutator/registry.rb +3 -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 +32 -13
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +4 -4
- metadata +10 -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
|
@@ -40,8 +40,12 @@ module Evilution
|
|
|
40
40
|
results.count(&:neutral?)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def equivalent
|
|
44
|
+
results.count(&:equivalent?)
|
|
45
|
+
end
|
|
46
|
+
|
|
43
47
|
def score
|
|
44
|
-
denominator = total - errors - neutral
|
|
48
|
+
denominator = total - errors - neutral - equivalent
|
|
45
49
|
return 0.0 if denominator.zero?
|
|
46
50
|
|
|
47
51
|
killed.to_f / denominator
|
|
@@ -63,6 +67,10 @@ module Evilution
|
|
|
63
67
|
results.select(&:neutral?)
|
|
64
68
|
end
|
|
65
69
|
|
|
70
|
+
def equivalent_results
|
|
71
|
+
results.select(&:equivalent?)
|
|
72
|
+
end
|
|
73
|
+
|
|
66
74
|
def peak_memory_mb
|
|
67
75
|
max_rss = nil
|
|
68
76
|
results.each do |result|
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -9,9 +9,9 @@ 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"
|
|
@@ -37,13 +37,18 @@ module Evilution
|
|
|
37
37
|
subjects = parse_subjects
|
|
38
38
|
subjects = filter_by_target(subjects) if config.target?
|
|
39
39
|
subjects = filter_by_line_ranges(subjects) if config.line_ranges?
|
|
40
|
-
subjects = filter_by_diff(subjects) if config.diff?
|
|
41
40
|
log_memory("after parse_subjects", "#{subjects.length} subjects")
|
|
42
41
|
|
|
43
42
|
baseline_result = run_baseline(subjects)
|
|
44
43
|
|
|
45
44
|
mutations = generate_mutations(subjects)
|
|
45
|
+
equivalent_mutations, mutations = filter_equivalent(mutations)
|
|
46
|
+
release_subject_nodes(subjects)
|
|
46
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
|
|
47
52
|
log_memory("after run_mutations", "#{results.length} results")
|
|
48
53
|
|
|
49
54
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
@@ -91,20 +96,24 @@ module Evilution
|
|
|
91
96
|
end
|
|
92
97
|
end
|
|
93
98
|
|
|
94
|
-
def filter_by_diff(subjects)
|
|
95
|
-
diff_parser = Diff::Parser.new
|
|
96
|
-
changed_ranges = diff_parser.parse(config.diff_base)
|
|
97
|
-
Diff::FileFilter.new.filter(subjects, changed_ranges)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
99
|
def generate_mutations(subjects)
|
|
101
100
|
subjects.flat_map do |subject|
|
|
102
|
-
|
|
103
|
-
subject.release_node!
|
|
104
|
-
mutations
|
|
101
|
+
registry.mutations_for(subject)
|
|
105
102
|
end
|
|
106
103
|
end
|
|
107
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
|
+
|
|
108
117
|
def run_baseline(subjects)
|
|
109
118
|
return nil unless config.baseline? && subjects.any?
|
|
110
119
|
|
|
@@ -281,7 +290,15 @@ module Evilution
|
|
|
281
290
|
return unless reporter
|
|
282
291
|
|
|
283
292
|
output = reporter.call(summary)
|
|
284
|
-
|
|
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
|
|
285
302
|
end
|
|
286
303
|
|
|
287
304
|
def log_baseline_start
|
|
@@ -350,6 +367,8 @@ module Evilution
|
|
|
350
367
|
Reporter::JSON.new
|
|
351
368
|
when :text
|
|
352
369
|
Reporter::CLI.new
|
|
370
|
+
when :html
|
|
371
|
+
Reporter::HTML.new
|
|
353
372
|
end
|
|
354
373
|
end
|
|
355
374
|
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -33,12 +33,13 @@ 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
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"
|
|
36
38
|
require_relative "evilution/mutator/registry"
|
|
39
|
+
require_relative "evilution/equivalent/detector"
|
|
37
40
|
require_relative "evilution/isolation/fork"
|
|
38
41
|
require_relative "evilution/isolation/in_process"
|
|
39
42
|
require_relative "evilution/parallel/pool"
|
|
40
|
-
require_relative "evilution/diff/parser"
|
|
41
|
-
require_relative "evilution/diff/file_filter"
|
|
42
43
|
require_relative "evilution/git/changed_files"
|
|
43
44
|
require_relative "evilution/integration/base"
|
|
44
45
|
require_relative "evilution/integration/rspec"
|
|
@@ -46,9 +47,8 @@ require_relative "evilution/result/mutation_result"
|
|
|
46
47
|
require_relative "evilution/result/summary"
|
|
47
48
|
require_relative "evilution/reporter/json"
|
|
48
49
|
require_relative "evilution/reporter/cli"
|
|
50
|
+
require_relative "evilution/reporter/html"
|
|
49
51
|
require_relative "evilution/reporter/suggestion"
|
|
50
|
-
require_relative "evilution/coverage/collector"
|
|
51
|
-
require_relative "evilution/coverage/test_map"
|
|
52
52
|
require_relative "evilution/spec_resolver"
|
|
53
53
|
require_relative "evilution/baseline"
|
|
54
54
|
require_relative "evilution/cache"
|
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.11.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-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -84,10 +84,11 @@ files:
|
|
|
84
84
|
- lib/evilution/cache.rb
|
|
85
85
|
- lib/evilution/cli.rb
|
|
86
86
|
- lib/evilution/config.rb
|
|
87
|
-
- lib/evilution/
|
|
88
|
-
- lib/evilution/
|
|
89
|
-
- lib/evilution/
|
|
90
|
-
- 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
|
|
91
92
|
- lib/evilution/git/changed_files.rb
|
|
92
93
|
- lib/evilution/integration/base.rb
|
|
93
94
|
- lib/evilution/integration/rspec.rb
|
|
@@ -99,6 +100,7 @@ files:
|
|
|
99
100
|
- lib/evilution/memory/leak_check.rb
|
|
100
101
|
- lib/evilution/mutation.rb
|
|
101
102
|
- lib/evilution/mutator/base.rb
|
|
103
|
+
- lib/evilution/mutator/operator/argument_nil_substitution.rb
|
|
102
104
|
- lib/evilution/mutator/operator/argument_removal.rb
|
|
103
105
|
- lib/evilution/mutator/operator/arithmetic_replacement.rb
|
|
104
106
|
- lib/evilution/mutator/operator/array_literal.rb
|
|
@@ -121,12 +123,14 @@ files:
|
|
|
121
123
|
- lib/evilution/mutator/operator/receiver_replacement.rb
|
|
122
124
|
- lib/evilution/mutator/operator/regexp_mutation.rb
|
|
123
125
|
- lib/evilution/mutator/operator/return_value_removal.rb
|
|
126
|
+
- lib/evilution/mutator/operator/send_mutation.rb
|
|
124
127
|
- lib/evilution/mutator/operator/statement_deletion.rb
|
|
125
128
|
- lib/evilution/mutator/operator/string_literal.rb
|
|
126
129
|
- lib/evilution/mutator/operator/symbol_literal.rb
|
|
127
130
|
- lib/evilution/mutator/registry.rb
|
|
128
131
|
- lib/evilution/parallel/pool.rb
|
|
129
132
|
- lib/evilution/reporter/cli.rb
|
|
133
|
+
- lib/evilution/reporter/html.rb
|
|
130
134
|
- lib/evilution/reporter/json.rb
|
|
131
135
|
- lib/evilution/reporter/suggestion.rb
|
|
132
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
|