Class: Crspec::ProcessRunner

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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_examplesObject (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_examplesObject (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_examplesObject (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_durationObject (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.

Returns:

  • (Boolean)


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

Returns:

  • (Boolean)


121
122
123
# File 'lib/crspec/process_runner.rb', line 121

def success?
  @failed_examples.empty?
end