megatest 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +156 -0
- data/TODO.md +17 -0
- data/exe/megatest +7 -0
- data/lib/megatest/assertions.rb +474 -0
- data/lib/megatest/backtrace.rb +70 -0
- data/lib/megatest/cli.rb +249 -0
- data/lib/megatest/compat.rb +74 -0
- data/lib/megatest/config.rb +281 -0
- data/lib/megatest/differ.rb +136 -0
- data/lib/megatest/dsl.rb +164 -0
- data/lib/megatest/executor.rb +104 -0
- data/lib/megatest/multi_process.rb +263 -0
- data/lib/megatest/output.rb +158 -0
- data/lib/megatest/patience_diff.rb +340 -0
- data/lib/megatest/pretty_print.rb +309 -0
- data/lib/megatest/queue.rb +239 -0
- data/lib/megatest/queue_monitor.rb +35 -0
- data/lib/megatest/queue_reporter.rb +42 -0
- data/lib/megatest/redis_queue.rb +459 -0
- data/lib/megatest/reporters.rb +266 -0
- data/lib/megatest/runner.rb +119 -0
- data/lib/megatest/runtime.rb +168 -0
- data/lib/megatest/selector.rb +293 -0
- data/lib/megatest/state.rb +708 -0
- data/lib/megatest/subprocess/main.rb +8 -0
- data/lib/megatest/subprocess.rb +48 -0
- data/lib/megatest/test.rb +115 -0
- data/lib/megatest/test_task.rb +132 -0
- data/lib/megatest/version.rb +5 -0
- data/lib/megatest.rb +123 -0
- metadata +80 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
module Megatest
|
|
6
|
+
class Differ
|
|
7
|
+
HEADER = "--- expected\n+++ actual\n\n"
|
|
8
|
+
|
|
9
|
+
def initialize(config)
|
|
10
|
+
@config = config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
using Compat::ByteRIndex unless String.method_defined?(:byterindex)
|
|
14
|
+
|
|
15
|
+
def call(expected, actual)
|
|
16
|
+
if String === expected && String === actual
|
|
17
|
+
if multiline?(expected) || multiline?(actual)
|
|
18
|
+
multiline_string_diff(expected, actual)
|
|
19
|
+
else
|
|
20
|
+
single_line_string_diff(expected, actual)
|
|
21
|
+
end
|
|
22
|
+
elsif Array === expected && Array === actual
|
|
23
|
+
array_diff(expected, actual)
|
|
24
|
+
elsif Hash === expected && Hash === actual
|
|
25
|
+
hash_diff(expected, actual)
|
|
26
|
+
else
|
|
27
|
+
expected_inspect = pp(expected)
|
|
28
|
+
actual_inspect = pp(actual)
|
|
29
|
+
|
|
30
|
+
if multiline?(expected_inspect) || multiline?(actual_inspect)
|
|
31
|
+
object_diff(expected, expected_inspect, actual_inspect)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def pp(object)
|
|
39
|
+
@config.pretty_print(object)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def object_diff(expected, expected_inspect, actual_inspect)
|
|
43
|
+
differ = PatienceDiff::Differ.new(@config.colors)
|
|
44
|
+
diff = differ.diff_text(expected_inspect, actual_inspect)
|
|
45
|
+
render_diff(expected, diff)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def multiline_string_diff(expected, actual)
|
|
49
|
+
differ = PatienceDiff::Differ.new(@config.colors)
|
|
50
|
+
|
|
51
|
+
if expected.encoding != actual.encoding
|
|
52
|
+
expected = encoding_prefix(expected) << expected
|
|
53
|
+
actual = encoding_prefix(actual) << actual
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if need_escape?(expected) || need_escape?(actual)
|
|
57
|
+
expected = escape_string(expected)
|
|
58
|
+
actual = escape_string(actual)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if expected.end_with?("\n") ^ actual.end_with?("\n")
|
|
62
|
+
expected = "#{expected}\n\\ No newline at end of string" unless expected.end_with?("\n")
|
|
63
|
+
actual = "#{actual}\n\\ No newline at end of string" unless actual.end_with?("\n")
|
|
64
|
+
end
|
|
65
|
+
diff = differ.diff_text(expected, actual)
|
|
66
|
+
render_diff(expected, diff)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def multiline?(string)
|
|
70
|
+
string.byterindex("\n", -1)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def render_diff(expected, diff)
|
|
74
|
+
if diff
|
|
75
|
+
"#{HEADER}#{diff.join}"
|
|
76
|
+
else
|
|
77
|
+
<<~TEXT
|
|
78
|
+
No visible difference in the #{expected.class}#inspect output.
|
|
79
|
+
You should look at the implementation of #== on #{expected.class.name} or its members.
|
|
80
|
+
#{pp(expected)}
|
|
81
|
+
TEXT
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def encoding_prefix(string)
|
|
86
|
+
encoding_name = string.encoding == Encoding::BINARY ? "BINARY" : string.encoding.name
|
|
87
|
+
prefix = +"# encoding: #{encoding_name}\n"
|
|
88
|
+
prefix.force_encoding(string.encoding)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def escape_string(string)
|
|
92
|
+
string.b.split("\n").map { |line| line.inspect.byteslice(1..-2) }.join("\n")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def need_escape?(string)
|
|
96
|
+
(string.encoding == Encoding::BINARY && !string.ascii_only?) || !string.valid_encoding?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def single_line_string_diff(expected, actual)
|
|
100
|
+
"Expected: #{pp(expected)}\n Actual: #{pp(actual)}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def array_diff(expected, actual)
|
|
104
|
+
differ = PatienceDiff::Differ.new(@config.colors)
|
|
105
|
+
diff = differ.diff_sequences(array_sequence(expected), array_sequence(actual))
|
|
106
|
+
render_diff(expected, diff)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def array_sequence(array)
|
|
110
|
+
array = array.map { |e| " ".dup << pp(e).chomp << ",\n" }
|
|
111
|
+
array.unshift("[\n")
|
|
112
|
+
array << "]\n"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def hash_diff(expected, actual)
|
|
116
|
+
differ = PatienceDiff::Differ.new(@config.colors)
|
|
117
|
+
expected_seq, actual_seq = hash_sort(expected, actual)
|
|
118
|
+
diff = differ.diff_sequences(hash_sequence(expected_seq), hash_sequence(actual_seq))
|
|
119
|
+
render_diff(expected, diff)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def hash_sort(expected, actual)
|
|
123
|
+
[expected.sort, actual.sort]
|
|
124
|
+
rescue ArgumentError
|
|
125
|
+
[expected, actual]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def hash_sequence(pairs)
|
|
129
|
+
pairs = pairs.map do |k, v|
|
|
130
|
+
" ".dup << pp(k) << " => " << pp(v).chomp << ",\n"
|
|
131
|
+
end
|
|
132
|
+
pairs.unshift("{\n")
|
|
133
|
+
pairs << "}\n"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/megatest/dsl.rb
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megatest
|
|
4
|
+
# All the methods necessary to define test cases.
|
|
5
|
+
# Can be used directly to define test cases in modules
|
|
6
|
+
# for later inclusion.
|
|
7
|
+
#
|
|
8
|
+
# Example:
|
|
9
|
+
#
|
|
10
|
+
# module SomeSharedTests
|
|
11
|
+
# extend Megatest::DSL
|
|
12
|
+
#
|
|
13
|
+
# setup do
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# test "the truth" do
|
|
17
|
+
# assert_equal 4, 2 + 2
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# class SomeTest < Megatest::Test
|
|
22
|
+
# include SomeSharedTests
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# class SomeOtherTest < Megatest::Test
|
|
26
|
+
# include SomeSharedTests
|
|
27
|
+
# end
|
|
28
|
+
module DSL
|
|
29
|
+
# :stopdoc:
|
|
30
|
+
class << self
|
|
31
|
+
def extended(mod)
|
|
32
|
+
super
|
|
33
|
+
if mod.is_a?(Class)
|
|
34
|
+
unless mod == ::Megatest::Test
|
|
35
|
+
raise ArgumentError, "Megatest::DSL should only be extended in modules"
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
::Megatest.registry.shared_suite(mod)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
using Compat::StartWith unless Symbol.method_defined?(:start_with?)
|
|
44
|
+
|
|
45
|
+
def method_added(name)
|
|
46
|
+
super
|
|
47
|
+
if name.start_with?("test_")
|
|
48
|
+
::Megatest.registry.suite(self).register_test_case(name, instance_method(name), nil)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# :startdoc:
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Define a test case.
|
|
56
|
+
#
|
|
57
|
+
# Example:
|
|
58
|
+
#
|
|
59
|
+
# test "the truth" do
|
|
60
|
+
# assert_equal 4, 2 + 2
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# For ease of transition from other test frameworks, any method
|
|
64
|
+
# that starts by +test_+ is also considered a test:
|
|
65
|
+
#
|
|
66
|
+
# Example:
|
|
67
|
+
#
|
|
68
|
+
# def test_the_truth
|
|
69
|
+
# assert_equal 4, 2 + 2
|
|
70
|
+
# end
|
|
71
|
+
def test(name, tags = nil, &block)
|
|
72
|
+
::Megatest.registry.suite(self).register_test_case(-name, block, tags)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Applies tags to all the test case of this suite
|
|
76
|
+
#
|
|
77
|
+
# Example:
|
|
78
|
+
#
|
|
79
|
+
# class SomeTest < Megatest::Test
|
|
80
|
+
# tag focus: true
|
|
81
|
+
#
|
|
82
|
+
# test "something" do
|
|
83
|
+
# assert_equal true, __test__.tag(:focus)
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# test "something else", focus: false do
|
|
87
|
+
# assert_equal false, __test__.tag(:focus)
|
|
88
|
+
# end
|
|
89
|
+
# end
|
|
90
|
+
def tag(**kwargs)
|
|
91
|
+
::Megatest.registry.suite(self).add_tags(kwargs)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# Creates a context block, for logically grouping test cases.
|
|
96
|
+
# The context string will be prepended to all the test cases
|
|
97
|
+
# defined within the block.
|
|
98
|
+
#
|
|
99
|
+
# Example:
|
|
100
|
+
#
|
|
101
|
+
# context "maths" do
|
|
102
|
+
# test "the truth" do
|
|
103
|
+
# assert_equal 4, 2 + 2
|
|
104
|
+
# end
|
|
105
|
+
#
|
|
106
|
+
# test "oddity" do
|
|
107
|
+
# refute_predicate 4, odd?
|
|
108
|
+
# end
|
|
109
|
+
# end
|
|
110
|
+
#
|
|
111
|
+
# Setup and teardown callbacks are not allowed withing a context blocks,
|
|
112
|
+
# as it too easily lead to "write only" tests. It's only meant to help
|
|
113
|
+
# group test cases together.
|
|
114
|
+
#
|
|
115
|
+
# If you need a common setup procedure, just define a helper method, and explictly call it.
|
|
116
|
+
#
|
|
117
|
+
# Example:
|
|
118
|
+
#
|
|
119
|
+
# context "admin user" do
|
|
120
|
+
# def setup_admin_user
|
|
121
|
+
# # ...
|
|
122
|
+
# end
|
|
123
|
+
#
|
|
124
|
+
# test "#admin?" do
|
|
125
|
+
# user = setup_admin_user
|
|
126
|
+
# assert_predicate user, :admin?
|
|
127
|
+
# end
|
|
128
|
+
#
|
|
129
|
+
# test "#can?(:delete_post)" do
|
|
130
|
+
# user = setup_admin_user
|
|
131
|
+
# assert user.can?(:delete_post)
|
|
132
|
+
# end
|
|
133
|
+
# end
|
|
134
|
+
def context(name, tags = nil, &block)
|
|
135
|
+
::Megatest.registry.suite(self).with_context(name, tags, &block)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Registers a block to be invoked before every test cases.
|
|
139
|
+
def setup(&block)
|
|
140
|
+
::Megatest.registry.suite(self).on_setup(block)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Registers a block to be invoked around every test cases.
|
|
144
|
+
# The block will recieve a Proc as first argument and MUST
|
|
145
|
+
# call it.
|
|
146
|
+
#
|
|
147
|
+
# Example:
|
|
148
|
+
#
|
|
149
|
+
# around do |block|
|
|
150
|
+
# do_something do
|
|
151
|
+
# block.call
|
|
152
|
+
# end
|
|
153
|
+
# end
|
|
154
|
+
def around(&block)
|
|
155
|
+
::Megatest.registry.suite(self).on_around(block)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Registers a block to be invoked after every test cases,
|
|
159
|
+
# regardless of whether it passed or failed.
|
|
160
|
+
def teardown(&block)
|
|
161
|
+
::Megatest.registry.suite(self).on_teardown(block)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
module Megatest
|
|
6
|
+
class Executor
|
|
7
|
+
class ExternalMonitor
|
|
8
|
+
def initialize(config)
|
|
9
|
+
require "rbconfig"
|
|
10
|
+
@config = config
|
|
11
|
+
spawn
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reap
|
|
15
|
+
if @pipe
|
|
16
|
+
@pipe.close
|
|
17
|
+
@pipe = nil
|
|
18
|
+
_, status = Process.waitpid2(@pid)
|
|
19
|
+
@pid = nil
|
|
20
|
+
status
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def spawn
|
|
25
|
+
child_read, @pipe = IO.pipe
|
|
26
|
+
ready_pipe, child_write = IO.pipe
|
|
27
|
+
@pid = Process.spawn(
|
|
28
|
+
RbConfig.ruby,
|
|
29
|
+
File.expand_path("../queue_monitor.rb", __FILE__),
|
|
30
|
+
in: child_read,
|
|
31
|
+
out: child_write,
|
|
32
|
+
)
|
|
33
|
+
child_read.close
|
|
34
|
+
Marshal.dump(@config, @pipe)
|
|
35
|
+
|
|
36
|
+
# Check the process is alive.
|
|
37
|
+
if ready_pipe.wait_readable(10)
|
|
38
|
+
ready_pipe.gets
|
|
39
|
+
ready_pipe.close
|
|
40
|
+
Process.kill(0, @pid)
|
|
41
|
+
else
|
|
42
|
+
Process.kill(0, @pid)
|
|
43
|
+
Process.wait(@pid)
|
|
44
|
+
raise Error, "ExternalMonitor failed to boot"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :wall_time
|
|
50
|
+
|
|
51
|
+
def initialize(config, out)
|
|
52
|
+
@config = config
|
|
53
|
+
@out = Output.new(out, colors: @config.colors)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def run(queue, reporters)
|
|
57
|
+
start_time = Megatest.now
|
|
58
|
+
|
|
59
|
+
@config.run_global_setup_callbacks
|
|
60
|
+
@config.run_job_setup_callbacks(nil)
|
|
61
|
+
|
|
62
|
+
monitor = ExternalMonitor.new(@config) if queue.respond_to?(:heartbeat)
|
|
63
|
+
|
|
64
|
+
reporters.each { |r| r.start(self, queue) }
|
|
65
|
+
|
|
66
|
+
runner = Runner.new(@config)
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
while true
|
|
70
|
+
if test_case = queue.pop_test
|
|
71
|
+
reporters.each { |r| r.before_test_case(queue, test_case) }
|
|
72
|
+
|
|
73
|
+
result = runner.execute(test_case)
|
|
74
|
+
|
|
75
|
+
result = queue.record_result(result)
|
|
76
|
+
reporters.each { |r| r.after_test_case(queue, test_case, result) }
|
|
77
|
+
|
|
78
|
+
@config.circuit_breaker.record_result(result)
|
|
79
|
+
break if @config.circuit_breaker.break?
|
|
80
|
+
elsif queue.empty?
|
|
81
|
+
break
|
|
82
|
+
else
|
|
83
|
+
# There was no tests to pop, but not all tests are completed.
|
|
84
|
+
# So we stick around to pop tests that could be lost.
|
|
85
|
+
sleep(1)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
rescue Interrupt
|
|
89
|
+
# Early exit
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
monitor&.reap
|
|
93
|
+
|
|
94
|
+
@wall_time = Megatest.now - start_time
|
|
95
|
+
reporters.each { |r| r.summary(self, queue, queue.summary) }
|
|
96
|
+
|
|
97
|
+
if @config.circuit_breaker.break?
|
|
98
|
+
@out.error("Exited early because too many failures were encountered")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
queue.cleanup
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
# :stopdoc:
|
|
7
|
+
|
|
8
|
+
module Megatest
|
|
9
|
+
# Fairly experimental multi-process queue implementation.
|
|
10
|
+
# It's absolutely not resilient yet, if something goes a bit wrong
|
|
11
|
+
# in may fail in unexpected ways (e.g. hang or whatever).
|
|
12
|
+
# At this stage it's only here to uncover what in the design
|
|
13
|
+
# need to be refactored to make multi-processing and test
|
|
14
|
+
# distribution work well (See the TODOs).
|
|
15
|
+
module MultiProcess
|
|
16
|
+
class << self
|
|
17
|
+
def socketpair
|
|
18
|
+
UNIXSocket.socketpair(:SOCK_STREAM).map { |s| MessageSocket.new(s) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class MessageSocket
|
|
23
|
+
def initialize(socket)
|
|
24
|
+
@socket = socket
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def <<(message)
|
|
28
|
+
begin
|
|
29
|
+
@socket.write(Marshal.dump(message))
|
|
30
|
+
rescue Errno::EPIPE
|
|
31
|
+
return nil # Other side was closed
|
|
32
|
+
end
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def read
|
|
37
|
+
Marshal.load(@socket)
|
|
38
|
+
rescue EOFError
|
|
39
|
+
nil # Other side was closed
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def closed?
|
|
43
|
+
@socket.closed?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def close
|
|
47
|
+
@socket.close
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_io
|
|
51
|
+
@socket
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class ClientQueue
|
|
56
|
+
def initialize(socket, test_cases_index)
|
|
57
|
+
@socket = socket
|
|
58
|
+
@test_cases_index = test_cases_index
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pop_test
|
|
62
|
+
@socket << [:pop]
|
|
63
|
+
if test_id = @socket.read
|
|
64
|
+
@test_cases_index.fetch(test_id)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def record_result(result)
|
|
69
|
+
@socket << [:record, result]
|
|
70
|
+
@socket.read
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def close
|
|
74
|
+
@socket.close
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def to_io
|
|
78
|
+
@socket.to_io
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class Job
|
|
83
|
+
def initialize(config, index)
|
|
84
|
+
@config = config
|
|
85
|
+
@index = index
|
|
86
|
+
@pid = nil
|
|
87
|
+
@child_socket, @parent_socket = MultiProcess.socketpair
|
|
88
|
+
@assigned_test = nil
|
|
89
|
+
@idle = false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def run(executor, parent_queue)
|
|
93
|
+
@pid = Process.fork do
|
|
94
|
+
@config.job_index = @index
|
|
95
|
+
@parent_socket.close
|
|
96
|
+
executor.after_fork_in_child(self)
|
|
97
|
+
|
|
98
|
+
queue = ClientQueue.new(@child_socket, parent_queue)
|
|
99
|
+
@config.run_job_setup_callbacks(@index)
|
|
100
|
+
|
|
101
|
+
runner = Runner.new(@config)
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
while (test_case = queue.pop_test)
|
|
105
|
+
result = runner.execute(test_case)
|
|
106
|
+
result = queue.record_result(result)
|
|
107
|
+
@config.circuit_breaker.record_result(result)
|
|
108
|
+
break if @config.circuit_breaker.break?
|
|
109
|
+
end
|
|
110
|
+
rescue Interrupt
|
|
111
|
+
end
|
|
112
|
+
queue.close
|
|
113
|
+
|
|
114
|
+
# We don't want to run at_exit hooks the app may have
|
|
115
|
+
# installed.
|
|
116
|
+
Process.exit!(0)
|
|
117
|
+
end
|
|
118
|
+
@child_socket.close
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def to_io
|
|
122
|
+
@parent_socket.to_io
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def term
|
|
126
|
+
Process.kill(:TERM, @pid)
|
|
127
|
+
rescue Errno::ESRCH
|
|
128
|
+
# Already dead
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def close
|
|
132
|
+
@parent_socket.close
|
|
133
|
+
@child_socket.close
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def closed?
|
|
137
|
+
@parent_socket.closed?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def idle?
|
|
141
|
+
@idle
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def process(queue, reporters)
|
|
145
|
+
if @idle
|
|
146
|
+
if @assigned_test = queue.pop_test
|
|
147
|
+
@idle = false
|
|
148
|
+
@parent_socket << @assigned_test&.id
|
|
149
|
+
end
|
|
150
|
+
return
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
message, *args = @parent_socket.read
|
|
154
|
+
case message
|
|
155
|
+
when nil
|
|
156
|
+
# Socket closed, child probably died
|
|
157
|
+
@parent_socket.close
|
|
158
|
+
when :pop
|
|
159
|
+
if @assigned_test = queue.pop_test
|
|
160
|
+
@parent_socket << @assigned_test&.id
|
|
161
|
+
else
|
|
162
|
+
@idle = true
|
|
163
|
+
end
|
|
164
|
+
when :record
|
|
165
|
+
result = queue.record_result(*args)
|
|
166
|
+
@assigned_test = nil
|
|
167
|
+
@parent_socket << result
|
|
168
|
+
reporters.each { |r| r.after_test_case(queue, nil, result) }
|
|
169
|
+
@config.circuit_breaker.record_result(result)
|
|
170
|
+
else
|
|
171
|
+
raise "Unexpected message: #{message.inspect}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def on_exit(queue, reporters)
|
|
176
|
+
if @assigned_test
|
|
177
|
+
result = queue.record_lost_test(@assigned_test)
|
|
178
|
+
@assigned_test = nil
|
|
179
|
+
reporters.each { |r| r.after_test_case(queue, nil, result) }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def reap
|
|
184
|
+
Process.wait(@pid)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
class InlineMonitor
|
|
189
|
+
def initialize(config, queue)
|
|
190
|
+
@config = config
|
|
191
|
+
@queue = queue
|
|
192
|
+
@last_heartbeat = 0
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def tick
|
|
196
|
+
now = Megatest.now
|
|
197
|
+
if now - @last_heartbeat > @config.heartbeat_frequency && @queue.heartbeat
|
|
198
|
+
@last_heartbeat = now
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
class Executor
|
|
204
|
+
attr_reader :wall_time
|
|
205
|
+
|
|
206
|
+
def initialize(config, out)
|
|
207
|
+
@config = config
|
|
208
|
+
@out = Output.new(out, colors: config.colors)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def after_fork_in_child(active_job)
|
|
212
|
+
@jobs.each do |job|
|
|
213
|
+
job.close unless job == active_job
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def run(queue, reporters)
|
|
218
|
+
start_time = Megatest.now
|
|
219
|
+
@config.run_global_setup_callbacks
|
|
220
|
+
@jobs = @config.jobs_count.times.map { |index| Job.new(@config, index) }
|
|
221
|
+
|
|
222
|
+
@config.before_fork_callbacks.each(&:call)
|
|
223
|
+
@jobs.each { |j| j.run(self, queue.test_cases_index) }
|
|
224
|
+
|
|
225
|
+
monitor = InlineMonitor.new(@config, queue) if queue.respond_to?(:heartbeat)
|
|
226
|
+
|
|
227
|
+
begin
|
|
228
|
+
while true
|
|
229
|
+
monitor&.tick
|
|
230
|
+
dead_jobs = @jobs.select(&:closed?).each { |j| j.on_exit(queue, reporters) }
|
|
231
|
+
@jobs -= dead_jobs
|
|
232
|
+
break if @jobs.empty?
|
|
233
|
+
break if queue.empty?
|
|
234
|
+
|
|
235
|
+
@jobs.select(&:idle?).each do |job|
|
|
236
|
+
job.process(queue, reporters)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
reads, = IO.select(@jobs, nil, nil, 1)
|
|
240
|
+
reads&.each do |job|
|
|
241
|
+
job.process(queue, reporters)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
break if @config.circuit_breaker.break?
|
|
245
|
+
end
|
|
246
|
+
rescue Interrupt
|
|
247
|
+
@jobs.each(&:term) # Early exit
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
@jobs.each(&:close)
|
|
251
|
+
@jobs.each(&:reap)
|
|
252
|
+
@wall_time = Megatest.now - start_time
|
|
253
|
+
reporters.each { |r| r.summary(self, queue, queue.summary) }
|
|
254
|
+
|
|
255
|
+
if @config.circuit_breaker.break?
|
|
256
|
+
@out.error("Exited early because too many failures were encountered")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
queue.cleanup
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|