ruptr 0.1.3
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 +7 -0
- data/bin/ruptr +10 -0
- data/lib/ruptr/adapters/assertions.rb +43 -0
- data/lib/ruptr/adapters/rr.rb +23 -0
- data/lib/ruptr/adapters/rspec_expect.rb +32 -0
- data/lib/ruptr/adapters/rspec_mocks.rb +29 -0
- data/lib/ruptr/adapters.rb +7 -0
- data/lib/ruptr/assertions.rb +493 -0
- data/lib/ruptr/autorun.rb +38 -0
- data/lib/ruptr/capture_output.rb +106 -0
- data/lib/ruptr/compat.rb +27 -0
- data/lib/ruptr/exceptions.rb +47 -0
- data/lib/ruptr/formatter.rb +78 -0
- data/lib/ruptr/golden_master.rb +143 -0
- data/lib/ruptr/instance.rb +37 -0
- data/lib/ruptr/main.rb +439 -0
- data/lib/ruptr/minitest/override.rb +4 -0
- data/lib/ruptr/minitest.rb +134 -0
- data/lib/ruptr/plain.rb +425 -0
- data/lib/ruptr/progress.rb +98 -0
- data/lib/ruptr/rake_task.rb +18 -0
- data/lib/ruptr/report.rb +104 -0
- data/lib/ruptr/result.rb +38 -0
- data/lib/ruptr/rspec/configuration.rb +191 -0
- data/lib/ruptr/rspec/example_group.rb +498 -0
- data/lib/ruptr/rspec/override.rb +4 -0
- data/lib/ruptr/rspec.rb +211 -0
- data/lib/ruptr/runner.rb +433 -0
- data/lib/ruptr/sink.rb +58 -0
- data/lib/ruptr/stringified.rb +57 -0
- data/lib/ruptr/suite.rb +188 -0
- data/lib/ruptr/surrogate_exception.rb +71 -0
- data/lib/ruptr/tabular.rb +21 -0
- data/lib/ruptr/tap.rb +51 -0
- data/lib/ruptr/testunit/override.rb +4 -0
- data/lib/ruptr/testunit.rb +117 -0
- data/lib/ruptr/timing_cache.rb +100 -0
- data/lib/ruptr/tty_colors.rb +60 -0
- data/lib/ruptr/utils.rb +57 -0
- metadata +77 -0
data/lib/ruptr/rspec.rb
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'suite'
|
|
4
|
+
require_relative 'compat'
|
|
5
|
+
require_relative 'autorun'
|
|
6
|
+
require_relative 'rspec/example_group'
|
|
7
|
+
require_relative 'rspec/configuration'
|
|
8
|
+
|
|
9
|
+
module Ruptr
|
|
10
|
+
class Compat
|
|
11
|
+
class RSpec < self
|
|
12
|
+
def default_project_load_paths = %w[spec]
|
|
13
|
+
|
|
14
|
+
def default_project_test_globs = %w[spec/**/*_spec.rb]
|
|
15
|
+
|
|
16
|
+
def global_install!
|
|
17
|
+
if Object.const_defined?(:RSpec)
|
|
18
|
+
return if Object.const_get(:RSpec) == @adapter_module
|
|
19
|
+
fail "rspec already loaded!"
|
|
20
|
+
end
|
|
21
|
+
Object.const_set(:RSpec, adapter_module)
|
|
22
|
+
this = self
|
|
23
|
+
m = Module.new do
|
|
24
|
+
define_method(:require) do |name|
|
|
25
|
+
name = name.to_path unless name.is_a?(String)
|
|
26
|
+
if name.start_with?('rspec/')
|
|
27
|
+
case name.delete_prefix('rspec/')
|
|
28
|
+
when 'version',
|
|
29
|
+
'support',
|
|
30
|
+
%r{\Asupport/},
|
|
31
|
+
'matchers',
|
|
32
|
+
%r{\Amatchers/},
|
|
33
|
+
'expectations',
|
|
34
|
+
%r{\Aexpectations/},
|
|
35
|
+
'mocks',
|
|
36
|
+
%r{\Amocks/}
|
|
37
|
+
nil
|
|
38
|
+
when 'core'
|
|
39
|
+
return
|
|
40
|
+
when 'autorun'
|
|
41
|
+
this.schedule_autorun!
|
|
42
|
+
return
|
|
43
|
+
else
|
|
44
|
+
fail "#{self.class.name}: unknown rspec library: #{name}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
super(name)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
Kernel.prepend(m)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def global_monkey_patch!
|
|
54
|
+
a = adapter_module
|
|
55
|
+
m = Module.new do
|
|
56
|
+
extend Forwardable
|
|
57
|
+
define_method(:ruptr_rspec_adapter) { a }
|
|
58
|
+
def_delegators :ruptr_rspec_adapter,
|
|
59
|
+
:describe, :context,
|
|
60
|
+
:shared_examples, :shared_examples_for, :shared_context
|
|
61
|
+
end
|
|
62
|
+
TOPLEVEL_BINDING.receiver.extend(m)
|
|
63
|
+
Module.prepend(m)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_default_frameworks
|
|
67
|
+
adapter_module.configure do |config|
|
|
68
|
+
if config.expectation_frameworks.empty? && config.mock_frameworks.empty? ||
|
|
69
|
+
config.expectation_frameworks.include?(:rspec) && config.mock_frameworks.include?(:rspec)
|
|
70
|
+
begin
|
|
71
|
+
config.expect_with(:rspec)
|
|
72
|
+
rescue LoadError
|
|
73
|
+
end
|
|
74
|
+
begin
|
|
75
|
+
config.mock_with(:rspec)
|
|
76
|
+
rescue LoadError
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def finalize_configuration!
|
|
83
|
+
load_default_frameworks
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class Adapter < Module
|
|
87
|
+
extend Forwardable
|
|
88
|
+
|
|
89
|
+
def configuration = @configuration ||= Configuration.new(self)
|
|
90
|
+
|
|
91
|
+
def configure = yield configuration
|
|
92
|
+
|
|
93
|
+
def_delegators :root_example_group,
|
|
94
|
+
:example_group, :describe, :context,
|
|
95
|
+
:fdescribe, :fcontext, :xdescribe, :xcontext,
|
|
96
|
+
:shared_examples, :shared_examples_for, :shared_context,
|
|
97
|
+
:before, :after
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def adapter_module
|
|
101
|
+
@adapter_module ||= begin
|
|
102
|
+
adapter_module = Adapter.new
|
|
103
|
+
root_example_group = Class.new(ExampleGroup)
|
|
104
|
+
adapter_module.define_singleton_method(:root_example_group) { root_example_group }
|
|
105
|
+
root_example_group.define_singleton_method(:configuration) { adapter_module.configuration }
|
|
106
|
+
shared_context_module = SharedContext.dup
|
|
107
|
+
shared_context_module.define_method(:configuration) { adapter_module.configuration }
|
|
108
|
+
adapter_module.const_set(:SharedContext, shared_context_module)
|
|
109
|
+
adapter_module
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private def make_test_case(example_group, example)
|
|
114
|
+
TestCase.new(example.label, tags: example.metadata) do |context|
|
|
115
|
+
instance = example_group.new
|
|
116
|
+
context.rspec_carryover_instance_variables(instance) # XXX
|
|
117
|
+
instance.ruptr_initialize_test_instance(context)
|
|
118
|
+
begin
|
|
119
|
+
instance.ruptr_wrap_test_instance { instance.run_example(example) }
|
|
120
|
+
rescue *Ruptr.passthrough_exceptions,
|
|
121
|
+
SkippedExceptionMixin
|
|
122
|
+
raise
|
|
123
|
+
rescue Exception
|
|
124
|
+
raise PendingSkippedException, instance.pending_reason if instance.pending?
|
|
125
|
+
raise
|
|
126
|
+
else
|
|
127
|
+
raise PendingPassedError, instance.pending_reason if instance.pending?
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private def make_test_group(example_group)
|
|
133
|
+
block = if example_group.need_wrap_context?
|
|
134
|
+
lambda do |context, &nest|
|
|
135
|
+
instance = example_group.new
|
|
136
|
+
context.rspec_carryover_instance_variables(instance) # XXX
|
|
137
|
+
instance.ruptr_initialize_test_instance(context)
|
|
138
|
+
instance.ruptr_wrap_test_instance { instance.wrap_context(&nest) }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
root = example_group.equal?(adapter_module.root_example_group)
|
|
142
|
+
TestGroup.new(root ? "[RSpec]" : example_group.label,
|
|
143
|
+
identifier: root ? :rspec : example_group.label,
|
|
144
|
+
tags: example_group.metadata, &block)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def adapted_test_group
|
|
148
|
+
traverse = lambda do |example_group|
|
|
149
|
+
make_test_group(example_group).tap do |tg|
|
|
150
|
+
example_group.each_example do |example|
|
|
151
|
+
tc = make_test_case(example_group, example)
|
|
152
|
+
tg.add_test_case(tc)
|
|
153
|
+
end
|
|
154
|
+
example_group.each_example_group do |child_example_group|
|
|
155
|
+
tg.add_test_subgroup(traverse.call(child_example_group))
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
traverse.call(adapter_module.root_example_group)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def filter_test_group(test_group)
|
|
163
|
+
conf = adapter_module.configuration
|
|
164
|
+
|
|
165
|
+
return test_group if conf.inclusion_filter.empty? && conf.exclusion_filter.empty?
|
|
166
|
+
|
|
167
|
+
matches_inclusion_filters = lambda do |tc|
|
|
168
|
+
conf.inclusion_filter.empty? ||
|
|
169
|
+
conf.inclusion_filter.any? { |f| ExampleGroup.filter_matches?(f, tc.tags) }
|
|
170
|
+
end
|
|
171
|
+
matches_exclusion_filters = lambda do |tc|
|
|
172
|
+
conf.exclusion_filter.empty? ||
|
|
173
|
+
conf.exclusion_filter.none? { |f| ExampleGroup.filter_matches?(f, tc.tags) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
test_group = super
|
|
177
|
+
filtered_test_group = test_group.filter_test_cases_recursive do |tc|
|
|
178
|
+
matches_exclusion_filters === tc && matches_inclusion_filters === tc
|
|
179
|
+
end
|
|
180
|
+
if conf.run_all_when_everything_filtered &&
|
|
181
|
+
filtered_test_group.count_test_cases.zero?
|
|
182
|
+
filtered_test_group = test_group.filter_test_cases_recursive do |tc|
|
|
183
|
+
matches_exclusion_filters === tc
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
filtered_test_group
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
class Ruptr::Context
|
|
191
|
+
attr_accessor :rspec_example_group_instance,
|
|
192
|
+
:rspec_example_group_instance_variables_carried_over
|
|
193
|
+
|
|
194
|
+
def rspec_carryover_instance_variables(instance)
|
|
195
|
+
context = parent
|
|
196
|
+
loop do
|
|
197
|
+
if context.rspec_example_group_instance_variables_carried_over
|
|
198
|
+
if (parent_instance = context.rspec_example_group_instance)
|
|
199
|
+
ExampleGroup.carryover_instance_variables(parent_instance, instance)
|
|
200
|
+
end
|
|
201
|
+
break
|
|
202
|
+
end
|
|
203
|
+
context = context.parent or break
|
|
204
|
+
end
|
|
205
|
+
self.rspec_example_group_instance = instance # XXX
|
|
206
|
+
self.rspec_example_group_instance_variables_carried_over = true;
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
data/lib/ruptr/runner.rb
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'suite'
|
|
4
|
+
require_relative 'result'
|
|
5
|
+
require_relative 'instance'
|
|
6
|
+
require_relative 'exceptions'
|
|
7
|
+
require_relative 'surrogate_exception'
|
|
8
|
+
require_relative 'utils'
|
|
9
|
+
require_relative 'capture_output'
|
|
10
|
+
|
|
11
|
+
module Ruptr
|
|
12
|
+
class Context
|
|
13
|
+
def initialize(runner, test_element, parent)
|
|
14
|
+
@runner = runner
|
|
15
|
+
@test_element = test_element
|
|
16
|
+
@parent = parent
|
|
17
|
+
@assertions_count = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :runner, :test_element, :parent
|
|
21
|
+
attr_accessor :assertions_count
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class TestElement
|
|
25
|
+
private def maybe_capture_output(context, &)
|
|
26
|
+
if context.runner.capture_output
|
|
27
|
+
CaptureOutput.capture_output(&)
|
|
28
|
+
else
|
|
29
|
+
yield
|
|
30
|
+
[nil, nil]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def make_result(context)
|
|
35
|
+
status = exception = user_time = system_time = nil
|
|
36
|
+
captured_stdout, captured_stderr = maybe_capture_output(context) do
|
|
37
|
+
user_time, system_time = Ruptr.measure_processor_time do
|
|
38
|
+
yield
|
|
39
|
+
rescue *Ruptr.passthrough_exceptions
|
|
40
|
+
raise
|
|
41
|
+
rescue SkippedExceptionMixin => exception
|
|
42
|
+
status = :skipped
|
|
43
|
+
rescue Exception => exception
|
|
44
|
+
status = :failed
|
|
45
|
+
else
|
|
46
|
+
status = :passed
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
assertions = context.assertions_count
|
|
50
|
+
TestResult.new(status,
|
|
51
|
+
user_time:, system_time:, assertions:, exception:,
|
|
52
|
+
captured_stdout:, captured_stderr:)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class TestCase
|
|
57
|
+
def run_result(runner, group_context)
|
|
58
|
+
context = Context.new(runner, self, group_context)
|
|
59
|
+
make_result(context) { run_context(context) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class TestGroup
|
|
64
|
+
def must_wrap? = block?
|
|
65
|
+
|
|
66
|
+
def wrap_result(runner, parent_context)
|
|
67
|
+
context = Context.new(runner, self, parent_context)
|
|
68
|
+
make_result(context) do
|
|
69
|
+
if must_wrap?
|
|
70
|
+
wrap_context(context) { yield context }
|
|
71
|
+
else
|
|
72
|
+
yield context
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class TestResult
|
|
79
|
+
def make_marshallable
|
|
80
|
+
return unless @exception
|
|
81
|
+
begin
|
|
82
|
+
Marshal.dump(@exception)
|
|
83
|
+
rescue TypeError
|
|
84
|
+
@exception = SurrogateException.from(@exception)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class Runner
|
|
90
|
+
class << self
|
|
91
|
+
def class_from_env(env = ENV, default_parallel: true)
|
|
92
|
+
if (s = env['RUPTR_RUNNER'])
|
|
93
|
+
find_runner(s.to_sym) or fail "unknown runner: #{s}"
|
|
94
|
+
else
|
|
95
|
+
n = (s = env['RUPTR_JOBS']) ? s.to_i : default_parallel ? 0 : 1
|
|
96
|
+
n = Runner::Parallel.default_parallel_jobs unless n.positive?
|
|
97
|
+
n > 1 ? Runner::Forking : Runner
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def opts_from_env(klass, env = ENV, **opts)
|
|
102
|
+
if klass <= Runner::Parallel
|
|
103
|
+
if !opts.key?(:parallel_jobs) && (s = env['RUPTR_JOBS'])
|
|
104
|
+
n = s.to_i
|
|
105
|
+
n = Runner::Parallel.default_parallel_jobs unless n.positive?
|
|
106
|
+
opts[:parallel_jobs] = n
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
opts
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def from_env(env = ENV, **opts)
|
|
113
|
+
klass = class_from_env(env)
|
|
114
|
+
opts = opts_from_env(klass, env, **opts)
|
|
115
|
+
klass.new(**opts)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
attr_accessor :runner_names
|
|
119
|
+
|
|
120
|
+
def find_runner(name)
|
|
121
|
+
traverse = proc do |c|
|
|
122
|
+
return c if c.runner_names&.include?(name)
|
|
123
|
+
c.subclasses.each(&traverse)
|
|
124
|
+
end
|
|
125
|
+
traverse.call(self)
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
self.runner_names = %i[single s]
|
|
131
|
+
|
|
132
|
+
def initialize(randomize: false, timing_store: nil, golden_store: nil, capture_output: true)
|
|
133
|
+
@randomize = randomize
|
|
134
|
+
@timing_store = timing_store
|
|
135
|
+
@golden_store = golden_store
|
|
136
|
+
@capture_output = capture_output
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
attr_reader :timing_store, :golden_store, :capture_output
|
|
140
|
+
|
|
141
|
+
def expected_processor_time(te) = @timing_store ? @timing_store[te] : Float::INFINITY
|
|
142
|
+
|
|
143
|
+
class BatchYielder
|
|
144
|
+
Batch = Struct.new(:group_context, :test_cases, :fork_overlap)
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def initialize(runner, sink, &block)
|
|
149
|
+
@runner = runner
|
|
150
|
+
@sink = sink
|
|
151
|
+
@process_batch = block
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def wrap_group(tg, gc)
|
|
155
|
+
@sink.submit_group(tg) do
|
|
156
|
+
ok = false
|
|
157
|
+
tr = tg.wrap_result(@runner, gc) do |gc|
|
|
158
|
+
ok = true
|
|
159
|
+
yield gc
|
|
160
|
+
end
|
|
161
|
+
traverse_group_children_blocked(tg, tr.failed? ? :blocked : :skipped) unless ok
|
|
162
|
+
tr
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def traverse_group(tg, gc)
|
|
167
|
+
wrap_group(tg, gc) { |gc| traverse_group_children(tg, gc) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def traverse_group_children_blocked(tg, status = :blocked)
|
|
171
|
+
tg.each_test_case do |tc|
|
|
172
|
+
@sink.submit_case(tc, TestResult.new(status))
|
|
173
|
+
end
|
|
174
|
+
tg.each_test_subgroup do |tg|
|
|
175
|
+
@sink.submit_group(tg) do
|
|
176
|
+
traverse_group_children_blocked(tg, status)
|
|
177
|
+
TestResult.new(status)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def traverse_group_children(tg, gc)
|
|
183
|
+
batched_cases = []
|
|
184
|
+
pending_groups = []
|
|
185
|
+
gather = lambda do |tg|
|
|
186
|
+
tg.each_test_case do |tc|
|
|
187
|
+
batched_cases << tc
|
|
188
|
+
end
|
|
189
|
+
tg.each_test_subgroup do |tg|
|
|
190
|
+
if tg.must_wrap?
|
|
191
|
+
pending_groups << tg
|
|
192
|
+
else
|
|
193
|
+
@sink.submit_group(tg) do
|
|
194
|
+
gather.call(tg)
|
|
195
|
+
TestResult.new(:passed)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
gather.call(tg)
|
|
201
|
+
|
|
202
|
+
if @randomize
|
|
203
|
+
batched_cases.shuffle!
|
|
204
|
+
elsif @runner.timing_store
|
|
205
|
+
batched_cases.sort_by! { |tc| -@runner.expected_processor_time(tc) }
|
|
206
|
+
end
|
|
207
|
+
@process_batch.call(Batch.new(gc, batched_cases, !tg.tags[:ruptr_no_fork_overlap]))
|
|
208
|
+
|
|
209
|
+
if @randomize
|
|
210
|
+
pending_groups.shuffle!
|
|
211
|
+
elsif @runner.timing_store
|
|
212
|
+
pending_groups.sort_by! { |tg| -@runner.expected_processor_time(tg) }
|
|
213
|
+
end
|
|
214
|
+
pending_groups.each do |tg|
|
|
215
|
+
traverse_group(tg, gc)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
public def yield_batches(tg, gc = nil)
|
|
220
|
+
traverse_group(tg, gc)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private def each_batch(ts, sink, &)
|
|
225
|
+
BatchYielder.new(self, sink, &).yield_batches(ts)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private def dispatch_batch(batch, sink)
|
|
229
|
+
batch.test_cases.each do |tc|
|
|
230
|
+
sink.submit_case(tc) { tc.run_result(self, batch.group_context) }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def dispatch(ts, sink)
|
|
235
|
+
each_batch(ts, sink) do |batch|
|
|
236
|
+
dispatch_batch(batch, sink)
|
|
237
|
+
golden_store&.flush_trial
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def run_sink(ts, sink)
|
|
242
|
+
fields = { planned_test_case_count: ts.count_test_cases }
|
|
243
|
+
sink.submit_plan(fields) { dispatch(ts, sink) }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def run_report(ts, report = Report.new)
|
|
247
|
+
run_sink(ts, Report::Builder.new(report))
|
|
248
|
+
report
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
class Parallel < self
|
|
252
|
+
def self.default_parallel_jobs
|
|
253
|
+
require 'etc'
|
|
254
|
+
Etc.nprocessors
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def initialize(parallel_jobs: self.class.default_parallel_jobs, **opts)
|
|
258
|
+
super(**opts)
|
|
259
|
+
@parallel_jobs = parallel_jobs
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
attr_reader :parallel_jobs
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
class Threaded < Parallel
|
|
266
|
+
self.runner_names = %i[t thread threads threaded]
|
|
267
|
+
|
|
268
|
+
def dispatch_batch(batch, sink)
|
|
269
|
+
pending_mtx = Mutex.new
|
|
270
|
+
sink_mtx = Mutex.new
|
|
271
|
+
pending_tcs = batch.test_cases
|
|
272
|
+
[parallel_jobs, pending_tcs.size].min.times.map do
|
|
273
|
+
Thread.new do
|
|
274
|
+
# TODO: reduce locking overhead?
|
|
275
|
+
while (tc = pending_mtx.synchronize { pending_tcs.shift })
|
|
276
|
+
sink_mtx.synchronize { sink.begin_case(tc) }
|
|
277
|
+
tr = tc.run_result(self, batch.group_context)
|
|
278
|
+
sink_mtx.synchronize { sink.finish_case(tc, tr) }
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end.each(&:join)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
class Forking < Parallel
|
|
286
|
+
self.runner_names = %i[f fork forking p process processes]
|
|
287
|
+
|
|
288
|
+
private def child_worker_loop(batch, all_tcs, read_io, write_io)
|
|
289
|
+
loop do
|
|
290
|
+
request = begin
|
|
291
|
+
Marshal.load(read_io)
|
|
292
|
+
rescue EOFError
|
|
293
|
+
break
|
|
294
|
+
end
|
|
295
|
+
response = all_tcs.values_at(*request).map do |tc|
|
|
296
|
+
tc.run_result(self, batch.group_context).tap(&:make_marshallable)
|
|
297
|
+
end
|
|
298
|
+
Marshal.dump(response, write_io)
|
|
299
|
+
end
|
|
300
|
+
ensure
|
|
301
|
+
read_io.close
|
|
302
|
+
write_io.close
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
Worker = Struct.new(:pid, :read_io, :write_io, :pending_tcs, :stale) do
|
|
306
|
+
def close
|
|
307
|
+
read_io.close
|
|
308
|
+
write_io.close
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
Master = Struct.new(:sink, :all_workers, :free_workers)
|
|
313
|
+
|
|
314
|
+
private def spawn_worker(master, batch, all_tcs)
|
|
315
|
+
pid, parent_read_io, parent_write_io = Ruptr.fork_piped_worker do |child_read_io, child_write_io|
|
|
316
|
+
ENV['RUPTR_WORKER_PROCESS_INDEX'] = master.all_workers.size.to_s
|
|
317
|
+
master.all_workers.reverse_each(&:close)
|
|
318
|
+
child_worker_loop(batch, all_tcs, child_read_io, child_write_io)
|
|
319
|
+
golden_store&.flush_trial
|
|
320
|
+
end
|
|
321
|
+
worker = Worker.new(pid, parent_read_io, parent_write_io, [], false)
|
|
322
|
+
master.all_workers << worker
|
|
323
|
+
master.free_workers << worker
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
private def distribute_work(master, all_tcs, remaining_tc_indexes)
|
|
327
|
+
workers = master.free_workers.pop(remaining_tc_indexes.size)
|
|
328
|
+
return if workers.empty?
|
|
329
|
+
worker_tc_indexes = workers.map { [] }
|
|
330
|
+
worker_total_time = workers.map { 0 }
|
|
331
|
+
i = 0
|
|
332
|
+
while (tc_index = remaining_tc_indexes.first)
|
|
333
|
+
tc = all_tcs[tc_index]
|
|
334
|
+
ptime = expected_processor_time(tc)
|
|
335
|
+
break if worker_tc_indexes[i].size >= 8 ||
|
|
336
|
+
!worker_tc_indexes[i].empty? && worker_total_time[i] + ptime >= 0.005
|
|
337
|
+
remaining_tc_indexes.shift
|
|
338
|
+
worker_tc_indexes[i] << tc_index
|
|
339
|
+
worker_total_time[i] += ptime
|
|
340
|
+
i = (i + 1) % workers.size
|
|
341
|
+
end
|
|
342
|
+
workers.zip(worker_tc_indexes) do |worker, tc_indexes|
|
|
343
|
+
worker.pending_tcs = all_tcs.values_at(*tc_indexes)
|
|
344
|
+
Marshal.dump(tc_indexes, worker.write_io)
|
|
345
|
+
worker.pending_tcs.each do |tc|
|
|
346
|
+
master.sink.begin_case(tc)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
private def process_responses(master)
|
|
352
|
+
busy_workers = master.all_workers - master.free_workers
|
|
353
|
+
ready_ios, _, _ = IO.select(busy_workers.map(&:read_io))
|
|
354
|
+
ready_ios.each do |ready_io|
|
|
355
|
+
worker = busy_workers.find { |w| w.read_io == ready_io }
|
|
356
|
+
results = Marshal.load(ready_io)
|
|
357
|
+
worker.pending_tcs.zip(results).each do |tc, tr|
|
|
358
|
+
master.sink.finish_case(tc, tr)
|
|
359
|
+
end
|
|
360
|
+
worker.pending_tcs = nil
|
|
361
|
+
master.free_workers.push(worker)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
private def drop_workers(master, workers)
|
|
366
|
+
workers.each { |w| w.write_io.close }
|
|
367
|
+
workers.each do |w|
|
|
368
|
+
Process.wait(w.pid)
|
|
369
|
+
raise "worker process #{$?}" unless $?.success?
|
|
370
|
+
end
|
|
371
|
+
master.all_workers -= workers
|
|
372
|
+
master.free_workers -= workers
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
private def finish_workers(master)
|
|
376
|
+
process_responses(master) until (master.all_workers - master.free_workers).empty?
|
|
377
|
+
drop_workers(master, master.all_workers)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
private def workers_count_for_test_cases(tcs)
|
|
381
|
+
# for very fast test cases, forking a process isn't worth it
|
|
382
|
+
m = [tcs.size, parallel_jobs].min
|
|
383
|
+
a = 0
|
|
384
|
+
tcs.each do |tc|
|
|
385
|
+
a += expected_processor_time(tc) * 512
|
|
386
|
+
return m if a >= m
|
|
387
|
+
end
|
|
388
|
+
[a.to_i, 1].max
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def dispatch(ts, sink)
|
|
392
|
+
master = Master.new(sink, [], [])
|
|
393
|
+
# NOTE: Each iteration of #yield_batches is run with a global state setup for this
|
|
394
|
+
# particular batch of test cases. Worker processes must not be reused if they were forked
|
|
395
|
+
# while a different state was active.
|
|
396
|
+
total_workers_limit = parallel_jobs
|
|
397
|
+
each_batch(ts, sink) do |batch|
|
|
398
|
+
all_tcs = batch.test_cases
|
|
399
|
+
batch_workers_limit = workers_count_for_test_cases(all_tcs)
|
|
400
|
+
unless batch_workers_limit > 1
|
|
401
|
+
# Fallback to single-threaded serial execution.
|
|
402
|
+
dispatch_batch(batch, sink)
|
|
403
|
+
next
|
|
404
|
+
end
|
|
405
|
+
batch_workers_count = 0
|
|
406
|
+
remaining_tc_indexes = all_tcs.size.times.to_a
|
|
407
|
+
until remaining_tc_indexes.empty?
|
|
408
|
+
while master.all_workers.size < total_workers_limit &&
|
|
409
|
+
batch_workers_count < batch_workers_limit
|
|
410
|
+
spawn_worker(master, batch, all_tcs)
|
|
411
|
+
batch_workers_count += 1
|
|
412
|
+
end
|
|
413
|
+
distribute_work(master, all_tcs, remaining_tc_indexes)
|
|
414
|
+
# See if we can spawn workers for the next batch before waiting on results.
|
|
415
|
+
break if remaining_tc_indexes.empty?
|
|
416
|
+
process_responses(master)
|
|
417
|
+
drop_workers(master, master.free_workers.select(&:stale))
|
|
418
|
+
end
|
|
419
|
+
master.all_workers.each { |w| w.stale = true }
|
|
420
|
+
if batch.fork_overlap
|
|
421
|
+
drop_workers(master, master.free_workers)
|
|
422
|
+
else
|
|
423
|
+
finish_workers(master)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
finish_workers(master)
|
|
427
|
+
ensure
|
|
428
|
+
master.all_workers.each(&:close)
|
|
429
|
+
master.all_workers = master.free_workers = nil
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
data/lib/ruptr/sink.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruptr
|
|
4
|
+
module Sink
|
|
5
|
+
def begin_plan(_fields) = nil
|
|
6
|
+
def finish_plan(_fields) = nil
|
|
7
|
+
|
|
8
|
+
def submit_plan(fields = {})
|
|
9
|
+
begin_plan(fields)
|
|
10
|
+
yield
|
|
11
|
+
ensure
|
|
12
|
+
finish_plan(fields)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private def begin_element(_te) = nil
|
|
16
|
+
private def finish_element(_te, _tr) = nil
|
|
17
|
+
|
|
18
|
+
def begin_case(tc) = begin_element(tc)
|
|
19
|
+
def finish_case(tc, tr) = finish_element(tc, tr)
|
|
20
|
+
|
|
21
|
+
def submit_case(tc, tr = yield)
|
|
22
|
+
begin_case(tc)
|
|
23
|
+
finish_case(tc, tr)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def begin_group(tg) = begin_element(tg)
|
|
27
|
+
def finish_group(tg, tr) = finish_element(tg, tr)
|
|
28
|
+
|
|
29
|
+
def submit_group(tg, tr = (tr_missing = true; nil))
|
|
30
|
+
begin_group(tg)
|
|
31
|
+
finish_group(tg, tr_missing ? yield : tr)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Tee
|
|
35
|
+
include Sink
|
|
36
|
+
|
|
37
|
+
def self.for(targets)
|
|
38
|
+
return targets.first if targets.size == 1
|
|
39
|
+
new(targets)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(targets) = @targets = targets
|
|
43
|
+
|
|
44
|
+
%i[begin_plan finish_plan begin_case finish_case begin_group finish_group].each do |method_name|
|
|
45
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
46
|
+
def #{method_name}(...) = @targets.each { |target| target.#{method_name}(...) }
|
|
47
|
+
RUBY
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Passed
|
|
52
|
+
include Sink
|
|
53
|
+
def begin_plan(_) = @passed = true
|
|
54
|
+
def finish_element(_, tr) = @passed &&= !tr.failed?
|
|
55
|
+
def passed? = @passed
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|