evilution 0.26.0 → 0.27.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 +10 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +22 -0
- data/README.md +57 -3
- 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 +16 -2
- 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/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 +78 -268
- 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/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 +35 -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 +7 -2
- 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 -261
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- data/lib/evilution/mutator/operator/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -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/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/runner/isolation_resolver.rb +9 -2
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
- data/lib/evilution/runner/mutation_executor.rb +58 -289
- data/lib/evilution/runner.rb +21 -0
- data/lib/evilution/version.rb +1 -1
- metadata +113 -2
|
@@ -4,7 +4,7 @@ require_relative "../operator"
|
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::BlockRemoval < Evilution::Mutator::Base
|
|
6
6
|
def visit_call_node(node)
|
|
7
|
-
if node.block
|
|
7
|
+
if node.block && !node.block.is_a?(Prism::BlockArgumentNode)
|
|
8
8
|
block_node = node.block
|
|
9
9
|
call_end = block_node.location.start_offset
|
|
10
10
|
call_start = node.location.start_offset
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::Base
|
|
6
|
-
|
|
6
|
+
ALWAYS_SAFE_REPLACEMENTS = %w[nil self].freeze
|
|
7
|
+
SUPER_REPLACEMENT = "super"
|
|
7
8
|
|
|
8
9
|
def visit_def_node(node)
|
|
9
10
|
if node.body
|
|
10
|
-
|
|
11
|
+
replacements = ALWAYS_SAFE_REPLACEMENTS.dup
|
|
12
|
+
replacements << SUPER_REPLACEMENT if body_calls_super?(node.body)
|
|
13
|
+
|
|
14
|
+
replacements.each do |replacement|
|
|
11
15
|
add_mutation(
|
|
12
16
|
offset: node.body.location.start_offset,
|
|
13
17
|
length: node.body.location.length,
|
|
@@ -19,4 +23,16 @@ class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::
|
|
|
19
23
|
|
|
20
24
|
super
|
|
21
25
|
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# The bare-super replacement raises NoMethodError at runtime when the enclosing
|
|
30
|
+
# class has no parent implementation of the method. We emit it only when the
|
|
31
|
+
# original body already calls super, using that as a heuristic that a super
|
|
32
|
+
# target is intended in this context.
|
|
33
|
+
def body_calls_super?(node)
|
|
34
|
+
return true if node.is_a?(Prism::SuperNode) || node.is_a?(Prism::ForwardingSuperNode)
|
|
35
|
+
|
|
36
|
+
node.child_nodes.any? { |child| child && body_calls_super?(child) }
|
|
37
|
+
end
|
|
22
38
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
def decode(header, payload)
|
|
14
|
+
return nil if header.nil? || header.bytesize < 4
|
|
15
|
+
|
|
16
|
+
length = header.unpack1("N")
|
|
17
|
+
return nil if payload.nil? || payload.bytesize < length
|
|
18
|
+
|
|
19
|
+
Marshal.load(payload) # rubocop:disable Security/MarshalLoad
|
|
20
|
+
end
|
|
21
|
+
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 Exception => e # rubocop:disable Lint/RescueException
|
|
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
|