evilution 0.25.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.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +15 -0
  3. data/.claude/prompts/architect.md +14 -1
  4. data/.claude/skills/create-issue/SKILL.md +55 -0
  5. data/.rubocop_todo.yml +7 -0
  6. data/CHANGELOG.md +38 -0
  7. data/README.md +57 -3
  8. data/lib/evilution/ast/constant_names.rb +34 -0
  9. data/lib/evilution/cache.rb +2 -0
  10. data/lib/evilution/child_output.rb +24 -0
  11. data/lib/evilution/cli/commands/run.rb +9 -0
  12. data/lib/evilution/cli/commands/version.rb +2 -0
  13. data/lib/evilution/cli/parser/options_builder.rb +16 -2
  14. data/lib/evilution/compare/invalid_input.rb +12 -0
  15. data/lib/evilution/compare.rb +1 -10
  16. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  17. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  18. data/lib/evilution/config/builders.rb +4 -0
  19. data/lib/evilution/config/env_loader.rb +12 -0
  20. data/lib/evilution/config/file_loader.rb +22 -0
  21. data/lib/evilution/config/sources.rb +14 -0
  22. data/lib/evilution/config/validators/base.rb +37 -0
  23. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  24. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  25. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  26. data/lib/evilution/config/validators/hooks.rb +12 -0
  27. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  28. data/lib/evilution/config/validators/integration.rb +11 -0
  29. data/lib/evilution/config/validators/isolation.rb +19 -0
  30. data/lib/evilution/config/validators/jobs.rb +9 -0
  31. data/lib/evilution/config/validators/preload.rb +13 -0
  32. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  33. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  34. data/lib/evilution/config/validators.rb +4 -0
  35. data/lib/evilution/config.rb +78 -268
  36. data/lib/evilution/feedback/detector.rb +15 -0
  37. data/lib/evilution/feedback/messages.rb +42 -0
  38. data/lib/evilution/feedback.rb +5 -0
  39. data/lib/evilution/integration/base.rb +4 -155
  40. data/lib/evilution/integration/loading/concern_state_cleaner.rb +49 -0
  41. data/lib/evilution/integration/loading/constant_pinner.rb +24 -0
  42. data/lib/evilution/integration/loading/mutation_applier.rb +52 -0
  43. data/lib/evilution/integration/loading/redefinition_recovery.rb +54 -0
  44. data/lib/evilution/integration/loading/source_evaluator.rb +15 -0
  45. data/lib/evilution/integration/loading/syntax_validator.rb +19 -0
  46. data/lib/evilution/integration/loading.rb +6 -0
  47. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  48. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  49. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  50. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  51. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  52. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  53. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  54. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +35 -0
  55. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  58. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  59. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  60. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  61. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  62. data/lib/evilution/integration/rspec.rb +61 -232
  63. data/lib/evilution/isolation/fork.rb +7 -2
  64. data/lib/evilution/load_path/subpath_resolver.rb +25 -0
  65. data/lib/evilution/load_path.rb +4 -0
  66. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  67. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  68. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  69. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  70. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  71. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  72. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  73. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  75. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  76. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  77. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  78. data/lib/evilution/mcp/info_tool.rb +43 -261
  79. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  80. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  81. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  82. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  83. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  84. data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -0
  85. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  86. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  87. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  88. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  89. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  90. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  91. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  92. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  93. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  94. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  95. data/lib/evilution/parallel/work_queue.rb +42 -327
  96. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  97. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  98. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  99. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  100. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  101. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  102. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  103. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  104. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  105. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  106. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  107. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  108. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  109. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  110. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  111. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  112. data/lib/evilution/reporter/cli/pct.rb +9 -0
  113. data/lib/evilution/reporter/cli/section.rb +13 -0
  114. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  115. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  116. data/lib/evilution/reporter/cli.rb +79 -162
  117. data/lib/evilution/runner/isolation_resolver.rb +20 -2
  118. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
  119. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
  120. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
  121. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
  122. data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
  123. data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
  124. data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
  125. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
  126. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
  127. data/lib/evilution/runner/mutation_executor.rb +58 -289
  128. data/lib/evilution/runner/subject_pipeline.rb +18 -8
  129. data/lib/evilution/runner.rb +21 -0
  130. data/lib/evilution/version.rb +1 -1
  131. metadata +125 -5
  132. data/lib/evilution/mcp/session_diff_tool.rb +0 -63
  133. data/lib/evilution/mcp/session_list_tool.rb +0 -50
  134. data/lib/evilution/mcp/session_show_tool.rb +0 -57
@@ -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
- REPLACEMENTS = %w[nil self super].freeze
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
- REPLACEMENTS.each do |replacement|
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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../work_queue"
4
+
5
+ module Evilution::Parallel::WorkQueue::Validators
6
+ 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