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
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
require_relative "../mutator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Registry
|
|
6
|
+
STRICT_EXTRA_OPERATORS = [
|
|
7
|
+
Evilution::Mutator::Operator::PredicateToNil
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
def self.for_profile(profile)
|
|
11
|
+
unless profile.is_a?(Symbol) || profile.is_a?(String)
|
|
12
|
+
raise ArgumentError, "unknown profile: #{profile.inspect} (expected :default or :strict)"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
case profile.to_sym
|
|
16
|
+
when :default then default
|
|
17
|
+
when :strict
|
|
18
|
+
registry = default
|
|
19
|
+
STRICT_EXTRA_OPERATORS.each { |op| registry.register(op) }
|
|
20
|
+
registry
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "unknown profile: #{profile.inspect} (expected :default or :strict)"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
6
26
|
def self.default
|
|
7
27
|
registry = new
|
|
8
28
|
[
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../channel"
|
|
4
|
+
|
|
5
|
+
module Evilution::Parallel::WorkQueue::Channel::Frame
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def encode(object)
|
|
9
|
+
payload = Marshal.dump(object)
|
|
10
|
+
[payload.bytesize].pack("N") + payload
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Marshal.load is safe here: payload originates from a sibling worker the
|
|
14
|
+
# parent itself forked, transferred over a private pipe inside our process
|
|
15
|
+
# tree. No external/untrusted input ever reaches this code. See
|
|
16
|
+
# .rubocop.yml (Security/MarshalLoad) for the full rationale.
|
|
17
|
+
def decode(header, payload)
|
|
18
|
+
return nil if header.nil? || header.bytesize < 4
|
|
19
|
+
|
|
20
|
+
length = header.unpack1("N")
|
|
21
|
+
return nil if payload.nil? || payload.bytesize < length
|
|
22
|
+
|
|
23
|
+
Marshal.load(payload)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../work_queue"
|
|
4
|
+
|
|
5
|
+
module Evilution::Parallel::WorkQueue::Channel
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def write(io, object)
|
|
9
|
+
io.write(Frame.encode(object))
|
|
10
|
+
io.flush
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read(io)
|
|
14
|
+
header = io.read(4)
|
|
15
|
+
return nil if header.nil? || header.bytesize < 4
|
|
16
|
+
|
|
17
|
+
length = header.unpack1("N")
|
|
18
|
+
payload = io.read(length)
|
|
19
|
+
Frame.decode(header, payload)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
require_relative "channel/frame"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../work_queue"
|
|
4
|
+
|
|
5
|
+
# CollectionState is a top-level private constant on WorkQueue (not under a
|
|
6
|
+
# sub-namespace) so Dispatcher accesses it via const_get.
|
|
7
|
+
class Evilution::Parallel::WorkQueue
|
|
8
|
+
CollectionState = Struct.new(:results, :in_flight, :next_index, :first_error) do
|
|
9
|
+
def initialize(item_count)
|
|
10
|
+
super(Array.new(item_count), 0, 0, nil)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
private_constant :CollectionState
|
|
14
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../work_queue"
|
|
4
|
+
require_relative "collection_state"
|
|
5
|
+
|
|
6
|
+
class Evilution::Parallel::WorkQueue::Dispatcher
|
|
7
|
+
attr_reader :first_error
|
|
8
|
+
|
|
9
|
+
def initialize(workers:, items:, prefetch:, item_timeout:, worker_max_items:, recycle_factory:)
|
|
10
|
+
@workers = workers
|
|
11
|
+
@items = items
|
|
12
|
+
@prefetch = prefetch
|
|
13
|
+
@item_timeout = item_timeout
|
|
14
|
+
@worker_max_items = worker_max_items
|
|
15
|
+
@recycle_factory = recycle_factory
|
|
16
|
+
@state = Evilution::Parallel::WorkQueue.send(:const_get, :CollectionState).new(items.length)
|
|
17
|
+
@retired = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
seed
|
|
22
|
+
collect
|
|
23
|
+
@first_error = @state.first_error
|
|
24
|
+
[@state.results, @retired]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def seed
|
|
30
|
+
@prefetch.times do
|
|
31
|
+
@workers.each do |w|
|
|
32
|
+
break unless more_to_send?
|
|
33
|
+
|
|
34
|
+
send_item(w)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def collect
|
|
40
|
+
io_to_worker = @workers.to_h { |w| [w.res_io, w] }
|
|
41
|
+
result_ios = io_to_worker.keys
|
|
42
|
+
|
|
43
|
+
while @state.in_flight.positive?
|
|
44
|
+
readable, = IO.select(result_ios, nil, nil, @item_timeout)
|
|
45
|
+
|
|
46
|
+
if readable.nil?
|
|
47
|
+
terminate_stuck
|
|
48
|
+
@state.first_error ||= Evilution::Error.new("worker timed out after #{@item_timeout}s")
|
|
49
|
+
break
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
readable.each do |io|
|
|
53
|
+
alive = handle(io_to_worker[io], io_to_worker, result_ios)
|
|
54
|
+
result_ios.delete(io) unless alive
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def handle(worker, io_to_worker, result_ios)
|
|
60
|
+
message = worker.read_result
|
|
61
|
+
return handle_dead(worker) if message.nil?
|
|
62
|
+
|
|
63
|
+
record(message, worker)
|
|
64
|
+
return false if recycle_and_dispatch(worker, io_to_worker, result_ios)
|
|
65
|
+
return true if draining_for_recycle?(worker)
|
|
66
|
+
|
|
67
|
+
send_item(worker) if more_to_send? && @state.first_error.nil?
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def record(message, worker)
|
|
72
|
+
index, status, value = message
|
|
73
|
+
@state.first_error = value if status == :error && @state.first_error.nil?
|
|
74
|
+
@state.results[index] = value if status == :ok
|
|
75
|
+
@state.in_flight -= 1
|
|
76
|
+
worker.pending -= 1
|
|
77
|
+
worker.items_completed += 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handle_dead(worker)
|
|
81
|
+
@state.first_error ||= Evilution::Error.new("worker process exited unexpectedly")
|
|
82
|
+
@state.in_flight -= worker.pending
|
|
83
|
+
worker.pending = 0
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def draining_for_recycle?(worker)
|
|
88
|
+
@worker_max_items && worker.items_completed >= @worker_max_items && worker.pending.positive?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def should_recycle?(worker)
|
|
92
|
+
return false unless @worker_max_items
|
|
93
|
+
return false if worker.items_completed < @worker_max_items
|
|
94
|
+
return false unless worker.pending.zero?
|
|
95
|
+
return false unless more_to_send?
|
|
96
|
+
|
|
97
|
+
@state.first_error.nil?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def recycle_and_dispatch(worker, io_to_worker, result_ios)
|
|
101
|
+
return false unless should_recycle?(worker)
|
|
102
|
+
|
|
103
|
+
new_worker = recycle(worker, io_to_worker, result_ios)
|
|
104
|
+
send_item(new_worker) if more_to_send?
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def recycle(old_worker, io_to_worker, result_ios)
|
|
109
|
+
io_to_worker.delete(old_worker.res_io)
|
|
110
|
+
result_ios.delete(old_worker.res_io)
|
|
111
|
+
@retired << old_worker.retire
|
|
112
|
+
|
|
113
|
+
new_worker = @recycle_factory.call(old_worker)
|
|
114
|
+
@workers[@workers.index(old_worker)] = new_worker
|
|
115
|
+
io_to_worker[new_worker.res_io] = new_worker
|
|
116
|
+
result_ios << new_worker.res_io
|
|
117
|
+
new_worker
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def send_item(worker)
|
|
121
|
+
worker.send_item(@state.next_index, @items[@state.next_index])
|
|
122
|
+
@state.next_index += 1
|
|
123
|
+
@state.in_flight += 1
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def more_to_send?
|
|
127
|
+
@state.next_index < @items.length
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def terminate_stuck
|
|
131
|
+
@workers.each(&:kill)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validators"
|
|
4
|
+
|
|
5
|
+
class Evilution::Parallel::WorkQueue::Validators::OptionalPositiveInt
|
|
6
|
+
def self.call!(name, value)
|
|
7
|
+
return if value.nil? || (value.is_a?(Integer) && value.positive?)
|
|
8
|
+
|
|
9
|
+
raise ArgumentError, "#{name} must be nil or a positive integer, got #{value.inspect}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validators"
|
|
4
|
+
|
|
5
|
+
class Evilution::Parallel::WorkQueue::Validators::OptionalPositiveNumber
|
|
6
|
+
def self.call!(name, value)
|
|
7
|
+
return if value.nil? || (value.is_a?(Numeric) && value.positive?)
|
|
8
|
+
|
|
9
|
+
raise ArgumentError, "#{name} must be nil or a positive number, got #{value.inspect}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validators"
|
|
4
|
+
|
|
5
|
+
class Evilution::Parallel::WorkQueue::Validators::PositiveInt
|
|
6
|
+
def self.call!(name, value)
|
|
7
|
+
return if value.is_a?(Integer) && value >= 1
|
|
8
|
+
|
|
9
|
+
raise ArgumentError, "#{name} must be a positive integer, got #{value.inspect}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../worker"
|
|
4
|
+
require_relative "../channel"
|
|
5
|
+
require_relative "../channel/frame"
|
|
6
|
+
|
|
7
|
+
module Evilution::Parallel::WorkQueue::Worker::Loop
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def run(cmd_io, res_io, hooks: nil, &block)
|
|
11
|
+
hooks.fire(:worker_process_start) if hooks
|
|
12
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
13
|
+
busy = 0.0
|
|
14
|
+
|
|
15
|
+
loop do
|
|
16
|
+
data = Evilution::Parallel::WorkQueue::Channel.read(cmd_io)
|
|
17
|
+
break if data.nil? || data == Evilution::Parallel::WorkQueue::SHUTDOWN
|
|
18
|
+
|
|
19
|
+
busy += run_one(res_io, data, &block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
wall = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
23
|
+
Evilution::Parallel::WorkQueue::Channel.write(
|
|
24
|
+
res_io, [Evilution::Parallel::WorkQueue::STATS, busy, wall]
|
|
25
|
+
)
|
|
26
|
+
ensure
|
|
27
|
+
cmd_io.close unless cmd_io.closed?
|
|
28
|
+
res_io.close unless res_io.closed?
|
|
29
|
+
exit!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run_one(res_io, data, &block)
|
|
33
|
+
index, item = data
|
|
34
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
35
|
+
begin
|
|
36
|
+
result = block.call(item)
|
|
37
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
38
|
+
Evilution::Parallel::WorkQueue::Channel.write(res_io, [index, :ok, result])
|
|
39
|
+
rescue StandardError, ScriptError, SystemStackError, NoMemoryError, SecurityError => e
|
|
40
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
41
|
+
Evilution::Parallel::WorkQueue::Channel.write(res_io, [index, :error, e])
|
|
42
|
+
end
|
|
43
|
+
elapsed
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../work_queue"
|
|
4
|
+
require_relative "../../child_output"
|
|
5
|
+
require_relative "channel"
|
|
6
|
+
require_relative "channel/frame"
|
|
7
|
+
|
|
8
|
+
class Evilution::Parallel::WorkQueue::Worker
|
|
9
|
+
attr_reader :pid, :worker_index
|
|
10
|
+
attr_accessor :items_completed, :pending, :busy_time, :wall_time
|
|
11
|
+
|
|
12
|
+
def self.spawn(worker_index:, hooks:, &block)
|
|
13
|
+
cmd_read, cmd_write = IO.pipe
|
|
14
|
+
res_read, res_write = IO.pipe
|
|
15
|
+
[cmd_read, cmd_write, res_read, res_write].each(&:binmode)
|
|
16
|
+
|
|
17
|
+
pid = Process.fork do
|
|
18
|
+
cmd_write.close
|
|
19
|
+
res_read.close
|
|
20
|
+
ENV["TEST_ENV_NUMBER"] = test_env_number_for(worker_index)
|
|
21
|
+
Evilution::ChildOutput.redirect!
|
|
22
|
+
Loop.run(cmd_read, res_write, hooks: hooks, &block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
cmd_read.close
|
|
26
|
+
res_write.close
|
|
27
|
+
new(pid: pid, cmd_write: cmd_write, res_read: res_read, worker_index: worker_index)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# EV-kdns / GH #817: translate 0-based worker slot to parallel_tests'
|
|
31
|
+
# TEST_ENV_NUMBER convention ("" for slot 0, "2" for slot 1, ...). Rails
|
|
32
|
+
# apps interpolating TEST_ENV_NUMBER into database.yml get per-worker
|
|
33
|
+
# SQLite files, avoiding lock contention under jobs > 1.
|
|
34
|
+
def self.test_env_number_for(worker_index)
|
|
35
|
+
worker_index.zero? ? "" : (worker_index + 1).to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(pid:, cmd_write:, res_read:, worker_index:)
|
|
39
|
+
@pid = pid
|
|
40
|
+
@cmd_write = cmd_write
|
|
41
|
+
@res_read = res_read
|
|
42
|
+
@worker_index = worker_index
|
|
43
|
+
@items_completed = 0
|
|
44
|
+
@pending = 0
|
|
45
|
+
@busy_time = 0.0
|
|
46
|
+
@wall_time = 0.0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def res_io
|
|
50
|
+
@res_read
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def send_item(index, item)
|
|
54
|
+
Evilution::Parallel::WorkQueue::Channel.write(@cmd_write, [index, item])
|
|
55
|
+
@pending += 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def read_result
|
|
59
|
+
Evilution::Parallel::WorkQueue::Channel.read(@res_read)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def shutdown
|
|
63
|
+
Evilution::Parallel::WorkQueue::Channel.write(@cmd_write, Evilution::Parallel::WorkQueue::SHUTDOWN)
|
|
64
|
+
rescue Errno::EPIPE
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def kill
|
|
69
|
+
Process.kill("KILL", @pid)
|
|
70
|
+
rescue Errno::ESRCH
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close_pipes
|
|
75
|
+
@cmd_write.close unless @cmd_write.closed?
|
|
76
|
+
@res_read.close unless @res_read.closed?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def reap
|
|
80
|
+
Process.wait(@pid)
|
|
81
|
+
rescue Errno::ECHILD
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def retire
|
|
86
|
+
shutdown
|
|
87
|
+
busy, wall = drain_stats
|
|
88
|
+
close_pipes
|
|
89
|
+
reap
|
|
90
|
+
@busy_time = busy
|
|
91
|
+
@wall_time = wall
|
|
92
|
+
to_stat
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_stat
|
|
96
|
+
Evilution::Parallel::WorkQueue::WorkerStat.new(
|
|
97
|
+
@pid, @items_completed, @busy_time || 0.0, @wall_time || 0.0
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def drain_stats
|
|
104
|
+
return [0.0, 0.0] unless @res_read.wait_readable(Evilution::Parallel::WorkQueue::TIMING_GRACE_PERIOD)
|
|
105
|
+
|
|
106
|
+
message = read_result
|
|
107
|
+
return [0.0, 0.0] if message.nil?
|
|
108
|
+
|
|
109
|
+
tag, busy, wall = message
|
|
110
|
+
return [0.0, 0.0] unless tag == Evilution::Parallel::WorkQueue::STATS
|
|
111
|
+
|
|
112
|
+
[busy, wall]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../work_queue"
|
|
4
|
+
|
|
5
|
+
class Evilution::Parallel::WorkQueue
|
|
6
|
+
WorkerStat = Struct.new(:pid, :items_completed, :busy_time, :wall_time) do
|
|
7
|
+
def idle_time
|
|
8
|
+
wall_time - busy_time
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def utilization
|
|
12
|
+
return 0.0 if wall_time.nil? || wall_time.zero?
|
|
13
|
+
|
|
14
|
+
busy_time / wall_time
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|