Class: Crspec::ProcessRunner
- Inherits:
-
Object
- Object
- Crspec::ProcessRunner
- Defined in:
- lib/crspec/process_runner.rb
Overview
Process tier for true multi-core execution (--processes P). The parent loads specs once (copy-on-write memory), forks P children, and shards examples across them using persisted timings (bin-packing, slowest-first; round-robin on first run). Each child runs the existing N-thread x M-fiber Runner on its shard and streams marshalled result structs back over a pipe (example blocks cannot cross process boundaries). Fail-fast propagates via SIGTERM.
Defined Under Namespace
Modules: FilteredGroups Classes: ChildFormatter, Result
Instance Attribute Summary collapse
-
#failed_examples ⇒ Object
readonly
Returns the value of attribute failed_examples.
-
#passed_examples ⇒ Object
readonly
Returns the value of attribute passed_examples.
-
#pending_examples ⇒ Object
readonly
Returns the value of attribute pending_examples.
-
#total_duration ⇒ Object
readonly
Returns the value of attribute total_duration.
Class Method Summary collapse
-
.fork_supported? ⇒ Boolean
Forking is only meaningful (and only available) on runtimes with a GVL, i.e.
Instance Method Summary collapse
-
#initialize(processes:, concurrency: Etc.nprocessors, fibers: 1, formatter: nil, fail_fast: false, seed: nil, only_failures: false, persistence_path: nil) ⇒ ProcessRunner
constructor
A new instance of ProcessRunner.
- #run(example_groups) ⇒ Object
- #success? ⇒ Boolean
Constructor Details
#initialize(processes:, concurrency: Etc.nprocessors, fibers: 1, formatter: nil, fail_fast: false, seed: nil, only_failures: false, persistence_path: nil) ⇒ ProcessRunner
Returns a new instance of ProcessRunner.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/crspec/process_runner.rb', line 28 def initialize(processes:, concurrency: Etc.nprocessors, fibers: 1, formatter: nil, fail_fast: false, seed: nil, only_failures: false, persistence_path: nil) unless self.class.fork_supported? raise Crspec::Error, <<~MSG --processes requires fork, which #{RUBY_ENGINE} does not support. On #{RUBY_ENGINE} threads are not limited by a GVL, so worker threads already use all cores: use -c/--concurrency instead (e.g. `crspec -c #{Etc.nprocessors}`). MSG end @processes = processes == :auto ? physical_core_count : processes @concurrency = concurrency @fibers = fibers @formatter = formatter || Formatters::ProgressFormatter.new @fail_fast = fail_fast == true ? 1 : fail_fast @seed = seed @only_failures = only_failures @persistence_path = persistence_path @persistence = StatusPersistence.new(persistence_path) @passed_examples = [] @failed_examples = [] @pending_examples = [] @total_duration = 0 end |
Instance Attribute Details
#failed_examples ⇒ Object (readonly)
Returns the value of attribute failed_examples.
18 19 20 |
# File 'lib/crspec/process_runner.rb', line 18 def failed_examples @failed_examples end |
#passed_examples ⇒ Object (readonly)
Returns the value of attribute passed_examples.
18 19 20 |
# File 'lib/crspec/process_runner.rb', line 18 def passed_examples @passed_examples end |
#pending_examples ⇒ Object (readonly)
Returns the value of attribute pending_examples.
18 19 20 |
# File 'lib/crspec/process_runner.rb', line 18 def pending_examples @pending_examples end |
#total_duration ⇒ Object (readonly)
Returns the value of attribute total_duration.
18 19 20 |
# File 'lib/crspec/process_runner.rb', line 18 def total_duration @total_duration end |
Class Method Details
.fork_supported? ⇒ Boolean
Forking is only meaningful (and only available) on runtimes with a
GVL, i.e. CRuby. On JRuby/TruffleRuby threads already use all cores,
so -c N is the multi-core tier there.
23 24 25 26 |
# File 'lib/crspec/process_runner.rb', line 23 def self.fork_supported? Process.respond_to?(:fork) && !Process.method(:fork).nil? && RUBY_ENGINE == "ruby" end |
Instance Method Details
#run(example_groups) ⇒ Object
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/crspec/process_runner.rb', line 54 def run(example_groups) start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) @formatter.start example_groups.each(&:finalize!) examples = [] example_groups.each { |group| collect_examples(group, examples) } previous = @persistence.load if @only_failures examples = examples.select do |ex| entry = previous[ex.persistence_key] entry.nil? || entry["status"] == "failed" end end shards = shard_examples(examples, previous) Process.warmup if Process.respond_to?(:warmup) children = shards.each_with_index.filter_map do |shard, index| next if shard.empty? spawn_child(shard, index + 1, example_groups) end failure_total = 0 aborted = false readers = children.to_h { |c| [c[:reader], c] } until readers.empty? ready, = IO.select(readers.keys) ready.each do |io| result = read_result(io) if result.nil? readers.delete(io) io.close next end record(result) next unless result.status == "failed" && @fail_fast failure_total += 1 next if aborted || failure_total < @fail_fast aborted = true children.each do |c| Process.kill("TERM", c[:pid]) rescue Errno::ESRCH nil end end end children.each do |c| Process.wait(c[:pid]) rescue Errno::ECHILD nil end @total_duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time persist_results(previous) @formatter.finish self end |
#success? ⇒ Boolean
121 122 123 |
# File 'lib/crspec/process_runner.rb', line 121 def success? @failed_examples.empty? end |