evilution 0.12.0 → 0.14.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 +127 -0
- data/CHANGELOG.md +29 -0
- data/lib/evilution/ast/parser.rb +69 -68
- data/lib/evilution/ast/source_surgeon.rb +7 -9
- data/lib/evilution/ast.rb +4 -0
- data/lib/evilution/baseline.rb +73 -75
- data/lib/evilution/cache.rb +75 -77
- data/lib/evilution/cli.rb +408 -173
- data/lib/evilution/config.rb +141 -136
- data/lib/evilution/equivalent/detector.rb +25 -27
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +29 -33
- data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
- data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
- data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
- data/lib/evilution/equivalent/heuristic.rb +6 -0
- data/lib/evilution/equivalent.rb +4 -0
- data/lib/evilution/git/changed_files.rb +35 -37
- data/lib/evilution/git.rb +4 -0
- data/lib/evilution/integration/base.rb +5 -7
- data/lib/evilution/integration/rspec.rb +114 -116
- data/lib/evilution/integration.rb +4 -0
- data/lib/evilution/isolation/fork.rb +98 -100
- data/lib/evilution/isolation/in_process.rb +59 -61
- data/lib/evilution/isolation.rb +4 -0
- data/lib/evilution/mcp/mutate_tool.rb +172 -143
- data/lib/evilution/mcp/server.rb +12 -11
- data/lib/evilution/mcp/session_diff_tool.rb +89 -0
- data/lib/evilution/mcp/session_list_tool.rb +46 -0
- data/lib/evilution/mcp/session_show_tool.rb +53 -0
- data/lib/evilution/mcp.rb +4 -0
- data/lib/evilution/memory/leak_check.rb +80 -84
- data/lib/evilution/memory.rb +34 -36
- data/lib/evilution/mutation.rb +40 -42
- data/lib/evilution/mutator/base.rb +46 -48
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
- data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
- data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
- data/lib/evilution/mutator/operator/array_literal.rb +18 -22
- data/lib/evilution/mutator/operator/block_removal.rb +16 -20
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
- data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
- data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
- data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
- data/lib/evilution/mutator/operator/compound_assignment.rb +119 -0
- data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
- data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
- data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
- data/lib/evilution/mutator/operator/float_literal.rb +22 -26
- data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
- data/lib/evilution/mutator/operator/integer_literal.rb +18 -44
- data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
- data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
- data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
- data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
- data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
- data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
- data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
- data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
- data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
- data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
- data/lib/evilution/mutator/operator/string_literal.rb +18 -22
- data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
- data/lib/evilution/mutator/operator.rb +6 -0
- data/lib/evilution/mutator/registry.rb +54 -55
- data/lib/evilution/mutator.rb +4 -0
- data/lib/evilution/parallel/pool.rb +56 -58
- data/lib/evilution/parallel.rb +4 -0
- data/lib/evilution/reporter/cli.rb +99 -101
- data/lib/evilution/reporter/html.rb +242 -244
- data/lib/evilution/reporter/json.rb +57 -59
- data/lib/evilution/reporter/suggestion.rb +326 -313
- data/lib/evilution/reporter.rb +4 -0
- data/lib/evilution/result/mutation_result.rb +43 -46
- data/lib/evilution/result/summary.rb +80 -81
- data/lib/evilution/result.rb +4 -0
- data/lib/evilution/runner.rb +334 -323
- data/lib/evilution/session/store.rb +147 -0
- data/lib/evilution/session.rb +4 -0
- data/lib/evilution/spec_resolver.rb +49 -47
- data/lib/evilution/subject.rb +14 -16
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +14 -0
- metadata +20 -2
data/lib/evilution/runner.rb
CHANGED
|
@@ -18,416 +18,427 @@ require_relative "result/summary"
|
|
|
18
18
|
require_relative "baseline"
|
|
19
19
|
require_relative "cache"
|
|
20
20
|
require_relative "parallel/pool"
|
|
21
|
+
require_relative "session/store"
|
|
22
|
+
|
|
23
|
+
class Evilution::Runner
|
|
24
|
+
attr_reader :config
|
|
25
|
+
|
|
26
|
+
def initialize(config: Evilution::Config.new, on_result: nil)
|
|
27
|
+
@config = config
|
|
28
|
+
@on_result = on_result
|
|
29
|
+
@parser = Evilution::AST::Parser.new
|
|
30
|
+
@registry = Evilution::Mutator::Registry.default
|
|
31
|
+
@isolator = build_isolator
|
|
32
|
+
@cache = config.incremental? ? Evilution::Cache.new : nil
|
|
33
|
+
end
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
attr_reader :config
|
|
35
|
+
def call
|
|
36
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@isolator = build_isolator
|
|
31
|
-
@cache = config.incremental? ? Cache.new : nil
|
|
32
|
-
end
|
|
38
|
+
subjects = parse_subjects
|
|
39
|
+
subjects = filter_by_target(subjects) if config.target?
|
|
40
|
+
subjects = filter_by_line_ranges(subjects) if config.line_ranges?
|
|
41
|
+
log_memory("after parse_subjects", "#{subjects.length} subjects")
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
43
|
+
baseline_result = run_baseline(subjects)
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
mutations = generate_mutations(subjects)
|
|
46
|
+
equivalent_mutations, mutations = filter_equivalent(mutations)
|
|
47
|
+
release_subject_nodes(subjects)
|
|
48
|
+
results, truncated = run_mutations(mutations, baseline_result)
|
|
49
|
+
results += equivalent_mutations.map do |m|
|
|
50
|
+
m.strip_sources!
|
|
51
|
+
equivalent_result(m)
|
|
52
|
+
end
|
|
53
|
+
log_memory("after run_mutations", "#{results.length} results")
|
|
41
54
|
|
|
42
|
-
|
|
55
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
52
|
-
log_memory("after run_mutations", "#{results.length} results")
|
|
57
|
+
summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: truncated)
|
|
58
|
+
output_report(summary)
|
|
59
|
+
save_session(summary)
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
summary
|
|
62
|
+
end
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
output_report(summary)
|
|
64
|
+
private
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
end
|
|
66
|
+
attr_reader :parser, :registry, :isolator, :cache, :on_result
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
def parse_subjects
|
|
69
|
+
files = resolve_target_files
|
|
70
|
+
files.flat_map { |file| parser.call(file) }
|
|
71
|
+
end
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
def resolve_target_files
|
|
74
|
+
return config.target_files unless config.target_files.empty?
|
|
65
75
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
files.flat_map { |file| parser.call(file) }
|
|
69
|
-
end
|
|
76
|
+
Evilution::Git::ChangedFiles.new.call
|
|
77
|
+
end
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
def filter_by_target(subjects)
|
|
80
|
+
matched = if config.target.include?("#")
|
|
81
|
+
subjects.select { |s| s.name == config.target }
|
|
82
|
+
else
|
|
83
|
+
subjects.select { |s| s.name.start_with?("#{config.target}#") }
|
|
84
|
+
end
|
|
85
|
+
raise Evilution::Error, "no method found matching '#{config.target}'" if matched.empty?
|
|
73
86
|
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
matched
|
|
88
|
+
end
|
|
76
89
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
subjects.select { |s| s.name.start_with?("#{config.target}#") }
|
|
82
|
-
end
|
|
83
|
-
raise Error, "no method found matching '#{config.target}'" if matched.empty?
|
|
90
|
+
def filter_by_line_ranges(subjects)
|
|
91
|
+
subjects.select do |subject|
|
|
92
|
+
range = config.line_ranges[subject.file_path]
|
|
93
|
+
next true unless range
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
subject_start = subject.line_number
|
|
96
|
+
subject_end = subject_start + subject.source.count("\n")
|
|
97
|
+
subject_start <= range.last && subject_end >= range.first
|
|
86
98
|
end
|
|
99
|
+
end
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
next true unless range
|
|
92
|
-
|
|
93
|
-
subject_start = subject.line_number
|
|
94
|
-
subject_end = subject_start + subject.source.count("\n")
|
|
95
|
-
subject_start <= range.last && subject_end >= range.first
|
|
96
|
-
end
|
|
101
|
+
def generate_mutations(subjects)
|
|
102
|
+
subjects.flat_map do |subject|
|
|
103
|
+
registry.mutations_for(subject)
|
|
97
104
|
end
|
|
105
|
+
end
|
|
98
106
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
end
|
|
103
|
-
end
|
|
107
|
+
def filter_equivalent(mutations)
|
|
108
|
+
Evilution::Equivalent::Detector.new.call(mutations)
|
|
109
|
+
end
|
|
104
110
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
def release_subject_nodes(subjects)
|
|
112
|
+
subjects.each(&:release_node!)
|
|
113
|
+
end
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
def equivalent_result(mutation)
|
|
116
|
+
Evilution::Result::MutationResult.new(mutation: mutation, status: :equivalent, duration: 0.0)
|
|
117
|
+
end
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
end
|
|
119
|
+
def run_baseline(subjects)
|
|
120
|
+
return nil unless config.baseline? && subjects.any?
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
log_baseline_start
|
|
123
|
+
baseline = Evilution::Baseline.new(timeout: config.timeout)
|
|
124
|
+
result = baseline.call(subjects)
|
|
125
|
+
log_baseline_complete(result)
|
|
126
|
+
result
|
|
127
|
+
end
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
def run_mutations(mutations, baseline_result = nil)
|
|
130
|
+
if config.jobs > 1
|
|
131
|
+
run_mutations_parallel(mutations, baseline_result)
|
|
132
|
+
else
|
|
133
|
+
run_mutations_sequential(mutations, baseline_result)
|
|
125
134
|
end
|
|
135
|
+
end
|
|
126
136
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
end
|
|
137
|
+
def run_mutations_sequential(mutations, baseline_result = nil)
|
|
138
|
+
integration = build_integration
|
|
139
|
+
spec_resolver = baseline_result&.failed? ? Evilution::SpecResolver.new : nil
|
|
140
|
+
results = []
|
|
141
|
+
survived_count = 0
|
|
142
|
+
truncated = false
|
|
134
143
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
log_progress(index + 1, result.status)
|
|
152
|
-
log_mutation_diagnostics(result)
|
|
153
|
-
|
|
154
|
-
if config.fail_fast? && survived_count >= config.fail_fast
|
|
155
|
-
truncated = true
|
|
156
|
-
break
|
|
157
|
-
end
|
|
144
|
+
mutations.each_with_index do |mutation, index|
|
|
145
|
+
result = execute_or_fetch(mutation) do
|
|
146
|
+
test_command = ->(m) { integration.call(m) }
|
|
147
|
+
isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
|
|
148
|
+
end
|
|
149
|
+
mutation.strip_sources!
|
|
150
|
+
result = neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
|
|
151
|
+
results << result
|
|
152
|
+
survived_count += 1 if result.survived?
|
|
153
|
+
on_result&.call(result)
|
|
154
|
+
log_progress(index + 1, result.status)
|
|
155
|
+
log_mutation_diagnostics(result)
|
|
156
|
+
|
|
157
|
+
if config.fail_fast? && survived_count >= config.fail_fast
|
|
158
|
+
truncated = true
|
|
159
|
+
break
|
|
158
160
|
end
|
|
159
|
-
|
|
160
|
-
[results, truncated]
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
pool = Parallel::Pool.new(size: config.jobs)
|
|
166
|
-
worker_isolator = Isolation::InProcess.new
|
|
167
|
-
spec_resolver = baseline_result&.failed? ? SpecResolver.new : nil
|
|
168
|
-
state = { results: [], survived_count: 0, truncated: false, completed: 0 }
|
|
163
|
+
[results, truncated]
|
|
164
|
+
end
|
|
169
165
|
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
def run_mutations_parallel(mutations, baseline_result = nil)
|
|
167
|
+
integration = build_integration
|
|
168
|
+
pool = Evilution::Parallel::Pool.new(size: config.jobs)
|
|
169
|
+
worker_isolator = Evilution::Isolation::InProcess.new
|
|
170
|
+
spec_resolver = baseline_result&.failed? ? Evilution::SpecResolver.new : nil
|
|
171
|
+
state = { results: [], survived_count: 0, truncated: false, completed: 0 }
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
end
|
|
173
|
+
mutations.each_slice(config.jobs) do |batch|
|
|
174
|
+
break if state[:truncated]
|
|
176
175
|
|
|
177
|
-
|
|
176
|
+
batch_results = run_parallel_batch(batch, pool, worker_isolator, integration)
|
|
177
|
+
process_batch(batch_results, baseline_result, spec_resolver, state)
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
180
|
+
[state[:results], state[:truncated]]
|
|
181
|
+
end
|
|
189
182
|
|
|
190
|
-
|
|
191
|
-
|
|
183
|
+
def run_parallel_batch(batch, pool, worker_isolator, integration)
|
|
184
|
+
uncached_indices, cached_results = partition_cached(batch)
|
|
185
|
+
worker_results = run_uncached_workers(batch, uncached_indices, pool, worker_isolator, integration)
|
|
186
|
+
compact_results = merge_parallel_results(batch, uncached_indices, cached_results, worker_results)
|
|
187
|
+
batch.each(&:strip_sources!)
|
|
188
|
+
batch_results = rebuild_results(batch, compact_results)
|
|
189
|
+
batch_results.each { |r| store_cached_result(r.mutation, r) }
|
|
190
|
+
batch_results
|
|
191
|
+
end
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
193
|
+
def run_uncached_workers(batch, uncached_indices, pool, worker_isolator, integration)
|
|
194
|
+
return [] if uncached_indices.empty?
|
|
200
195
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
log_mutation_diagnostics(result)
|
|
209
|
-
end
|
|
196
|
+
uncached = uncached_indices.map { |i| batch[i] }
|
|
197
|
+
pool.map(uncached) do |mutation|
|
|
198
|
+
test_command = ->(m) { integration.call(m) }
|
|
199
|
+
result = worker_isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
|
|
200
|
+
compact_result(result)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
210
203
|
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
def process_batch(batch_results, baseline_result, spec_resolver, state)
|
|
205
|
+
batch_results.each do |result|
|
|
206
|
+
result = neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
|
|
207
|
+
state[:results] << result
|
|
208
|
+
state[:survived_count] += 1 if result.survived?
|
|
209
|
+
state[:completed] += 1
|
|
210
|
+
on_result&.call(result)
|
|
211
|
+
log_progress(state[:completed], result.status)
|
|
212
|
+
log_mutation_diagnostics(result)
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
log_memory("after batch", "#{state[:completed]} complete")
|
|
216
|
+
state[:truncated] = true if should_truncate?(state[:survived_count])
|
|
217
|
+
end
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
219
|
+
def neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
|
|
220
|
+
return result unless result.survived? && baseline_result && baseline_result.failed?
|
|
221
|
+
|
|
222
|
+
if config.spec_files.any?
|
|
223
|
+
neutralize = true
|
|
224
|
+
else
|
|
225
|
+
spec_file = spec_resolver.call(result.mutation.file_path) || "spec"
|
|
226
|
+
neutralize = baseline_result.failed_spec_files.include?(spec_file)
|
|
227
|
+
end
|
|
228
|
+
return result unless neutralize
|
|
229
|
+
|
|
230
|
+
Evilution::Result::MutationResult.new(
|
|
231
|
+
mutation: result.mutation,
|
|
232
|
+
status: :neutral,
|
|
233
|
+
duration: result.duration,
|
|
234
|
+
test_command: result.test_command,
|
|
235
|
+
child_rss_kb: result.child_rss_kb,
|
|
236
|
+
memory_delta_kb: result.memory_delta_kb
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def compact_result(result)
|
|
241
|
+
{
|
|
242
|
+
status: result.status,
|
|
243
|
+
duration: result.duration,
|
|
244
|
+
killing_test: result.killing_test,
|
|
245
|
+
test_command: result.test_command,
|
|
246
|
+
child_rss_kb: result.child_rss_kb,
|
|
247
|
+
memory_delta_kb: result.memory_delta_kb
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def rebuild_results(batch, compact_results)
|
|
252
|
+
batch.zip(compact_results).map do |mutation, data|
|
|
253
|
+
Evilution::Result::MutationResult.new(
|
|
254
|
+
mutation: mutation,
|
|
255
|
+
status: data[:status],
|
|
256
|
+
duration: data[:duration],
|
|
257
|
+
killing_test: data[:killing_test],
|
|
258
|
+
test_command: data[:test_command],
|
|
259
|
+
child_rss_kb: data[:child_rss_kb],
|
|
260
|
+
memory_delta_kb: data[:memory_delta_kb]
|
|
233
261
|
)
|
|
234
262
|
end
|
|
263
|
+
end
|
|
235
264
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
duration: result.duration,
|
|
240
|
-
killing_test: result.killing_test,
|
|
241
|
-
test_command: result.test_command,
|
|
242
|
-
child_rss_kb: result.child_rss_kb,
|
|
243
|
-
memory_delta_kb: result.memory_delta_kb
|
|
244
|
-
}
|
|
245
|
-
end
|
|
265
|
+
def should_truncate?(survived_count)
|
|
266
|
+
config.fail_fast? && survived_count >= config.fail_fast
|
|
267
|
+
end
|
|
246
268
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
status: data[:status],
|
|
252
|
-
duration: data[:duration],
|
|
253
|
-
killing_test: data[:killing_test],
|
|
254
|
-
test_command: data[:test_command],
|
|
255
|
-
child_rss_kb: data[:child_rss_kb],
|
|
256
|
-
memory_delta_kb: data[:memory_delta_kb]
|
|
257
|
-
)
|
|
258
|
-
end
|
|
269
|
+
def build_isolator
|
|
270
|
+
case resolve_isolation
|
|
271
|
+
when :fork then Evilution::Isolation::Fork.new
|
|
272
|
+
when :in_process then Evilution::Isolation::InProcess.new
|
|
259
273
|
end
|
|
274
|
+
end
|
|
260
275
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
end
|
|
276
|
+
def resolve_isolation
|
|
277
|
+
return :fork if config.isolation == :fork
|
|
264
278
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
:in_process
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def build_integration
|
|
283
|
+
case config.integration
|
|
284
|
+
when :rspec
|
|
285
|
+
test_files = config.spec_files.empty? ? nil : config.spec_files
|
|
286
|
+
Evilution::Integration::RSpec.new(test_files: test_files)
|
|
287
|
+
else
|
|
288
|
+
raise Evilution::Error, "unknown integration: #{config.integration}"
|
|
270
289
|
end
|
|
290
|
+
end
|
|
271
291
|
|
|
272
|
-
|
|
273
|
-
|
|
292
|
+
def output_report(summary)
|
|
293
|
+
reporter = build_reporter
|
|
294
|
+
return unless reporter
|
|
274
295
|
|
|
275
|
-
|
|
276
|
-
|
|
296
|
+
output = reporter.call(summary)
|
|
297
|
+
return if config.quiet
|
|
277
298
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
raise Error, "unknown integration: #{config.integration}"
|
|
285
|
-
end
|
|
299
|
+
if config.html?
|
|
300
|
+
path = "evilution-report.html"
|
|
301
|
+
File.write(path, output)
|
|
302
|
+
warn "HTML report written to #{path}"
|
|
303
|
+
else
|
|
304
|
+
$stdout.puts(output)
|
|
286
305
|
end
|
|
306
|
+
end
|
|
287
307
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return unless reporter
|
|
308
|
+
def log_baseline_start
|
|
309
|
+
return if config.quiet || !config.text? || !$stderr.tty?
|
|
291
310
|
|
|
292
|
-
|
|
293
|
-
|
|
311
|
+
$stderr.write("Running baseline test suite...\n")
|
|
312
|
+
end
|
|
294
313
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
File.write(path, output)
|
|
298
|
-
warn "HTML report written to #{path}"
|
|
299
|
-
else
|
|
300
|
-
$stdout.puts(output)
|
|
301
|
-
end
|
|
302
|
-
end
|
|
314
|
+
def log_baseline_complete(result)
|
|
315
|
+
return if config.quiet || !config.text? || !$stderr.tty?
|
|
303
316
|
|
|
304
|
-
|
|
305
|
-
|
|
317
|
+
count = result.failed_spec_files.size
|
|
318
|
+
$stderr.write("Baseline complete: #{count} failing spec file#{"s" unless count == 1}\n")
|
|
319
|
+
end
|
|
306
320
|
|
|
307
|
-
|
|
308
|
-
|
|
321
|
+
def log_progress(current, status)
|
|
322
|
+
return if config.quiet || !config.text? || !$stderr.tty?
|
|
309
323
|
|
|
310
|
-
|
|
311
|
-
|
|
324
|
+
$stderr.write("mutation #{current} #{status}\n")
|
|
325
|
+
end
|
|
312
326
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
end
|
|
327
|
+
def log_memory(phase, context = nil)
|
|
328
|
+
return unless config.verbose && !config.quiet
|
|
316
329
|
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
rss = Evilution::Memory.rss_mb
|
|
331
|
+
return unless rss
|
|
319
332
|
|
|
320
|
-
|
|
321
|
-
|
|
333
|
+
gc = gc_stats_string
|
|
334
|
+
msg = format("[memory] %<phase>s: %<rss>.1f MB", phase: phase, rss: rss)
|
|
335
|
+
context = [context, gc].compact.join(", ")
|
|
336
|
+
msg += " (#{context})" unless context.empty?
|
|
337
|
+
$stderr.write("#{msg}\n")
|
|
338
|
+
end
|
|
322
339
|
|
|
323
|
-
|
|
324
|
-
|
|
340
|
+
def log_mutation_diagnostics(result)
|
|
341
|
+
return unless config.verbose && !config.quiet
|
|
325
342
|
|
|
326
|
-
|
|
327
|
-
|
|
343
|
+
parts = []
|
|
344
|
+
parts << format("child_rss: %<mb>.1f MB", mb: result.child_rss_kb / 1024.0) if result.child_rss_kb
|
|
328
345
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
msg += " (#{context})" unless context.empty?
|
|
333
|
-
$stderr.write("#{msg}\n")
|
|
346
|
+
if result.memory_delta_kb
|
|
347
|
+
sign = result.memory_delta_kb.negative? ? "" : "+"
|
|
348
|
+
parts << format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: result.memory_delta_kb / 1024.0)
|
|
334
349
|
end
|
|
335
350
|
|
|
336
|
-
|
|
337
|
-
return unless config.verbose && !config.quiet
|
|
351
|
+
parts << gc_stats_string
|
|
338
352
|
|
|
339
|
-
|
|
340
|
-
parts << format("child_rss: %<mb>.1f MB", mb: result.child_rss_kb / 1024.0) if result.child_rss_kb
|
|
353
|
+
return if parts.empty?
|
|
341
354
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
parts << format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: result.memory_delta_kb / 1024.0)
|
|
345
|
-
end
|
|
355
|
+
$stderr.write("[verbose] #{result.mutation}: #{parts.join(", ")}\n")
|
|
356
|
+
end
|
|
346
357
|
|
|
347
|
-
|
|
358
|
+
def gc_stats_string
|
|
359
|
+
stats = GC.stat
|
|
360
|
+
format(
|
|
361
|
+
"heap_live_slots: %<live>d, allocated: %<alloc>d, freed: %<freed>d",
|
|
362
|
+
live: stats[:heap_live_slots],
|
|
363
|
+
alloc: stats[:total_allocated_objects],
|
|
364
|
+
freed: stats[:total_freed_objects]
|
|
365
|
+
)
|
|
366
|
+
end
|
|
348
367
|
|
|
349
|
-
|
|
368
|
+
def save_session(summary)
|
|
369
|
+
return unless config.save_session?
|
|
350
370
|
|
|
351
|
-
|
|
352
|
-
|
|
371
|
+
Evilution::Session::Store.new.save(summary)
|
|
372
|
+
rescue StandardError => e
|
|
373
|
+
warn "[evilution] failed to save session: #{e.message}" unless config.quiet
|
|
374
|
+
end
|
|
353
375
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
376
|
+
def build_reporter
|
|
377
|
+
case config.format
|
|
378
|
+
when :json
|
|
379
|
+
Evilution::Reporter::JSON.new
|
|
380
|
+
when :text
|
|
381
|
+
Evilution::Reporter::CLI.new
|
|
382
|
+
when :html
|
|
383
|
+
Evilution::Reporter::HTML.new
|
|
362
384
|
end
|
|
385
|
+
end
|
|
363
386
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
Reporter::JSON.new
|
|
368
|
-
when :text
|
|
369
|
-
Reporter::CLI.new
|
|
370
|
-
when :html
|
|
371
|
-
Reporter::HTML.new
|
|
372
|
-
end
|
|
373
|
-
end
|
|
387
|
+
def partition_cached(batch)
|
|
388
|
+
uncached_indices = []
|
|
389
|
+
cached_results = {}
|
|
374
390
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if cached
|
|
382
|
-
cached_results[i] = compact_result(cached)
|
|
383
|
-
else
|
|
384
|
-
uncached_indices << i
|
|
385
|
-
end
|
|
391
|
+
batch.each_with_index do |mutation, i|
|
|
392
|
+
cached = fetch_cached_result(mutation)
|
|
393
|
+
if cached
|
|
394
|
+
cached_results[i] = compact_result(cached)
|
|
395
|
+
else
|
|
396
|
+
uncached_indices << i
|
|
386
397
|
end
|
|
387
|
-
|
|
388
|
-
[uncached_indices, cached_results]
|
|
389
398
|
end
|
|
390
399
|
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
400
|
+
[uncached_indices, cached_results]
|
|
401
|
+
end
|
|
396
402
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
403
|
+
def merge_parallel_results(batch, uncached_indices, cached_results, worker_results)
|
|
404
|
+
result_map = cached_results.dup
|
|
405
|
+
uncached_indices.each_with_index { |batch_idx, worker_idx| result_map[batch_idx] = worker_results[worker_idx] }
|
|
406
|
+
batch.each_index.map { |i| result_map[i] }
|
|
407
|
+
end
|
|
400
408
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
end
|
|
409
|
+
def execute_or_fetch(mutation)
|
|
410
|
+
cached = fetch_cached_result(mutation)
|
|
411
|
+
return cached if cached
|
|
405
412
|
|
|
406
|
-
|
|
407
|
-
|
|
413
|
+
result = yield
|
|
414
|
+
store_cached_result(mutation, result)
|
|
415
|
+
result
|
|
416
|
+
end
|
|
408
417
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return nil unless %i[killed timeout].include?(data[:status])
|
|
418
|
+
def fetch_cached_result(mutation)
|
|
419
|
+
return nil unless cache
|
|
412
420
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
duration: data[:duration],
|
|
417
|
-
killing_test: data[:killing_test],
|
|
418
|
-
test_command: data[:test_command]
|
|
419
|
-
)
|
|
420
|
-
end
|
|
421
|
+
data = cache.fetch(mutation)
|
|
422
|
+
return nil unless data
|
|
423
|
+
return nil unless %i[killed timeout].include?(data[:status])
|
|
421
424
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
+
Evilution::Result::MutationResult.new(
|
|
426
|
+
mutation: mutation,
|
|
427
|
+
status: data[:status],
|
|
428
|
+
duration: data[:duration],
|
|
429
|
+
killing_test: data[:killing_test],
|
|
430
|
+
test_command: data[:test_command]
|
|
431
|
+
)
|
|
432
|
+
end
|
|
425
433
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
434
|
+
def store_cached_result(mutation, result)
|
|
435
|
+
return unless cache
|
|
436
|
+
return unless result.killed? || result.timeout?
|
|
437
|
+
|
|
438
|
+
cache.store(mutation,
|
|
439
|
+
status: result.status,
|
|
440
|
+
duration: result.duration,
|
|
441
|
+
killing_test: result.killing_test,
|
|
442
|
+
test_command: result.test_command)
|
|
432
443
|
end
|
|
433
444
|
end
|