evilution 0.26.0 → 0.28.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 +23 -0
- data/.rubocop_todo.yml +6 -0
- data/CHANGELOG.md +54 -0
- data/README.md +76 -3
- data/lib/evilution/baseline.rb +5 -4
- data/lib/evilution/cache.rb +2 -0
- data/lib/evilution/child_output.rb +24 -0
- data/lib/evilution/cli/commands/run.rb +9 -0
- data/lib/evilution/cli/commands/version.rb +2 -0
- data/lib/evilution/cli/parser/options_builder.rb +23 -2
- data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
- data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
- data/lib/evilution/compare/diff_extractor.rb +6 -0
- data/lib/evilution/compare/fingerprint.rb +15 -72
- data/lib/evilution/compare/line_normalizer.rb +72 -0
- data/lib/evilution/compare/normalizer.rb +17 -4
- data/lib/evilution/config/builders/spec_resolver.rb +15 -0
- data/lib/evilution/config/builders/spec_selector.rb +16 -0
- data/lib/evilution/config/builders.rb +4 -0
- data/lib/evilution/config/env_loader.rb +12 -0
- data/lib/evilution/config/file_loader.rb +22 -0
- data/lib/evilution/config/sources.rb +14 -0
- data/lib/evilution/config/validators/base.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
- data/lib/evilution/config/validators/fail_fast.rb +11 -0
- data/lib/evilution/config/validators/hooks.rb +12 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
- data/lib/evilution/config/validators/integration.rb +11 -0
- data/lib/evilution/config/validators/isolation.rb +19 -0
- data/lib/evilution/config/validators/jobs.rb +9 -0
- data/lib/evilution/config/validators/preload.rb +13 -0
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config/validators/spec_mappings.rb +56 -0
- data/lib/evilution/config/validators/spec_pattern.rb +12 -0
- data/lib/evilution/config/validators.rb +4 -0
- data/lib/evilution/config.rb +93 -266
- data/lib/evilution/feedback/detector.rb +15 -0
- data/lib/evilution/feedback/messages.rb +42 -0
- data/lib/evilution/feedback.rb +5 -0
- data/lib/evilution/integration/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
- data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
- data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
- data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
- data/lib/evilution/integration/rspec/result_builder.rb +40 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
- data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard.rb +40 -0
- data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
- data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
- data/lib/evilution/integration/rspec.rb +61 -232
- data/lib/evilution/isolation/fork.rb +23 -13
- data/lib/evilution/isolation/in_process.rb +10 -6
- data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
- data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
- data/lib/evilution/mcp/info_tool/actions.rb +16 -0
- data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
- data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
- data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
- data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
- data/lib/evilution/mcp/info_tool.rb +43 -263
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- data/lib/evilution/mcp/session_tool.rb +0 -2
- data/lib/evilution/mutation.rb +47 -27
- data/lib/evilution/mutator/base.rb +8 -8
- data/lib/evilution/mutator/operator/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
- data/lib/evilution/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
- data/lib/evilution/parallel/work_queue/channel.rb +23 -0
- data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators.rb +6 -0
- data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
- data/lib/evilution/parallel/work_queue/worker.rb +114 -0
- data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
- data/lib/evilution/parallel/work_queue.rb +42 -327
- data/lib/evilution/process_cleanup.rb +19 -0
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
- data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
- data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
- data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
- data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
- data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
- data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
- data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
- data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
- data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
- data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
- data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
- data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
- data/lib/evilution/reporter/cli/pct.rb +9 -0
- data/lib/evilution/reporter/cli/section.rb +13 -0
- data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
- data/lib/evilution/reporter/cli/trailer.rb +22 -0
- data/lib/evilution/reporter/cli.rb +79 -162
- data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
- data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
- data/lib/evilution/reporter/html/escape.rb +1 -1
- data/lib/evilution/reporter/html/section.rb +1 -1
- data/lib/evilution/reporter/html/sections.rb +4 -2
- data/lib/evilution/reporter/html/stylesheet.rb +1 -1
- data/lib/evilution/reporter/html.rb +8 -3
- data/lib/evilution/reporter/suggestion/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
- data/lib/evilution/reporter/suggestion/templates.rb +6 -0
- data/lib/evilution/result/error_info.rb +20 -0
- data/lib/evilution/result/memory_stats.rb +20 -0
- data/lib/evilution/result/mutation_result.rb +30 -14
- data/lib/evilution/runner/baseline_runner.rb +1 -2
- data/lib/evilution/runner/diagnostics.rb +1 -2
- data/lib/evilution/runner/isolation_resolver.rb +10 -4
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +53 -292
- data/lib/evilution/runner/mutation_planner.rb +1 -2
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +1 -2
- data/lib/evilution/runner.rb +53 -30
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- data/script/memory_check +3 -1
- metadata +125 -3
- data/lib/evilution/reporter/html/namespace.rb +0 -11
|
@@ -9,20 +9,12 @@ class Evilution::Parallel::WorkQueue
|
|
|
9
9
|
|
|
10
10
|
TIMING_GRACE_PERIOD = 5
|
|
11
11
|
|
|
12
|
-
WorkerStat = Struct.new(:pid, :items_completed, :busy_time, :wall_time) do
|
|
13
|
-
def idle_time
|
|
14
|
-
wall_time - busy_time
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def utilization
|
|
18
|
-
return 0.0 if wall_time.nil? || wall_time.zero?
|
|
19
|
-
|
|
20
|
-
busy_time / wall_time
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
12
|
def initialize(size:, hooks: nil, prefetch: 1, item_timeout: nil, worker_max_items: nil)
|
|
25
|
-
|
|
13
|
+
Validators::PositiveInt.call!(:size, size)
|
|
14
|
+
Validators::PositiveInt.call!(:prefetch, prefetch)
|
|
15
|
+
Validators::OptionalPositiveNumber.call!(:item_timeout, item_timeout)
|
|
16
|
+
Validators::OptionalPositiveInt.call!(:worker_max_items, worker_max_items)
|
|
17
|
+
|
|
26
18
|
@size = size
|
|
27
19
|
@hooks = hooks
|
|
28
20
|
@prefetch = prefetch
|
|
@@ -34,18 +26,25 @@ class Evilution::Parallel::WorkQueue
|
|
|
34
26
|
def map(items, &block)
|
|
35
27
|
return [] if items.empty?
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
workers = (0...[@size, items.length].min).map { |i| spawn_one(i, &block) }
|
|
30
|
+
dispatcher = Dispatcher.new(
|
|
31
|
+
workers: workers, items: items, prefetch: @prefetch,
|
|
32
|
+
item_timeout: @item_timeout, worker_max_items: @worker_max_items,
|
|
33
|
+
recycle_factory: ->(old) { spawn_one(old.worker_index, &block) }
|
|
34
|
+
)
|
|
41
35
|
|
|
36
|
+
retired = []
|
|
42
37
|
begin
|
|
43
|
-
|
|
38
|
+
results, retired = dispatcher.run
|
|
39
|
+
raise dispatcher.first_error if dispatcher.first_error
|
|
40
|
+
|
|
41
|
+
results
|
|
44
42
|
ensure
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
workers.each(&:shutdown)
|
|
44
|
+
collect_final_timings(workers)
|
|
45
|
+
workers.each(&:close_pipes)
|
|
46
|
+
workers.each(&:reap)
|
|
47
|
+
@worker_stats = retired + workers.map(&:to_stat)
|
|
49
48
|
end
|
|
50
49
|
end
|
|
51
50
|
|
|
@@ -55,272 +54,12 @@ class Evilution::Parallel::WorkQueue
|
|
|
55
54
|
|
|
56
55
|
private
|
|
57
56
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
validate_positive_int!(:prefetch, prefetch)
|
|
61
|
-
validate_optional_positive_number!(:item_timeout, item_timeout)
|
|
62
|
-
validate_optional_positive_int!(:worker_max_items, worker_max_items)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def validate_positive_int!(name, value)
|
|
66
|
-
return if value.is_a?(Integer) && value >= 1
|
|
67
|
-
|
|
68
|
-
raise ArgumentError, "#{name} must be a positive integer, got #{value.inspect}"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def validate_optional_positive_int!(name, value)
|
|
72
|
-
return if value.nil? || (value.is_a?(Integer) && value.positive?)
|
|
73
|
-
|
|
74
|
-
raise ArgumentError, "#{name} must be nil or a positive integer, got #{value.inspect}"
|
|
57
|
+
def spawn_one(worker_index, &)
|
|
58
|
+
Worker.spawn(worker_index: worker_index, hooks: @hooks, &)
|
|
75
59
|
end
|
|
76
60
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
raise ArgumentError, "#{name} must be nil or a positive number, got #{value.inspect}"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def spawn_workers(count, &block)
|
|
84
|
-
count.times.map { |slot| spawn_one_worker(worker_index: slot, &block) }
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# EV-kdns / GH #817: translate 0-based worker slot to parallel_tests'
|
|
88
|
-
# TEST_ENV_NUMBER convention ("" for slot 0, "2" for slot 1, ...). Rails
|
|
89
|
-
# apps interpolating TEST_ENV_NUMBER into database.yml get per-worker
|
|
90
|
-
# SQLite files, avoiding lock contention under jobs > 1.
|
|
91
|
-
def test_env_number_for(worker_index)
|
|
92
|
-
worker_index.zero? ? "" : (worker_index + 1).to_s
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def spawn_one_worker(worker_index:, &block)
|
|
96
|
-
cmd_read, cmd_write = IO.pipe
|
|
97
|
-
res_read, res_write = IO.pipe
|
|
98
|
-
# Marshal payloads are ASCII-8BIT; pipes default to text mode and may
|
|
99
|
-
# transcode according to their external/internal encodings (influenced by
|
|
100
|
-
# Encoding.default_external and/or Encoding.default_internal — Rails sets
|
|
101
|
-
# the latter to UTF-8), failing on bytes with no mapping. Force binmode on
|
|
102
|
-
# all four ends.
|
|
103
|
-
[cmd_read, cmd_write, res_read, res_write].each(&:binmode)
|
|
104
|
-
|
|
105
|
-
pid = Process.fork do
|
|
106
|
-
cmd_write.close
|
|
107
|
-
res_read.close
|
|
108
|
-
ENV["TEST_ENV_NUMBER"] = test_env_number_for(worker_index)
|
|
109
|
-
worker_loop(cmd_read, res_write, &block)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
cmd_read.close
|
|
113
|
-
res_write.close
|
|
114
|
-
|
|
115
|
-
{ pid: pid, cmd_write: cmd_write, res_read: res_read, items_completed: 0, pending: 0, worker_index: worker_index }
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def worker_loop(cmd_read, res_write, &block)
|
|
119
|
-
@hooks.fire(:worker_process_start) if @hooks
|
|
120
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
121
|
-
busy_time = 0.0
|
|
122
|
-
|
|
123
|
-
loop do
|
|
124
|
-
data = read_command(cmd_read)
|
|
125
|
-
break if data == SHUTDOWN
|
|
126
|
-
|
|
127
|
-
index, item = data
|
|
128
|
-
begin
|
|
129
|
-
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
130
|
-
result = block.call(item)
|
|
131
|
-
busy_time += Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
132
|
-
write_message(res_write, [index, :ok, result])
|
|
133
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
134
|
-
busy_time += Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
135
|
-
write_message(res_write, [index, :error, e])
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
wall_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
140
|
-
write_message(res_write, [STATS, busy_time, wall_time])
|
|
141
|
-
ensure
|
|
142
|
-
cmd_read.close
|
|
143
|
-
res_write.close
|
|
144
|
-
exit!
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def distribute_and_collect(items, workers)
|
|
148
|
-
state = CollectionState.new(items.length)
|
|
149
|
-
seed_workers(items, workers, state)
|
|
150
|
-
collect_results(items, workers, state)
|
|
151
|
-
raise state.first_error if state.first_error
|
|
152
|
-
|
|
153
|
-
state.results
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def seed_workers(items, workers, state)
|
|
157
|
-
@prefetch.times do
|
|
158
|
-
workers.each do |worker|
|
|
159
|
-
break unless state.next_index < items.length
|
|
160
|
-
|
|
161
|
-
send_item(worker, items, state)
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def collect_results(items, workers, state)
|
|
167
|
-
io_to_worker = workers.to_h { |w| [w[:res_read], w] }
|
|
168
|
-
result_ios = io_to_worker.keys
|
|
169
|
-
|
|
170
|
-
while state.in_flight.positive?
|
|
171
|
-
readable, = IO.select(result_ios, nil, nil, @item_timeout)
|
|
172
|
-
|
|
173
|
-
if readable.nil?
|
|
174
|
-
terminate_stuck_workers(workers)
|
|
175
|
-
state.first_error = Evilution::Error.new("worker timed out after #{@item_timeout}s") if state.first_error.nil?
|
|
176
|
-
break
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
readable.each do |io|
|
|
180
|
-
alive = handle_result(io, io_to_worker[io], items, state, workers, io_to_worker, result_ios)
|
|
181
|
-
result_ios.delete(io) unless alive
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def handle_result(io, worker, items, state, workers, io_to_worker, result_ios)
|
|
187
|
-
message = read_result(io)
|
|
188
|
-
return handle_dead_worker(worker, state) if message.nil?
|
|
189
|
-
|
|
190
|
-
record_result(message, worker, state)
|
|
191
|
-
return false if recycle_and_dispatch(worker, items, state, workers, io_to_worker, result_ios)
|
|
192
|
-
return true if draining_for_recycle?(worker)
|
|
193
|
-
|
|
194
|
-
send_item(worker, items, state) if state.next_index < items.length && state.first_error.nil?
|
|
195
|
-
true
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Once worker hits K, stop dispatching so pending drains to 0; recycle fires
|
|
199
|
-
# on the next result. Prevents prefetch > 1 from refilling pending forever.
|
|
200
|
-
def draining_for_recycle?(worker)
|
|
201
|
-
@worker_max_items && worker[:items_completed] >= @worker_max_items && worker[:pending].positive?
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def handle_dead_worker(worker, state)
|
|
205
|
-
state.first_error = Evilution::Error.new("worker process exited unexpectedly") if state.first_error.nil?
|
|
206
|
-
state.in_flight -= worker[:pending]
|
|
207
|
-
worker[:pending] = 0
|
|
208
|
-
false
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def record_result(message, worker, state)
|
|
212
|
-
index, status, value = message
|
|
213
|
-
state.first_error = value if status == :error && state.first_error.nil?
|
|
214
|
-
state.results[index] = value if status == :ok
|
|
215
|
-
state.in_flight -= 1
|
|
216
|
-
worker[:pending] -= 1
|
|
217
|
-
worker[:items_completed] += 1
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def recycle_and_dispatch(worker, items, state, workers, io_to_worker, result_ios)
|
|
221
|
-
return false unless should_recycle?(worker, state, items)
|
|
222
|
-
|
|
223
|
-
new_worker = recycle_worker(worker, workers, io_to_worker, result_ios)
|
|
224
|
-
send_item(new_worker, items, state) if state.next_index < items.length && state.first_error.nil?
|
|
225
|
-
true
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def should_recycle?(worker, state, items)
|
|
229
|
-
return false unless @worker_max_items
|
|
230
|
-
return false if worker[:items_completed] < @worker_max_items
|
|
231
|
-
return false unless worker[:pending].zero?
|
|
232
|
-
return false unless state.next_index < items.length
|
|
233
|
-
return false unless state.first_error.nil?
|
|
234
|
-
|
|
235
|
-
true
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def recycle_worker(old_worker, workers, io_to_worker, result_ios)
|
|
239
|
-
io_to_worker.delete(old_worker[:res_read])
|
|
240
|
-
result_ios.delete(old_worker[:res_read])
|
|
241
|
-
retire_worker(old_worker)
|
|
242
|
-
|
|
243
|
-
new_worker = spawn_one_worker(worker_index: old_worker[:worker_index], &@block)
|
|
244
|
-
workers[workers.index(old_worker)] = new_worker
|
|
245
|
-
io_to_worker[new_worker[:res_read]] = new_worker
|
|
246
|
-
result_ios << new_worker[:res_read]
|
|
247
|
-
|
|
248
|
-
new_worker
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def retire_worker(worker)
|
|
252
|
-
begin
|
|
253
|
-
write_message(worker[:cmd_write], SHUTDOWN)
|
|
254
|
-
rescue Errno::EPIPE
|
|
255
|
-
nil
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
busy, wall = drain_worker_stats(worker)
|
|
259
|
-
|
|
260
|
-
worker[:cmd_write].close unless worker[:cmd_write].closed?
|
|
261
|
-
worker[:res_read].close unless worker[:res_read].closed?
|
|
262
|
-
begin
|
|
263
|
-
Process.wait(worker[:pid])
|
|
264
|
-
rescue Errno::ECHILD
|
|
265
|
-
nil
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
@retired_workers << WorkerStat.new(worker[:pid], worker[:items_completed], busy, wall)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def drain_worker_stats(worker)
|
|
272
|
-
return [0.0, 0.0] unless worker[:res_read].wait_readable(TIMING_GRACE_PERIOD)
|
|
273
|
-
|
|
274
|
-
message = read_result(worker[:res_read])
|
|
275
|
-
return [0.0, 0.0] if message.nil?
|
|
276
|
-
|
|
277
|
-
tag, busy_time, wall_time = message
|
|
278
|
-
return [0.0, 0.0] unless tag == STATS
|
|
279
|
-
|
|
280
|
-
[busy_time, wall_time]
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def send_item(worker, items, state)
|
|
284
|
-
write_message(worker[:cmd_write], [state.next_index, items[state.next_index]])
|
|
285
|
-
state.next_index += 1
|
|
286
|
-
state.in_flight += 1
|
|
287
|
-
worker[:pending] += 1
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def build_worker_stats(workers)
|
|
291
|
-
workers.map do |worker|
|
|
292
|
-
WorkerStat.new(worker[:pid], worker[:items_completed], worker[:busy_time] || 0.0, worker[:wall_time] || 0.0)
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def terminate_stuck_workers(workers)
|
|
297
|
-
workers.each do |worker|
|
|
298
|
-
Process.kill("KILL", worker[:pid])
|
|
299
|
-
rescue Errno::ESRCH
|
|
300
|
-
nil # Already exited
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def shutdown_workers(workers)
|
|
305
|
-
workers.each do |worker|
|
|
306
|
-
write_message(worker[:cmd_write], SHUTDOWN)
|
|
307
|
-
rescue Errno::EPIPE
|
|
308
|
-
# Worker already exited
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
collect_worker_timing(workers)
|
|
312
|
-
|
|
313
|
-
workers.each do |worker|
|
|
314
|
-
worker[:cmd_write].close unless worker[:cmd_write].closed?
|
|
315
|
-
worker[:res_read].close unless worker[:res_read].closed?
|
|
316
|
-
Process.wait(worker[:pid])
|
|
317
|
-
rescue Errno::ECHILD
|
|
318
|
-
# Already reaped
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def collect_worker_timing(workers)
|
|
323
|
-
io_to_worker = workers.reject { |w| w[:res_read].closed? }.to_h { |w| [w[:res_read], w] }
|
|
61
|
+
def collect_final_timings(workers)
|
|
62
|
+
io_to_worker = workers.reject { |w| w.res_io.closed? }.to_h { |w| [w.res_io, w] }
|
|
324
63
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + TIMING_GRACE_PERIOD
|
|
325
64
|
|
|
326
65
|
until io_to_worker.empty?
|
|
@@ -330,54 +69,30 @@ class Evilution::Parallel::WorkQueue
|
|
|
330
69
|
readable, = IO.select(io_to_worker.keys, nil, nil, remaining)
|
|
331
70
|
break unless readable
|
|
332
71
|
|
|
333
|
-
readable.each { |io|
|
|
72
|
+
readable.each { |io| apply_final_timing(io_to_worker.delete(io), io) }
|
|
334
73
|
end
|
|
335
74
|
end
|
|
336
75
|
|
|
337
|
-
def
|
|
338
|
-
message =
|
|
76
|
+
def apply_final_timing(worker, io)
|
|
77
|
+
message = Evilution::Parallel::WorkQueue::Channel.read(io)
|
|
339
78
|
return if message.nil?
|
|
340
79
|
|
|
341
80
|
tag, busy_time, wall_time = message
|
|
342
81
|
return unless tag == STATS
|
|
343
82
|
|
|
344
|
-
worker
|
|
345
|
-
worker
|
|
83
|
+
worker.busy_time = busy_time
|
|
84
|
+
worker.wall_time = wall_time
|
|
346
85
|
end
|
|
347
|
-
|
|
348
|
-
def write_message(io, data)
|
|
349
|
-
payload = Marshal.dump(data)
|
|
350
|
-
io.write([payload.bytesize].pack("N"))
|
|
351
|
-
io.write(payload)
|
|
352
|
-
io.flush
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
def read_command(io)
|
|
356
|
-
header = io.read(4)
|
|
357
|
-
return SHUTDOWN if header.nil? || header.bytesize < 4
|
|
358
|
-
|
|
359
|
-
length = header.unpack1("N")
|
|
360
|
-
payload = io.read(length)
|
|
361
|
-
return SHUTDOWN if payload.nil? || payload.bytesize < length
|
|
362
|
-
|
|
363
|
-
Marshal.load(payload) # rubocop:disable Security/MarshalLoad
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def read_result(io)
|
|
367
|
-
header = io.read(4)
|
|
368
|
-
return nil if header.nil? || header.bytesize < 4
|
|
369
|
-
|
|
370
|
-
length = header.unpack1("N")
|
|
371
|
-
payload = io.read(length)
|
|
372
|
-
return nil if payload.nil? || payload.bytesize < length
|
|
373
|
-
|
|
374
|
-
Marshal.load(payload) # rubocop:disable Security/MarshalLoad
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
CollectionState = Struct.new(:results, :in_flight, :next_index, :first_error) do
|
|
378
|
-
def initialize(item_count)
|
|
379
|
-
super(Array.new(item_count), 0, 0, nil)
|
|
380
|
-
end
|
|
381
|
-
end
|
|
382
|
-
private_constant :CollectionState
|
|
383
86
|
end
|
|
87
|
+
|
|
88
|
+
require_relative "work_queue/worker_stat"
|
|
89
|
+
require_relative "work_queue/validators"
|
|
90
|
+
require_relative "work_queue/validators/positive_int"
|
|
91
|
+
require_relative "work_queue/validators/optional_positive_int"
|
|
92
|
+
require_relative "work_queue/validators/optional_positive_number"
|
|
93
|
+
require_relative "work_queue/channel"
|
|
94
|
+
require_relative "work_queue/channel/frame"
|
|
95
|
+
require_relative "work_queue/worker"
|
|
96
|
+
require_relative "work_queue/worker/loop"
|
|
97
|
+
require_relative "work_queue/collection_state"
|
|
98
|
+
require_relative "work_queue/dispatcher"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
|
|
5
|
+
module Evilution::ProcessCleanup
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def safe_kill(signal, pid)
|
|
9
|
+
::Process.kill(signal, pid)
|
|
10
|
+
rescue Errno::ESRCH
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def safe_wait(pid)
|
|
15
|
+
::Process.wait(pid)
|
|
16
|
+
rescue Errno::ECHILD
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../item_formatters"
|
|
4
|
+
|
|
5
|
+
class Evilution::Reporter::CLI::ItemFormatters::CoverageGap
|
|
6
|
+
def format(gap)
|
|
7
|
+
location = "#{gap.file_path}:#{gap.line}"
|
|
8
|
+
header = if gap.single?
|
|
9
|
+
" #{gap.primary_operator}: #{location} (#{gap.subject_name})"
|
|
10
|
+
else
|
|
11
|
+
operators = gap.operator_names.join(", ")
|
|
12
|
+
" #{location} (#{gap.subject_name}) [#{gap.count} mutations: #{operators}]"
|
|
13
|
+
end
|
|
14
|
+
body = gap.mutation_results.first.mutation.unified_diff || gap.primary_diff
|
|
15
|
+
indented = body.split("\n").map { |l| " #{l}" }.join("\n")
|
|
16
|
+
"#{header}\n#{indented}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../item_formatters"
|
|
4
|
+
|
|
5
|
+
class Evilution::Reporter::CLI::ItemFormatters::Error
|
|
6
|
+
def format(result)
|
|
7
|
+
mutation = result.mutation
|
|
8
|
+
header = " #{mutation.operator_name}: #{mutation.file_path}:#{mutation.line}"
|
|
9
|
+
return header unless result.error_message
|
|
10
|
+
|
|
11
|
+
indented = result.error_message.lines.map { |line| " #{line.chomp}" }.join("\n")
|
|
12
|
+
"#{header}\n#{indented}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../item_formatters"
|
|
4
|
+
|
|
5
|
+
class Evilution::Reporter::CLI::ItemFormatters::ResultLocation
|
|
6
|
+
def format(result)
|
|
7
|
+
mutation = result.mutation
|
|
8
|
+
" #{mutation.operator_name}: #{mutation.file_path}:#{mutation.line}"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
require_relative "../pct"
|
|
5
|
+
|
|
6
|
+
class Evilution::Reporter::CLI::LineFormatters::Efficiency
|
|
7
|
+
def initialize(pct: Evilution::Reporter::CLI::Pct.new)
|
|
8
|
+
@pct = pct
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def format(summary)
|
|
12
|
+
return nil unless summary.duration.positive?
|
|
13
|
+
|
|
14
|
+
pct = @pct.format(summary.efficiency)
|
|
15
|
+
rate = Kernel.format("%.2f", summary.mutations_per_second)
|
|
16
|
+
"Efficiency: #{pct} killtime, #{rate} mutations/s"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
require_relative "../../../feedback/detector"
|
|
5
|
+
require_relative "../../../feedback/messages"
|
|
6
|
+
|
|
7
|
+
class Evilution::Reporter::CLI::LineFormatters::FeedbackFooter
|
|
8
|
+
def format(summary)
|
|
9
|
+
return nil unless Evilution::Feedback::Detector.friction?(summary)
|
|
10
|
+
|
|
11
|
+
Evilution::Feedback::Messages.cli_footer
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
require_relative "../../../version"
|
|
5
|
+
|
|
6
|
+
class Evilution::Reporter::CLI::LineFormatters::Header
|
|
7
|
+
def format(_summary)
|
|
8
|
+
"Evilution v#{Evilution::VERSION} — Mutation Testing Results"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
|
|
5
|
+
class Evilution::Reporter::CLI::LineFormatters::Mutations
|
|
6
|
+
def format(summary)
|
|
7
|
+
parts = "Mutations: #{summary.total} total, #{summary.killed} killed, " \
|
|
8
|
+
"#{summary.survived} survived, #{summary.timed_out} timed out"
|
|
9
|
+
parts += ", #{summary.neutral} neutral" if summary.neutral.positive?
|
|
10
|
+
parts += ", #{summary.equivalent} equivalent" if summary.equivalent.positive?
|
|
11
|
+
parts += ", #{summary.unresolved} unresolved" if summary.unresolved.positive?
|
|
12
|
+
parts += ", #{summary.unparseable} unparseable" if summary.unparseable.positive?
|
|
13
|
+
parts += ", #{summary.skipped} skipped" if summary.skipped.positive?
|
|
14
|
+
parts
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
|
|
5
|
+
class Evilution::Reporter::CLI::LineFormatters::PeakMemory
|
|
6
|
+
def format(summary)
|
|
7
|
+
peak = summary.peak_memory_mb
|
|
8
|
+
return nil unless peak
|
|
9
|
+
|
|
10
|
+
Kernel.format("Peak memory: %<mb>.1f MB", mb: peak)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
require_relative "../pct"
|
|
5
|
+
|
|
6
|
+
class Evilution::Reporter::CLI::LineFormatters::ResultLine
|
|
7
|
+
DEFAULT_MIN_SCORE = 0.8
|
|
8
|
+
|
|
9
|
+
def initialize(pct: Evilution::Reporter::CLI::Pct.new, min_score: DEFAULT_MIN_SCORE)
|
|
10
|
+
@pct = pct
|
|
11
|
+
@min_score = min_score
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def format(summary)
|
|
15
|
+
pass_fail = summary.success?(min_score: @min_score) ? "PASS" : "FAIL"
|
|
16
|
+
score_pct = @pct.format(summary.score)
|
|
17
|
+
threshold_pct = @pct.format(@min_score)
|
|
18
|
+
"Result: #{pass_fail} (score #{score_pct} #{pass_fail == "PASS" ? ">=" : "<"} #{threshold_pct})"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
require_relative "../pct"
|
|
5
|
+
|
|
6
|
+
class Evilution::Reporter::CLI::LineFormatters::Score
|
|
7
|
+
def initialize(pct: Evilution::Reporter::CLI::Pct.new)
|
|
8
|
+
@pct = pct
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def format(summary)
|
|
12
|
+
"Score: #{@pct.format(summary.score)} (#{summary.killed}/#{summary.score_denominator})"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
|
|
5
|
+
class Evilution::Reporter::CLI::LineFormatters::TruncationNotice
|
|
6
|
+
def format(summary)
|
|
7
|
+
return nil unless summary.truncated?
|
|
8
|
+
|
|
9
|
+
"[TRUNCATED] Stopped early due to --fail-fast"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../cli"
|
|
4
|
+
require_relative "line_formatters/mutations"
|
|
5
|
+
require_relative "line_formatters/score"
|
|
6
|
+
require_relative "line_formatters/duration"
|
|
7
|
+
require_relative "line_formatters/efficiency"
|
|
8
|
+
require_relative "line_formatters/peak_memory"
|
|
9
|
+
|
|
10
|
+
class Evilution::Reporter::CLI::MetricsBlock
|
|
11
|
+
DEFAULT_LINES = [
|
|
12
|
+
Evilution::Reporter::CLI::LineFormatters::Mutations.new,
|
|
13
|
+
Evilution::Reporter::CLI::LineFormatters::Score.new,
|
|
14
|
+
Evilution::Reporter::CLI::LineFormatters::Duration.new,
|
|
15
|
+
Evilution::Reporter::CLI::LineFormatters::Efficiency.new,
|
|
16
|
+
Evilution::Reporter::CLI::LineFormatters::PeakMemory.new
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
def initialize(lines: DEFAULT_LINES)
|
|
20
|
+
@lines = lines
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(summary)
|
|
24
|
+
@lines.filter_map { |line| line.format(summary) }
|
|
25
|
+
end
|
|
26
|
+
end
|