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,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megatest
|
|
4
|
+
class Backtrace
|
|
5
|
+
module InternalFilter
|
|
6
|
+
class << self
|
|
7
|
+
INTERNAL_PATHS = [
|
|
8
|
+
File.expand_path("../assertions.rb:", __FILE__).freeze,
|
|
9
|
+
File.expand_path("../runtime.rb:", __FILE__).freeze,
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
def call(backtrace)
|
|
13
|
+
backtrace.reject do |frame|
|
|
14
|
+
frame.start_with?(*INTERNAL_PATHS)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module RelativePathCleaner
|
|
21
|
+
class << self
|
|
22
|
+
def call(path)
|
|
23
|
+
Megatest.relative_path(path)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_accessor :filters, :formatters
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@filters = [InternalFilter]
|
|
32
|
+
@formatters = [RelativePathCleaner]
|
|
33
|
+
@full = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def full!
|
|
37
|
+
@full = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clean(backtrace)
|
|
41
|
+
return backtrace if @full
|
|
42
|
+
|
|
43
|
+
format(filter(backtrace))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def filter(backtrace)
|
|
47
|
+
return backtrace if @full
|
|
48
|
+
|
|
49
|
+
if backtrace
|
|
50
|
+
filters.each do |filter|
|
|
51
|
+
backtrace = filter.call(backtrace)
|
|
52
|
+
end
|
|
53
|
+
backtrace
|
|
54
|
+
else
|
|
55
|
+
[]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def format(backtrace)
|
|
60
|
+
return backtrace if @full
|
|
61
|
+
|
|
62
|
+
backtrace.map do |frame|
|
|
63
|
+
formatters.each do |formatter|
|
|
64
|
+
frame = formatter.call(frame)
|
|
65
|
+
end
|
|
66
|
+
frame
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/megatest/cli.rb
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
require "optparse"
|
|
6
|
+
|
|
7
|
+
module Megatest
|
|
8
|
+
class CLI
|
|
9
|
+
InvalidArgument = Class.new(ArgumentError)
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def run!
|
|
13
|
+
program_name = $PROGRAM_NAME
|
|
14
|
+
program_name = "megatest" if program_name == `command -v megatest`.strip
|
|
15
|
+
exit(new(program_name, $stdout, $stderr, ARGV, ENV).run)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
undef_method :puts, :print # Should only use @out.puts or @err.puts
|
|
20
|
+
|
|
21
|
+
RUNNERS = {
|
|
22
|
+
"report" => :report,
|
|
23
|
+
"run" => :run,
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def initialize(program_name, out, err, argv, env)
|
|
27
|
+
@out = out
|
|
28
|
+
@err = err
|
|
29
|
+
@argv = argv.dup
|
|
30
|
+
@processes = nil
|
|
31
|
+
@config = Config.new(env)
|
|
32
|
+
@program_name = @config.program_name = program_name
|
|
33
|
+
@runner = nil
|
|
34
|
+
@verbose = false
|
|
35
|
+
@junit = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def run
|
|
39
|
+
configure
|
|
40
|
+
case @runner
|
|
41
|
+
when :report
|
|
42
|
+
report
|
|
43
|
+
when nil, :run
|
|
44
|
+
run_tests
|
|
45
|
+
else
|
|
46
|
+
raise InvalidArgument, "Parsing failure"
|
|
47
|
+
end
|
|
48
|
+
rescue InvalidArgument, OptionParser::ParseError => error
|
|
49
|
+
if error.is_a?(InvalidArgument)
|
|
50
|
+
@err.puts "invalid arguments: #{error.message}"
|
|
51
|
+
else
|
|
52
|
+
@err.puts error.message
|
|
53
|
+
end
|
|
54
|
+
@err.puts
|
|
55
|
+
@err.puts @parser
|
|
56
|
+
1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def configure
|
|
60
|
+
if @runner = RUNNERS[@argv.first]
|
|
61
|
+
@argv.shift
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Megatest.config = @config
|
|
65
|
+
@parser = build_parser(@runner)
|
|
66
|
+
@parser.parse!(@argv)
|
|
67
|
+
@argv.shift if @argv.first == "--"
|
|
68
|
+
@config
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def run_tests
|
|
72
|
+
queue = @config.build_queue
|
|
73
|
+
|
|
74
|
+
if queue.distributed?
|
|
75
|
+
raise InvalidArgument, "Distributed queues require a build-id" unless @config.build_id
|
|
76
|
+
raise InvalidArgument, "Distributed queues require a worker-id" unless @config.worker_id
|
|
77
|
+
elsif queue.sharded?
|
|
78
|
+
unless @config.valid_worker_index?
|
|
79
|
+
raise InvalidArgument, "Splitting the queue requires a worker-id lower than workers-count, got: #{@config.worker_id.inspect}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@config.selectors = Selector.parse(@argv)
|
|
84
|
+
Megatest.load_config(@config)
|
|
85
|
+
Megatest.init(@config)
|
|
86
|
+
test_cases = Megatest.load_tests(@config)
|
|
87
|
+
|
|
88
|
+
if test_cases.empty?
|
|
89
|
+
@err.puts "No tests to run"
|
|
90
|
+
return 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
queue.populate(test_cases)
|
|
94
|
+
executor.run(queue, default_reporters)
|
|
95
|
+
queue.success? ? 0 : 1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def report
|
|
99
|
+
queue = @config.build_queue
|
|
100
|
+
|
|
101
|
+
raise InvalidArgument, "Only distributed queues can be summarized" unless queue.distributed?
|
|
102
|
+
raise InvalidArgument, "Distributed queues require a build-id" unless @config.build_id
|
|
103
|
+
raise InvalidArgument, @argv.join(" ") unless @argv.empty?
|
|
104
|
+
|
|
105
|
+
Megatest.load_config(@argv)
|
|
106
|
+
|
|
107
|
+
QueueReporter.new(@config, queue, @out).run(default_reporters) ? 0 : 1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def default_reporters
|
|
113
|
+
reporters = if @verbose || @config.ci
|
|
114
|
+
[
|
|
115
|
+
Reporters::VerboseReporter.new(@config, @out),
|
|
116
|
+
]
|
|
117
|
+
else
|
|
118
|
+
[
|
|
119
|
+
Reporters::SimpleReporter.new(@config, @out),
|
|
120
|
+
]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if @config.ci
|
|
124
|
+
reporters << Reporters::OrderReporter.new(@config, open_file("log/test_order.log"))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if @junit != false
|
|
128
|
+
junit_file = open_file(@junit || "log/junit.xml")
|
|
129
|
+
reporters << Reporters::JUnitReporter.new(@config, Megatest::Output.new(junit_file, colors: true))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
reporters
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def open_file(path)
|
|
136
|
+
File.open(path, "w+")
|
|
137
|
+
rescue Errno::ENOENT
|
|
138
|
+
mkdir_p(File.dirname(path))
|
|
139
|
+
retry
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def mkdir_p(directory)
|
|
143
|
+
raise InvalidArgument if directory.empty?
|
|
144
|
+
|
|
145
|
+
Dir.mkdir(directory)
|
|
146
|
+
rescue Errno::ENOENT
|
|
147
|
+
mkdir_p(File.dirname(directory))
|
|
148
|
+
retry
|
|
149
|
+
rescue InvalidArgument
|
|
150
|
+
raise InvalidArgument, "Couldn't create directory: #{directory}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def executor
|
|
154
|
+
if @config.jobs_count > 1
|
|
155
|
+
require "megatest/multi_process"
|
|
156
|
+
MultiProcess::Executor.new(@config, @out)
|
|
157
|
+
else
|
|
158
|
+
Executor.new(@config, @out)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def build_parser(runner)
|
|
163
|
+
runner = :run if runner.nil?
|
|
164
|
+
OptionParser.new do |opts|
|
|
165
|
+
case runner
|
|
166
|
+
when :report
|
|
167
|
+
opts.banner = "Usage: #{@program_name} report [options]"
|
|
168
|
+
when :run
|
|
169
|
+
opts.banner = "Usage: #{@program_name} run [options] [files or directories]"
|
|
170
|
+
else
|
|
171
|
+
opts.banner = "Usage: #{@program_name} command [options] [files or directories]"
|
|
172
|
+
opts.separator ""
|
|
173
|
+
opts.separator "Commands:"
|
|
174
|
+
opts.separator ""
|
|
175
|
+
|
|
176
|
+
opts.separator "\trun\t\tExecute the given tests."
|
|
177
|
+
opts.separator "\t\t\t $ #{@program_name} test/integration/"
|
|
178
|
+
opts.separator "\t\t\t $ #{@program_name} test/my_test.rb:42 test/another_test.rb:36"
|
|
179
|
+
opts.separator ""
|
|
180
|
+
|
|
181
|
+
opts.separator "\treport\t\tWait for the queue to be entirely processed and report the status"
|
|
182
|
+
opts.separator "\t\t\t $ #{@program_name} report --queue redis://ci-queue.example.com --build-id $CI_BUILD_ID"
|
|
183
|
+
opts.separator ""
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
opts.separator ""
|
|
187
|
+
opts.separator "Options:"
|
|
188
|
+
opts.separator ""
|
|
189
|
+
|
|
190
|
+
opts.on("-b", "--backtrace", "Print full backtraces") do
|
|
191
|
+
@config.backtrace.full!
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
opts.on("-v", "--verbose", "Use the verbose reporter") do
|
|
195
|
+
@verbose = true
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
opts.on("--junit [PATH]", String, "Generate a junit.xml file") do |path|
|
|
199
|
+
@junit = path
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
if runner == :run
|
|
203
|
+
opts.on("--seed SEED", Integer, "The seed used to define run order") do |seed|
|
|
204
|
+
@config.seed = seed
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
opts.on("-j", "--jobs JOBS", Integer, "Number of processes to use") do |jobs|
|
|
208
|
+
@config.jobs_count = jobs
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
help = "Number of consecutive failures before exiting. Default to 1"
|
|
212
|
+
opts.on("-f", "--fail-fast [COUNT]", Integer, help) do |max|
|
|
213
|
+
@config.max_consecutive_failures = (max || 1)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
opts.on("--max-retries COUNT", Integer, "How many times a given test may be retried") do |max_retries|
|
|
217
|
+
@config.max_retries = max_retries
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
opts.on("--retry-tolerance RATE", Float, "The proportion of tests that may be retried. e.g. 0.05 for 5% of retried tests") do |retry_tolerance|
|
|
221
|
+
@config.retry_tolerance = retry_tolerance
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
opts.separator ""
|
|
226
|
+
opts.separator "Test distribution and sharding:"
|
|
227
|
+
opts.separator ""
|
|
228
|
+
|
|
229
|
+
opts.on("--queue URL", String, "URL of queue server to use for test distribution. Default to $MEGATEST_QUEUE_URL") do |queue_url|
|
|
230
|
+
@config.queue_url = queue_url
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
opts.on("--build-id ID", String, "Unique identifier for the CI build") do |build_id|
|
|
234
|
+
@config.build_id = build_id
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
if runner == :run
|
|
238
|
+
opts.on("--worker-id ID", String, "Unique identifier for the CI job") do |worker_id|
|
|
239
|
+
@config.worker_id = worker_id
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
opts.on("--workers-count COUNT", Integer, "Number of CI jobs") do |workers_count|
|
|
243
|
+
@config.workers_count = workers_count
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
module Megatest
|
|
6
|
+
module Compat
|
|
7
|
+
unless Enumerable.method_defined?(:filter_map) # RUBY_VERSION >= "2.7"
|
|
8
|
+
module FilterMap
|
|
9
|
+
refine Enumerable do
|
|
10
|
+
def filter_map(&block)
|
|
11
|
+
result = map(&block)
|
|
12
|
+
result.compact!
|
|
13
|
+
result
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
unless Symbol.method_defined?(:start_with?) # RUBY_VERSION >= "2.7"
|
|
20
|
+
module StartWith
|
|
21
|
+
refine Symbol do
|
|
22
|
+
def start_with?(*args)
|
|
23
|
+
to_s.start_with?(*args)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
unless UnboundMethod.method_defined?(:bind_call) # RUBY_VERSION >= "2.7"
|
|
30
|
+
module BindCall
|
|
31
|
+
refine UnboundMethod do
|
|
32
|
+
def bind_call(receiver, *args, &block)
|
|
33
|
+
bind(receiver).call(*args, &block)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
unless Enumerable.method_defined?(:tally) # RUBY_VERSION >= "2.7"
|
|
40
|
+
module Tally
|
|
41
|
+
refine Enumerable do
|
|
42
|
+
def tally(hash = {})
|
|
43
|
+
each do |element|
|
|
44
|
+
hash[element] = (hash[element] || 0) + 1
|
|
45
|
+
end
|
|
46
|
+
hash
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
unless Symbol.method_defined?(:name) # RUBY_VERSION >= "3.0"
|
|
53
|
+
module Name
|
|
54
|
+
refine Symbol do
|
|
55
|
+
alias_method :name, :to_s
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
unless String.method_defined?(:byterindex) # RUBY_VERSION >= "3.2"
|
|
61
|
+
module ByteRIndex
|
|
62
|
+
refine String do
|
|
63
|
+
def byterindex(matcher, offset = -1)
|
|
64
|
+
if encoding == Encoding::BINARY
|
|
65
|
+
rindex(matcher, offset)
|
|
66
|
+
else
|
|
67
|
+
b.rindex(matcher, offset)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
module Megatest
|
|
6
|
+
class CircuitBreaker
|
|
7
|
+
def initialize(max)
|
|
8
|
+
@max = max
|
|
9
|
+
@consecutive_failures = 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def record_result(result)
|
|
13
|
+
if result.bad?
|
|
14
|
+
@consecutive_failures += 1
|
|
15
|
+
elsif result.success?
|
|
16
|
+
@consecutive_failures = 0
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def break?
|
|
21
|
+
@consecutive_failures >= @max
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class CIService
|
|
26
|
+
@implementations = []
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
def inherited(base)
|
|
30
|
+
super
|
|
31
|
+
@implementations << base
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def configure(config, env)
|
|
35
|
+
@implementations.each do |service|
|
|
36
|
+
service.new(env).configure(config)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :env
|
|
42
|
+
|
|
43
|
+
def initialize(env)
|
|
44
|
+
@env = env
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def configure(_config)
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class CircleCI < self
|
|
52
|
+
def configure(config)
|
|
53
|
+
if env["CIRCLE_BUILD_URL"]
|
|
54
|
+
config.ci = true
|
|
55
|
+
config.build_id = env["CIRCLE_BUILD_URL"]
|
|
56
|
+
config.worker_id = env["CIRCLE_NODE_INDEX"]
|
|
57
|
+
config.workers_count = Integer(env["CIRCLE_NODE_TOTAL"])
|
|
58
|
+
config.seed = env["CIRCLE_SHA1"]&.first(4)&.to_i(16)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class Buildkite < self
|
|
64
|
+
def configure(config)
|
|
65
|
+
if env["BUILDKITE_BUILD_ID"]
|
|
66
|
+
config.ci = true
|
|
67
|
+
config.build_id = env["BUILDKITE_BUILD_ID"]
|
|
68
|
+
config.worker_id = env["BUILDKITE_PARALLEL_JOB"]
|
|
69
|
+
config.workers_count = env["BUILDKITE_PARALLEL_JOB_COUNT"]
|
|
70
|
+
config.seed = env["BUILDKITE_COMMIT"]&.first(4)&.to_i(16)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Travis < self
|
|
76
|
+
def configure(config)
|
|
77
|
+
if env["TRAVIS_BUILD_ID"]
|
|
78
|
+
config.ci = true
|
|
79
|
+
config.build_id = env["TRAVIS_BUILD_ID"]
|
|
80
|
+
# Travis doesn't have builtin parallelization
|
|
81
|
+
# but CI_NODE_INDEX is what is used in their documentation
|
|
82
|
+
# https://docs.travis-ci.com/user/speeding-up-the-build#parallelizing-rspec-cucumber-and-minitest-on-multiple-vms
|
|
83
|
+
config.worker_id = env["CI_NODE_INDEX"]
|
|
84
|
+
config.workers_count = env["CI_NODE_TOTAL"]
|
|
85
|
+
config.seed = env["TRAVIS_COMMIT"]&.first(4)&.to_i(16)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Heroku < self
|
|
91
|
+
def configure(config)
|
|
92
|
+
if env["HEROKU_TEST_RUN_ID"]
|
|
93
|
+
config.ci = true
|
|
94
|
+
config.build_id = env["HEROKU_TEST_RUN_ID"]
|
|
95
|
+
config.worker_id = env["CI_NODE_INDEX"]
|
|
96
|
+
config.workers_count = env["CI_NODE_TOTAL"]
|
|
97
|
+
config.seed = env["HEROKU_TEST_RUN_COMMIT_VERSION"]&.first(4)&.to_i(16)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class Megatest < self
|
|
103
|
+
def configure(config)
|
|
104
|
+
if env["CI"]
|
|
105
|
+
config.ci = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if url = env["MEGATEST_QUEUE_URL"]
|
|
109
|
+
config.queue_url = url
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if id = env["MEGATEST_BUILD_ID"]
|
|
113
|
+
config.build_id = id
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if id = env["MEGATEST_WORKER_ID"]
|
|
117
|
+
config.worker_id = id
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if seed = env["SEED"]
|
|
121
|
+
config.seed = seed
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# :startdoc:
|
|
128
|
+
|
|
129
|
+
class << self
|
|
130
|
+
attr_writer :config
|
|
131
|
+
|
|
132
|
+
def config
|
|
133
|
+
yield @config if block_given?
|
|
134
|
+
@config
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class Config
|
|
139
|
+
attr_accessor :queue_url, :retry_tolerance, :max_retries, :jobs_count, :job_index, :load_paths, :deprecations,
|
|
140
|
+
:build_id, :heartbeat_frequency, :minitest_compatibility, :ci, :selectors
|
|
141
|
+
attr_reader :before_fork_callbacks, :global_setup_callbacks, :worker_setup_callbacks, :backtrace, :circuit_breaker, :seed,
|
|
142
|
+
:worker_id, :workers_count
|
|
143
|
+
attr_writer :differ, :pretty_printer, :program_name, :colors
|
|
144
|
+
|
|
145
|
+
def initialize(env)
|
|
146
|
+
@load_paths = ["test"] # For easier transition from other frameworks
|
|
147
|
+
@retry_tolerance = 0.0
|
|
148
|
+
@max_retries = 0
|
|
149
|
+
@deprecations = true
|
|
150
|
+
@full_backtrace = false
|
|
151
|
+
@queue_url = nil
|
|
152
|
+
@ci = false
|
|
153
|
+
@build_id = nil
|
|
154
|
+
@worker_id = nil
|
|
155
|
+
@workers_count = 1
|
|
156
|
+
@jobs_count = 1
|
|
157
|
+
@colors = nil # auto
|
|
158
|
+
@before_fork_callbacks = []
|
|
159
|
+
@global_setup_callbacks = []
|
|
160
|
+
@job_setup_callbacks = []
|
|
161
|
+
@heartbeat_frequency = 5
|
|
162
|
+
@backtrace = Backtrace.new
|
|
163
|
+
@program_name = nil
|
|
164
|
+
@circuit_breaker = CircuitBreaker.new(Float::INFINITY)
|
|
165
|
+
@seed = Random.rand(0xFFFF)
|
|
166
|
+
@differ = Differ.new(self)
|
|
167
|
+
@pretty_printer = PrettyPrint.new(self)
|
|
168
|
+
@minitest_compatibility = false
|
|
169
|
+
@selectors = nil
|
|
170
|
+
CIService.configure(self, env)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def program_name
|
|
174
|
+
@program_name || "megatest"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def worker_id=(id)
|
|
178
|
+
@worker_id = if id.is_a?(String) && /\A\d+\z/.match?(id)
|
|
179
|
+
Integer(id)
|
|
180
|
+
else
|
|
181
|
+
id
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def workers_count=(count)
|
|
186
|
+
@workers_count = count ? Integer(count) : 1
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def valid_worker_index?
|
|
190
|
+
worker_id.is_a?(Integer) && worker_id.positive? && worker_id < workers_count
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def colors(io = nil)
|
|
194
|
+
case @colors
|
|
195
|
+
when true
|
|
196
|
+
Output::ANSIColors
|
|
197
|
+
when false
|
|
198
|
+
Output::NoColors
|
|
199
|
+
else
|
|
200
|
+
if io && !io.tty?
|
|
201
|
+
Output::NoColors
|
|
202
|
+
else
|
|
203
|
+
Output::ANSIColors
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def max_consecutive_failures=(max)
|
|
209
|
+
@circuit_breaker = CircuitBreaker.new(max)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def diff(expected, actual)
|
|
213
|
+
@differ&.call(expected, actual)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def pretty_print(object)
|
|
217
|
+
@pretty_printer.pretty_print(object)
|
|
218
|
+
end
|
|
219
|
+
alias_method :pp, :pretty_print
|
|
220
|
+
|
|
221
|
+
# We always return a new generator with the same seed as to
|
|
222
|
+
# best reproduce remote builds locally if the same seed is given.
|
|
223
|
+
def random
|
|
224
|
+
Random.new(@seed)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def seed=(seed)
|
|
228
|
+
@seed = Integer(seed)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def build_queue
|
|
232
|
+
case @queue_url
|
|
233
|
+
when nil
|
|
234
|
+
Queue.build(self)
|
|
235
|
+
when /\Arediss?:/
|
|
236
|
+
require "megatest/redis_queue"
|
|
237
|
+
RedisQueue.build(self)
|
|
238
|
+
else
|
|
239
|
+
raise ArgumentError, "Unsupported queue type: #{@queue_url.inspect}"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def run_before_fork_callback
|
|
244
|
+
@before_fork_callback.each { |c| c.call(self) }
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def before_fork(&block)
|
|
248
|
+
@before_fork_callbacks << block
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def run_global_setup_callbacks
|
|
252
|
+
@global_setup_callbacks.each { |c| c.call(self) }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def global_setup(&block)
|
|
256
|
+
@global_setup_callbacks << block
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def run_job_setup_callbacks(job_index)
|
|
260
|
+
@job_setup_callbacks.each { |c| c.call(self, job_index) }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def job_setup(&block)
|
|
264
|
+
@job_setup_callbacks << block
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def retries?
|
|
268
|
+
@max_retries.positive?
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def total_max_retries(size)
|
|
272
|
+
if @retry_tolerance.positive?
|
|
273
|
+
(size * @retry_tolerance).ceil
|
|
274
|
+
else
|
|
275
|
+
@max_retries * size
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
@config = Config.new({})
|
|
281
|
+
end
|