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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +23 -0
  3. data/.rubocop_todo.yml +6 -0
  4. data/CHANGELOG.md +54 -0
  5. data/README.md +76 -3
  6. data/lib/evilution/baseline.rb +5 -4
  7. data/lib/evilution/cache.rb +2 -0
  8. data/lib/evilution/child_output.rb +24 -0
  9. data/lib/evilution/cli/commands/run.rb +9 -0
  10. data/lib/evilution/cli/commands/version.rb +2 -0
  11. data/lib/evilution/cli/parser/options_builder.rb +23 -2
  12. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  13. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  14. data/lib/evilution/compare/diff_extractor.rb +6 -0
  15. data/lib/evilution/compare/fingerprint.rb +15 -72
  16. data/lib/evilution/compare/line_normalizer.rb +72 -0
  17. data/lib/evilution/compare/normalizer.rb +17 -4
  18. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  19. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  20. data/lib/evilution/config/builders.rb +4 -0
  21. data/lib/evilution/config/env_loader.rb +12 -0
  22. data/lib/evilution/config/file_loader.rb +22 -0
  23. data/lib/evilution/config/sources.rb +14 -0
  24. data/lib/evilution/config/validators/base.rb +37 -0
  25. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  26. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  27. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  28. data/lib/evilution/config/validators/hooks.rb +12 -0
  29. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  30. data/lib/evilution/config/validators/integration.rb +11 -0
  31. data/lib/evilution/config/validators/isolation.rb +19 -0
  32. data/lib/evilution/config/validators/jobs.rb +9 -0
  33. data/lib/evilution/config/validators/preload.rb +13 -0
  34. data/lib/evilution/config/validators/profile.rb +11 -0
  35. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  36. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  37. data/lib/evilution/config/validators.rb +4 -0
  38. data/lib/evilution/config.rb +93 -266
  39. data/lib/evilution/feedback/detector.rb +15 -0
  40. data/lib/evilution/feedback/messages.rb +42 -0
  41. data/lib/evilution/feedback.rb +5 -0
  42. data/lib/evilution/integration/crash_detector.rb +2 -2
  43. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  44. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  45. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  46. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  47. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  48. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  49. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  50. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  51. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  52. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
  53. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  54. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  55. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  58. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  59. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  60. data/lib/evilution/integration/rspec.rb +61 -232
  61. data/lib/evilution/isolation/fork.rb +23 -13
  62. data/lib/evilution/isolation/in_process.rb +10 -6
  63. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  64. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  65. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  66. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  67. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  68. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  69. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  70. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  71. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  72. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  73. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  75. data/lib/evilution/mcp/info_tool.rb +43 -263
  76. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  77. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  78. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  79. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  80. data/lib/evilution/mcp/session_tool.rb +0 -2
  81. data/lib/evilution/mutation.rb +47 -27
  82. data/lib/evilution/mutator/base.rb +8 -8
  83. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  84. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  85. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  86. data/lib/evilution/mutator/registry.rb +20 -0
  87. data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
  88. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  89. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  90. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  91. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  92. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  93. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  94. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  95. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  96. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  97. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  98. data/lib/evilution/parallel/work_queue.rb +42 -327
  99. data/lib/evilution/process_cleanup.rb +19 -0
  100. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  101. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  102. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  103. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  104. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  105. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  106. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  107. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  108. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  109. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  110. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  111. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  112. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  113. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  114. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  115. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  116. data/lib/evilution/reporter/cli/pct.rb +9 -0
  117. data/lib/evilution/reporter/cli/section.rb +13 -0
  118. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  119. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  120. data/lib/evilution/reporter/cli.rb +79 -162
  121. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  122. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  123. data/lib/evilution/reporter/html/escape.rb +1 -1
  124. data/lib/evilution/reporter/html/section.rb +1 -1
  125. data/lib/evilution/reporter/html/sections.rb +4 -2
  126. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  127. data/lib/evilution/reporter/html.rb +8 -3
  128. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  129. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  130. data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
  131. data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
  132. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  133. data/lib/evilution/result/error_info.rb +20 -0
  134. data/lib/evilution/result/memory_stats.rb +20 -0
  135. data/lib/evilution/result/mutation_result.rb +30 -14
  136. data/lib/evilution/runner/baseline_runner.rb +1 -2
  137. data/lib/evilution/runner/diagnostics.rb +1 -2
  138. data/lib/evilution/runner/isolation_resolver.rb +10 -4
  139. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
  140. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
  141. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
  142. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
  143. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  144. data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
  145. data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
  146. data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
  147. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
  148. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
  149. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  150. data/lib/evilution/runner/mutation_executor.rb +53 -292
  151. data/lib/evilution/runner/mutation_planner.rb +1 -2
  152. data/lib/evilution/runner/report_publisher.rb +1 -2
  153. data/lib/evilution/runner/subject_pipeline.rb +1 -2
  154. data/lib/evilution/runner.rb +53 -30
  155. data/lib/evilution/version.rb +1 -1
  156. data/lib/evilution.rb +1 -0
  157. data/script/memory_check +3 -1
  158. metadata +125 -3
  159. 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,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 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