evilution 0.19.0 → 0.21.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 +35 -35
- data/CHANGELOG.md +36 -0
- data/README.md +25 -4
- data/lib/evilution/cli.rb +11 -2
- data/lib/evilution/config.rb +12 -2
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +2 -1
- data/lib/evilution/equivalent/heuristic/void_context.rb +77 -0
- data/lib/evilution/integration/crash_detector.rb +55 -0
- data/lib/evilution/integration/rspec.rb +64 -29
- data/lib/evilution/isolation/fork.rb +3 -6
- data/lib/evilution/mutator/base.rb +1 -1
- data/lib/evilution/mutator/operator/begin_unwrap.rb +21 -0
- data/lib/evilution/mutator/operator/block_param_removal.rb +57 -0
- data/lib/evilution/mutator/operator/case_when.rb +55 -0
- data/lib/evilution/mutator/operator/equality_to_identity.rb +22 -0
- data/lib/evilution/mutator/operator/index_to_dig.rb +1 -1
- data/lib/evilution/mutator/operator/lambda_body.rb +18 -0
- data/lib/evilution/mutator/operator/loop_flip.rb +27 -0
- data/lib/evilution/mutator/operator/method_body_replacement.rb +10 -6
- data/lib/evilution/mutator/operator/predicate_replacement.rb +27 -0
- data/lib/evilution/mutator/operator/retry_removal.rb +16 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +8 -1
- data/lib/evilution/mutator/operator/string_interpolation.rb +32 -0
- data/lib/evilution/mutator/operator/string_literal.rb +18 -0
- data/lib/evilution/mutator/registry.rb +19 -3
- data/lib/evilution/related_spec_heuristic.rb +63 -0
- data/lib/evilution/reporter/cli.rb +14 -8
- data/lib/evilution/reporter/html.rb +32 -2
- data/lib/evilution/reporter/json.rb +14 -0
- data/lib/evilution/result/coverage_gap.rb +35 -0
- data/lib/evilution/result/coverage_gap_grouper.rb +22 -0
- data/lib/evilution/result/summary.rb +5 -0
- data/lib/evilution/runner.rb +28 -1
- data/lib/evilution/session/store.rb +13 -0
- data/lib/evilution/temp_dir_tracker.rb +39 -0
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +9 -0
- metadata +17 -2
data/lib/evilution/config.rb
CHANGED
|
@@ -25,14 +25,16 @@ class Evilution::Config
|
|
|
25
25
|
spec_files: [],
|
|
26
26
|
ignore_patterns: [],
|
|
27
27
|
show_disabled: false,
|
|
28
|
-
baseline_session: nil
|
|
28
|
+
baseline_session: nil,
|
|
29
|
+
skip_heredoc_literals: false
|
|
29
30
|
}.freeze
|
|
30
31
|
|
|
31
32
|
attr_reader :target_files, :timeout, :format,
|
|
32
33
|
:target, :min_score, :integration, :verbose, :quiet,
|
|
33
34
|
:jobs, :fail_fast, :baseline, :isolation, :incremental, :suggest_tests,
|
|
34
35
|
:progress, :save_session, :line_ranges, :spec_files, :hooks,
|
|
35
|
-
:ignore_patterns, :show_disabled, :baseline_session
|
|
36
|
+
:ignore_patterns, :show_disabled, :baseline_session,
|
|
37
|
+
:skip_heredoc_literals
|
|
36
38
|
|
|
37
39
|
def initialize(**options)
|
|
38
40
|
file_options = options.delete(:skip_config_file) ? {} : load_config_file
|
|
@@ -89,6 +91,10 @@ class Evilution::Config
|
|
|
89
91
|
show_disabled
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
def skip_heredoc_literals?
|
|
95
|
+
skip_heredoc_literals
|
|
96
|
+
end
|
|
97
|
+
|
|
92
98
|
def self.file_options
|
|
93
99
|
CONFIG_FILES.each do |path|
|
|
94
100
|
next unless File.exist?(path)
|
|
@@ -131,6 +137,9 @@ class Evilution::Config
|
|
|
131
137
|
# Generate concrete RSpec test code in suggestions (default: false)
|
|
132
138
|
# suggest_tests: false
|
|
133
139
|
|
|
140
|
+
# Skip all string literal mutations inside heredocs (default: false)
|
|
141
|
+
# skip_heredoc_literals: false
|
|
142
|
+
|
|
134
143
|
# Hooks: Ruby files returning a Proc, keyed by lifecycle event
|
|
135
144
|
# hooks:
|
|
136
145
|
# worker_process_start: config/evilution_hooks/worker_start.rb
|
|
@@ -179,6 +188,7 @@ class Evilution::Config
|
|
|
179
188
|
@ignore_patterns = validate_ignore_patterns(merged[:ignore_patterns])
|
|
180
189
|
@show_disabled = merged[:show_disabled]
|
|
181
190
|
@baseline_session = merged[:baseline_session]
|
|
191
|
+
@skip_heredoc_literals = merged[:skip_heredoc_literals]
|
|
182
192
|
@hooks = validate_hooks(merged[:hooks])
|
|
183
193
|
end
|
|
184
194
|
|
|
@@ -6,6 +6,7 @@ require_relative "heuristic/alias_swap"
|
|
|
6
6
|
require_relative "heuristic/dead_code"
|
|
7
7
|
require_relative "heuristic/arithmetic_identity"
|
|
8
8
|
require_relative "heuristic/comment_marking"
|
|
9
|
+
require_relative "heuristic/void_context"
|
|
9
10
|
|
|
10
11
|
require_relative "../equivalent"
|
|
11
12
|
|
|
@@ -38,7 +39,8 @@ class Evilution::Equivalent::Detector
|
|
|
38
39
|
Evilution::Equivalent::Heuristic::AliasSwap.new,
|
|
39
40
|
Evilution::Equivalent::Heuristic::DeadCode.new,
|
|
40
41
|
Evilution::Equivalent::Heuristic::ArithmeticIdentity.new,
|
|
41
|
-
Evilution::Equivalent::Heuristic::CommentMarking.new
|
|
42
|
+
Evilution::Equivalent::Heuristic::CommentMarking.new,
|
|
43
|
+
Evilution::Equivalent::Heuristic::VoidContext.new
|
|
42
44
|
]
|
|
43
45
|
end
|
|
44
46
|
end
|
|
@@ -7,7 +7,8 @@ class Evilution::Equivalent::Heuristic::AliasSwap
|
|
|
7
7
|
Set[:detect, :find],
|
|
8
8
|
Set[:length, :size],
|
|
9
9
|
Set[:collect, :map],
|
|
10
|
-
Set[:count, :length]
|
|
10
|
+
Set[:count, :length],
|
|
11
|
+
Set[:count, :size]
|
|
11
12
|
].freeze
|
|
12
13
|
|
|
13
14
|
MATCHING_OPERATORS = Set["send_mutation", "collection_replacement"].freeze
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../heuristic"
|
|
4
|
+
|
|
5
|
+
class Evilution::Equivalent::Heuristic::VoidContext
|
|
6
|
+
# Method pairs where the only difference is the return value.
|
|
7
|
+
# In void context (return value unused), these are equivalent.
|
|
8
|
+
VOID_EQUIVALENT_PAIRS = Set[
|
|
9
|
+
Set[:each, :map],
|
|
10
|
+
Set[:each, :reverse_each]
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
MATCHING_OPERATORS = Set["send_mutation", "collection_replacement"].freeze
|
|
14
|
+
|
|
15
|
+
def match?(mutation)
|
|
16
|
+
return false unless MATCHING_OPERATORS.include?(mutation.operator_name)
|
|
17
|
+
|
|
18
|
+
pair = extract_method_pair(mutation.diff)
|
|
19
|
+
return false unless pair
|
|
20
|
+
return false unless VOID_EQUIVALENT_PAIRS.include?(pair)
|
|
21
|
+
|
|
22
|
+
void_context?(mutation)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def extract_method_pair(diff)
|
|
28
|
+
removed = extract_method(diff, "- ")
|
|
29
|
+
added = extract_method(diff, "+ ")
|
|
30
|
+
return nil unless removed && added
|
|
31
|
+
|
|
32
|
+
Set[removed.to_sym, added.to_sym]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def extract_method(diff, prefix)
|
|
36
|
+
line = diff.split("\n").find { |l| l.start_with?(prefix) }
|
|
37
|
+
return nil unless line
|
|
38
|
+
|
|
39
|
+
match = line.match(/\.(\w+)(?:[\s({]|$)/)
|
|
40
|
+
match && match[1]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def void_context?(mutation)
|
|
44
|
+
node = mutation.subject.node
|
|
45
|
+
return false unless node
|
|
46
|
+
|
|
47
|
+
body = node.body
|
|
48
|
+
return false unless body.is_a?(Prism::StatementsNode)
|
|
49
|
+
|
|
50
|
+
statements = body.body
|
|
51
|
+
call_node = find_call_at_line(statements, mutation.line)
|
|
52
|
+
return false unless call_node
|
|
53
|
+
|
|
54
|
+
# The call is in void context if:
|
|
55
|
+
# 1. It's a direct statement (not wrapped in assignment)
|
|
56
|
+
# 2. It's not the last statement in the method body
|
|
57
|
+
statement_index = statements.index { |s| contains_line?(s, mutation.line) && direct_call?(s) }
|
|
58
|
+
return false unless statement_index
|
|
59
|
+
|
|
60
|
+
statement_index < statements.length - 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def find_call_at_line(statements, line)
|
|
64
|
+
statements.each do |stmt|
|
|
65
|
+
return stmt if stmt.is_a?(Prism::CallNode) && stmt.location.start_line == line
|
|
66
|
+
end
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def direct_call?(statement)
|
|
71
|
+
statement.is_a?(Prism::CallNode)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def contains_line?(node, line)
|
|
75
|
+
line.between?(node.location.start_line, node.location.end_line)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../integration"
|
|
4
|
+
|
|
5
|
+
class Evilution::Integration::CrashDetector
|
|
6
|
+
def self.register_with_rspec
|
|
7
|
+
::RSpec::Core::Formatters.register self, :example_failed
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(_output)
|
|
11
|
+
reset
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reset
|
|
15
|
+
@assertion_failures = 0
|
|
16
|
+
@crashes = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def example_failed(notification)
|
|
20
|
+
exception = notification.example.exception
|
|
21
|
+
|
|
22
|
+
if assertion_exception?(exception)
|
|
23
|
+
@assertion_failures += 1
|
|
24
|
+
else
|
|
25
|
+
@crashes << exception
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def has_assertion_failure? # rubocop:disable Naming/PredicatePrefix
|
|
30
|
+
@assertion_failures.positive?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def has_crash? # rubocop:disable Naming/PredicatePrefix
|
|
34
|
+
@crashes.any?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def only_crashes?
|
|
38
|
+
@crashes.any? && @assertion_failures.zero?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def crash_summary
|
|
42
|
+
return nil if @crashes.empty?
|
|
43
|
+
|
|
44
|
+
types = @crashes.map { |e| e.class.name }.uniq
|
|
45
|
+
"#{types.join(", ")} (#{@crashes.length} crash#{"es" unless @crashes.length == 1})"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def assertion_exception?(exception)
|
|
51
|
+
exception.is_a?(::RSpec::Expectations::ExpectationNotMetError) ||
|
|
52
|
+
(defined?(::RSpec::Mocks::MockExpectationError) &&
|
|
53
|
+
exception.is_a?(::RSpec::Mocks::MockExpectationError))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -4,7 +4,10 @@ require "fileutils"
|
|
|
4
4
|
require "stringio"
|
|
5
5
|
require "tmpdir"
|
|
6
6
|
require_relative "base"
|
|
7
|
+
require_relative "crash_detector"
|
|
7
8
|
require_relative "../spec_resolver"
|
|
9
|
+
require_relative "../related_spec_heuristic"
|
|
10
|
+
require_relative "../temp_dir_tracker"
|
|
8
11
|
|
|
9
12
|
require_relative "../integration"
|
|
10
13
|
|
|
@@ -13,14 +16,14 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
13
16
|
@test_files = test_files
|
|
14
17
|
@rspec_loaded = false
|
|
15
18
|
@spec_resolver = Evilution::SpecResolver.new
|
|
19
|
+
@related_spec_heuristic = Evilution::RelatedSpecHeuristic.new
|
|
20
|
+
@crash_detector = nil
|
|
16
21
|
@warned_files = Set.new
|
|
17
22
|
super(hooks: hooks)
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
def call(mutation)
|
|
21
|
-
@original_content = nil
|
|
22
26
|
@temp_dir = nil
|
|
23
|
-
@lock_file = nil
|
|
24
27
|
ensure_rspec_loaded
|
|
25
28
|
@hooks.fire(:mutation_insert_pre, mutation: mutation, file_path: mutation.file_path) if @hooks
|
|
26
29
|
apply_mutation(mutation)
|
|
@@ -39,6 +42,7 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
39
42
|
|
|
40
43
|
@hooks.fire(:setup_integration_pre, integration: :rspec) if @hooks
|
|
41
44
|
require "rspec/core"
|
|
45
|
+
Evilution::Integration::CrashDetector.register_with_rspec
|
|
42
46
|
@rspec_loaded = true
|
|
43
47
|
@hooks.fire(:setup_integration_post, integration: :rspec) if @hooks
|
|
44
48
|
rescue LoadError => e
|
|
@@ -46,51 +50,60 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
def apply_mutation(mutation)
|
|
53
|
+
@temp_dir = Dir.mktmpdir("evilution")
|
|
54
|
+
Evilution::TempDirTracker.register(@temp_dir)
|
|
55
|
+
@displaced_feature = nil
|
|
49
56
|
subpath = resolve_require_subpath(mutation.file_path)
|
|
50
57
|
|
|
51
58
|
if subpath
|
|
52
|
-
@temp_dir = Dir.mktmpdir("evilution")
|
|
53
59
|
dest = File.join(@temp_dir, subpath)
|
|
54
60
|
FileUtils.mkdir_p(File.dirname(dest))
|
|
55
61
|
File.write(dest, mutation.mutated_source)
|
|
56
62
|
$LOAD_PATH.unshift(@temp_dir)
|
|
63
|
+
displace_loaded_feature(mutation.file_path)
|
|
57
64
|
else
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@original_content = File.read(mutation.file_path)
|
|
64
|
-
File.write(mutation.file_path, mutation.mutated_source)
|
|
65
|
+
absolute = File.expand_path(mutation.file_path)
|
|
66
|
+
dest = File.join(@temp_dir, absolute)
|
|
67
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
68
|
+
File.write(dest, mutation.mutated_source)
|
|
69
|
+
load(dest)
|
|
65
70
|
end
|
|
66
71
|
end
|
|
67
72
|
|
|
68
|
-
def restore_original(mutation)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@lock_file = nil
|
|
79
|
-
end
|
|
73
|
+
def restore_original(mutation) # rubocop:disable Lint/UnusedMethodArgument
|
|
74
|
+
return unless @temp_dir
|
|
75
|
+
|
|
76
|
+
$LOAD_PATH.delete(@temp_dir)
|
|
77
|
+
$LOADED_FEATURES.reject! { |f| f.start_with?(@temp_dir) }
|
|
78
|
+
$LOADED_FEATURES << @displaced_feature if @displaced_feature && !$LOADED_FEATURES.include?(@displaced_feature)
|
|
79
|
+
@displaced_feature = nil
|
|
80
|
+
FileUtils.rm_rf(@temp_dir)
|
|
81
|
+
Evilution::TempDirTracker.unregister(@temp_dir)
|
|
82
|
+
@temp_dir = nil
|
|
80
83
|
end
|
|
81
84
|
|
|
82
85
|
def resolve_require_subpath(file_path)
|
|
83
86
|
absolute = File.expand_path(file_path)
|
|
87
|
+
best_subpath = nil
|
|
84
88
|
|
|
85
89
|
$LOAD_PATH.each do |entry|
|
|
86
90
|
dir = File.expand_path(entry)
|
|
87
91
|
prefix = dir.end_with?("/") ? dir : "#{dir}/"
|
|
88
92
|
next unless absolute.start_with?(prefix)
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
candidate = absolute.delete_prefix(prefix)
|
|
95
|
+
best_subpath = candidate if best_subpath.nil? || candidate.length < best_subpath.length
|
|
91
96
|
end
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
best_subpath
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def displace_loaded_feature(file_path)
|
|
102
|
+
absolute = File.expand_path(file_path)
|
|
103
|
+
return unless $LOADED_FEATURES.include?(absolute)
|
|
104
|
+
|
|
105
|
+
@displaced_feature = absolute
|
|
106
|
+
$LOADED_FEATURES.delete(absolute)
|
|
94
107
|
end
|
|
95
108
|
|
|
96
109
|
def run_rspec(mutation)
|
|
@@ -115,10 +128,11 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
115
128
|
args = build_args(mutation)
|
|
116
129
|
command = "rspec #{args.join(" ")}"
|
|
117
130
|
|
|
131
|
+
detector = reset_crash_detector
|
|
118
132
|
eg_before = snapshot_example_groups
|
|
119
133
|
status = ::RSpec::Core::Runner.run(args, out, err)
|
|
120
134
|
|
|
121
|
-
|
|
135
|
+
build_rspec_result(status, command, detector)
|
|
122
136
|
rescue StandardError => e
|
|
123
137
|
{ passed: false, error: e.message, test_command: command }
|
|
124
138
|
ensure
|
|
@@ -171,6 +185,26 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
171
185
|
world.instance_variable_set(:@sources_by_path, {}) if world.instance_variable_defined?(:@sources_by_path)
|
|
172
186
|
end
|
|
173
187
|
|
|
188
|
+
def reset_crash_detector
|
|
189
|
+
if @crash_detector
|
|
190
|
+
@crash_detector.reset
|
|
191
|
+
else
|
|
192
|
+
@crash_detector = Evilution::Integration::CrashDetector.new(StringIO.new)
|
|
193
|
+
::RSpec.configuration.add_formatter(@crash_detector)
|
|
194
|
+
end
|
|
195
|
+
@crash_detector
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def build_rspec_result(status, command, detector)
|
|
199
|
+
if status.zero?
|
|
200
|
+
{ passed: true, test_command: command }
|
|
201
|
+
elsif detector.only_crashes?
|
|
202
|
+
{ passed: false, error: "test crashes: #{detector.crash_summary}", test_command: command }
|
|
203
|
+
else
|
|
204
|
+
{ passed: false, test_command: command }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
174
208
|
def build_args(mutation)
|
|
175
209
|
files = resolve_test_files(mutation)
|
|
176
210
|
["--format", "progress", "--no-color", "--order", "defined", *files]
|
|
@@ -180,12 +214,13 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
180
214
|
return test_files if test_files
|
|
181
215
|
|
|
182
216
|
resolved = @spec_resolver.call(mutation.file_path)
|
|
183
|
-
|
|
184
|
-
[resolved]
|
|
185
|
-
else
|
|
217
|
+
unless resolved
|
|
186
218
|
warn_unresolved_spec(mutation.file_path)
|
|
187
|
-
["spec"]
|
|
219
|
+
return ["spec"]
|
|
188
220
|
end
|
|
221
|
+
|
|
222
|
+
related = @related_spec_heuristic.call(mutation)
|
|
223
|
+
([resolved] + related).uniq
|
|
189
224
|
end
|
|
190
225
|
|
|
191
226
|
def warn_unresolved_spec(file_path)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require_relative "../memory"
|
|
6
|
+
require_relative "../temp_dir_tracker"
|
|
6
7
|
|
|
7
8
|
require_relative "../isolation"
|
|
8
9
|
|
|
@@ -44,12 +45,8 @@ class Evilution::Isolation::Fork
|
|
|
44
45
|
|
|
45
46
|
private
|
|
46
47
|
|
|
47
|
-
def restore_original_source(mutation)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
File.write(mutation.file_path, mutation.original_source)
|
|
51
|
-
rescue StandardError => e
|
|
52
|
-
warn("Warning: failed to restore #{mutation.file_path}: #{e.message}")
|
|
48
|
+
def restore_original_source(mutation) # rubocop:disable Lint/UnusedMethodArgument
|
|
49
|
+
Evilution::TempDirTracker.cleanup_all
|
|
53
50
|
end
|
|
54
51
|
|
|
55
52
|
def suppress_child_output
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::BeginUnwrap < Evilution::Mutator::Base
|
|
6
|
+
def visit_begin_node(node)
|
|
7
|
+
return super if node.rescue_clause || node.else_clause || node.ensure_clause
|
|
8
|
+
return super if node.statements.nil?
|
|
9
|
+
return super if node.begin_keyword_loc.nil?
|
|
10
|
+
|
|
11
|
+
body_text = @file_source.byteslice(node.statements.location.start_offset, node.statements.location.length)
|
|
12
|
+
add_mutation(
|
|
13
|
+
offset: node.location.start_offset,
|
|
14
|
+
length: node.location.length,
|
|
15
|
+
replacement: body_text,
|
|
16
|
+
node: node
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::BlockParamRemoval < Evilution::Mutator::Base
|
|
6
|
+
def visit_def_node(node)
|
|
7
|
+
return super unless node.parameters
|
|
8
|
+
return super unless node.parameters.block
|
|
9
|
+
|
|
10
|
+
if only_block_param?(node.parameters)
|
|
11
|
+
remove_entire_params(node)
|
|
12
|
+
else
|
|
13
|
+
remove_block_param(node)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def only_block_param?(params)
|
|
22
|
+
params.requireds.empty? &&
|
|
23
|
+
params.optionals.empty? &&
|
|
24
|
+
params.keywords.empty? &&
|
|
25
|
+
params.rest.nil? &&
|
|
26
|
+
params.keyword_rest.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def remove_entire_params(node)
|
|
30
|
+
start_offset = node.lparen_loc.start_offset
|
|
31
|
+
end_offset = node.rparen_loc.start_offset + node.rparen_loc.length
|
|
32
|
+
add_mutation(
|
|
33
|
+
offset: start_offset,
|
|
34
|
+
length: end_offset - start_offset,
|
|
35
|
+
replacement: "",
|
|
36
|
+
node: node
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def remove_block_param(node)
|
|
41
|
+
block_loc = node.parameters.block.location
|
|
42
|
+
params_text = @file_source.byteslice(node.parameters.location.start_offset, node.parameters.location.length)
|
|
43
|
+
block_rel = block_loc.start_offset - node.parameters.location.start_offset
|
|
44
|
+
|
|
45
|
+
# Find the comma before the block param and remove ", &block"
|
|
46
|
+
comma_pos = params_text.rindex(",", block_rel - 1)
|
|
47
|
+
remove_start = node.parameters.location.start_offset + comma_pos
|
|
48
|
+
remove_end = block_loc.start_offset + block_loc.length
|
|
49
|
+
|
|
50
|
+
add_mutation(
|
|
51
|
+
offset: remove_start,
|
|
52
|
+
length: remove_end - remove_start,
|
|
53
|
+
replacement: "",
|
|
54
|
+
node: node
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::CaseWhen < Evilution::Mutator::Base
|
|
6
|
+
def visit_case_node(node)
|
|
7
|
+
remove_when_branches(node)
|
|
8
|
+
replace_when_bodies(node)
|
|
9
|
+
remove_else_branch(node)
|
|
10
|
+
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def remove_when_branches(node)
|
|
17
|
+
return if node.conditions.length < 2
|
|
18
|
+
|
|
19
|
+
node.conditions.each do |when_node|
|
|
20
|
+
add_mutation(
|
|
21
|
+
offset: when_node.location.start_offset,
|
|
22
|
+
length: when_node.location.length,
|
|
23
|
+
replacement: "",
|
|
24
|
+
node: when_node
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def replace_when_bodies(node)
|
|
30
|
+
node.conditions.each do |when_node|
|
|
31
|
+
next if when_node.statements.nil? || when_node.statements.body.empty?
|
|
32
|
+
|
|
33
|
+
add_mutation(
|
|
34
|
+
offset: when_node.statements.location.start_offset,
|
|
35
|
+
length: when_node.statements.location.length,
|
|
36
|
+
replacement: "nil",
|
|
37
|
+
node: when_node
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def remove_else_branch(node)
|
|
43
|
+
return if node.else_clause.nil?
|
|
44
|
+
return if node.else_clause.statements.nil?
|
|
45
|
+
|
|
46
|
+
start_offset = node.else_clause.else_keyword_loc.start_offset
|
|
47
|
+
end_offset = node.else_clause.statements.location.start_offset + node.else_clause.statements.location.length
|
|
48
|
+
add_mutation(
|
|
49
|
+
offset: start_offset,
|
|
50
|
+
length: end_offset - start_offset,
|
|
51
|
+
replacement: "",
|
|
52
|
+
node: node.else_clause
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::EqualityToIdentity < Evilution::Mutator::Base
|
|
6
|
+
def visit_call_node(node)
|
|
7
|
+
if node.name == :== && node.receiver && node.arguments
|
|
8
|
+
receiver_text = @file_source.byteslice(node.receiver.location.start_offset, node.receiver.location.length)
|
|
9
|
+
arg = node.arguments.arguments.first
|
|
10
|
+
arg_text = @file_source.byteslice(arg.location.start_offset, arg.location.length)
|
|
11
|
+
|
|
12
|
+
add_mutation(
|
|
13
|
+
offset: node.location.start_offset,
|
|
14
|
+
length: node.location.length,
|
|
15
|
+
replacement: "#{receiver_text}.equal?(#{arg_text})",
|
|
16
|
+
node: node
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::LambdaBody < Evilution::Mutator::Base
|
|
6
|
+
def visit_lambda_node(node)
|
|
7
|
+
if node.body
|
|
8
|
+
add_mutation(
|
|
9
|
+
offset: node.body.location.start_offset,
|
|
10
|
+
length: node.body.location.length,
|
|
11
|
+
replacement: "nil",
|
|
12
|
+
node: node
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::LoopFlip < Evilution::Mutator::Base
|
|
6
|
+
def visit_while_node(node)
|
|
7
|
+
add_mutation(
|
|
8
|
+
offset: node.keyword_loc.start_offset,
|
|
9
|
+
length: node.keyword_loc.length,
|
|
10
|
+
replacement: "until",
|
|
11
|
+
node: node
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def visit_until_node(node)
|
|
18
|
+
add_mutation(
|
|
19
|
+
offset: node.keyword_loc.start_offset,
|
|
20
|
+
length: node.keyword_loc.length,
|
|
21
|
+
replacement: "while",
|
|
22
|
+
node: node
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::Base
|
|
6
|
+
REPLACEMENTS = %w[nil self super].freeze
|
|
7
|
+
|
|
6
8
|
def visit_def_node(node)
|
|
7
9
|
if node.body
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
REPLACEMENTS.each do |replacement|
|
|
11
|
+
add_mutation(
|
|
12
|
+
offset: node.body.location.start_offset,
|
|
13
|
+
length: node.body.location.length,
|
|
14
|
+
replacement: replacement,
|
|
15
|
+
node: node
|
|
16
|
+
)
|
|
17
|
+
end
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
super
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::PredicateReplacement < Evilution::Mutator::Base
|
|
6
|
+
def visit_call_node(node)
|
|
7
|
+
if node.name.to_s.end_with?("?")
|
|
8
|
+
loc = node.location
|
|
9
|
+
|
|
10
|
+
add_mutation(
|
|
11
|
+
offset: loc.start_offset,
|
|
12
|
+
length: loc.length,
|
|
13
|
+
replacement: "true",
|
|
14
|
+
node: node
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
add_mutation(
|
|
18
|
+
offset: loc.start_offset,
|
|
19
|
+
length: loc.length,
|
|
20
|
+
replacement: "false",
|
|
21
|
+
node: node
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|