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.
@@ -0,0 +1,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prettyprint"
4
+
5
+ # :stopdoc:
6
+
7
+ module Megatest
8
+ class PrettyPrint
9
+ # This class is largely a copy of the `pp` gem
10
+ # but rewritten to not rely on monkey patches
11
+ # and with some small rendering modifications
12
+ # notably around multiline strings.
13
+ class Printer < ::PrettyPrint
14
+ class << self
15
+ def pp(obj, out = +"", width = 79)
16
+ q = new(out, width)
17
+ q.guard_inspect_key { q.pp obj }
18
+ q.flush
19
+ out
20
+ end
21
+ end
22
+
23
+ # Yields to a block
24
+ # and preserves the previous set of objects being printed.
25
+ def guard_inspect_key
26
+ @recursive_key = {}.compare_by_identity
27
+
28
+ save = @recursive_key
29
+
30
+ begin
31
+ @recursive_key = {}.compare_by_identity
32
+ yield
33
+ ensure
34
+ @recursive_key = save
35
+ end
36
+ end
37
+
38
+ # Check whether the object_id +id+ is in the current buffer of objects
39
+ # to be pretty printed. Used to break cycles in chains of objects to be
40
+ # pretty printed.
41
+ def check_inspect_key(id)
42
+ @recursive_key&.include?(id)
43
+ end
44
+
45
+ # Adds the object_id +id+ to the set of objects being pretty printed, so
46
+ # as to not repeat objects.
47
+ def push_inspect_key(id)
48
+ @recursive_key[id] = true
49
+ end
50
+
51
+ # Removes an object from the set of objects being pretty printed.
52
+ def pop_inspect_key(id)
53
+ @recursive_key.delete id
54
+ end
55
+
56
+ # Adds +obj+ to the pretty printing buffer
57
+ # using Object#pretty_print or Object#pretty_print_cycle.
58
+ #
59
+ # Object#pretty_print_cycle is used when +obj+ is already
60
+ # printed, a.k.a the object reference chain has a cycle.
61
+ def pp(obj)
62
+ # If obj is a Delegator then use the object being delegated to for cycle
63
+ # detection
64
+ obj = obj.__getobj__ if defined?(::Delegator) && ::Delegator === obj
65
+
66
+ if check_inspect_key(obj)
67
+ group { pretty_print_cycle(obj) }
68
+ return
69
+ end
70
+
71
+ begin
72
+ push_inspect_key(obj)
73
+ group { pretty_print(obj) }
74
+ ensure
75
+ pop_inspect_key(obj)
76
+ end
77
+ end
78
+
79
+ # A convenience method which is same as follows:
80
+ #
81
+ # group(1, '#<' + obj.class.name, '>') { ... }
82
+ def object_group(obj, &block)
83
+ group(1, "#<#{obj.class.name}>", &block)
84
+ end
85
+
86
+ using Compat::BindCall unless UnboundMethod.method_defined?(:bind_call)
87
+
88
+ # A convenience method, like object_group, but also reformats the Object's
89
+ # object_id.
90
+ def object_address_group(obj, &block)
91
+ str = Kernel.instance_method(:to_s).bind_call(obj)
92
+ str.chomp!(">")
93
+ group(1, str, ">", &block)
94
+ end
95
+
96
+ # A convenience method which is same as follows:
97
+ #
98
+ # text ','
99
+ # breakable
100
+ def comma_breakable
101
+ text ","
102
+ breakable
103
+ end
104
+
105
+ # Adds a separated list.
106
+ # The list is separated by comma with breakable space, by default.
107
+ #
108
+ # #seplist iterates the +list+ using +iter_method+.
109
+ # It yields each object to the block given for #seplist.
110
+ # The procedure +separator_proc+ is called between each yields.
111
+ #
112
+ # If the iteration is zero times, +separator_proc+ is not called at all.
113
+ #
114
+ # If +separator_proc+ is nil or not given,
115
+ # +lambda { comma_breakable }+ is used.
116
+ # If +iter_method+ is not given, :each is used.
117
+ #
118
+ # For example, following 3 code fragments has similar effect.
119
+ #
120
+ # q.seplist([1,2,3]) {|v| xxx v }
121
+ #
122
+ # q.seplist([1,2,3], lambda { q.comma_breakable }, :each) {|v| xxx v }
123
+ #
124
+ # xxx 1
125
+ # q.comma_breakable
126
+ # xxx 2
127
+ # q.comma_breakable
128
+ # xxx 3
129
+ def seplist(list, sep = nil, iter_method = :each)
130
+ sep ||= -> { comma_breakable }
131
+ first = true
132
+ list.__send__(iter_method) do |*v|
133
+ if first
134
+ first = false
135
+ else
136
+ sep.call
137
+ end
138
+ yield(*v)
139
+ end
140
+ end
141
+
142
+ # A present standard failsafe for pretty printing any given Object
143
+ def pp_object(obj)
144
+ object_address_group(obj) do
145
+ seplist(pretty_print_instance_variables(obj), -> { text "," }) do |v|
146
+ breakable
147
+ v = v.to_s if Symbol === v
148
+ text v
149
+ text "="
150
+ group(1) do
151
+ breakable ""
152
+ pp(obj.instance_eval(v))
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ INSTANCE_VARIABLES = Object.instance_method(:instance_variables)
159
+ def pretty_print_instance_variables(obj)
160
+ INSTANCE_VARIABLES.bind_call(obj).sort
161
+ end
162
+
163
+ # A pretty print for a Hash
164
+ def pp_hash(obj)
165
+ group(1, "{", "}") do
166
+ seplist(obj, nil, :each_pair) do |k, v|
167
+ group do
168
+ pp k
169
+ text "=>"
170
+ group(1) do
171
+ breakable ""
172
+ pp v
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ using Compat::ByteRIndex unless String.method_defined?(:byterindex)
180
+
181
+ CLASS = Kernel.instance_method(:class)
182
+
183
+ def pretty_print(obj)
184
+ case obj
185
+ when String
186
+ if obj.size > 30 && obj.byterindex("\n", -1)
187
+ text obj.inspect.gsub('\n', "\\n\n").sub(/\\n\n"\z/, '\n"')
188
+ else
189
+ text obj.inspect
190
+ end
191
+ when Array
192
+ group(1, "[", "]") do
193
+ seplist(obj) do |v|
194
+ pp v
195
+ end
196
+ end
197
+ when Hash
198
+ pp_hash(obj)
199
+ when Range
200
+ pp obj.begin
201
+ breakable ""
202
+ text(obj.exclude_end? ? "..." : "..")
203
+ breakable ""
204
+ pp obj.end if obj.end
205
+ when MatchData
206
+ nc = []
207
+ obj.regexp.named_captures.each do |name, indexes|
208
+ indexes.each { |i| nc[i] = name }
209
+ end
210
+
211
+ object_group(obj) do
212
+ breakable
213
+ seplist(0...obj.size, -> { breakable }) do |i|
214
+ if i != 0
215
+ if nc[i]
216
+ text nc[i]
217
+ else
218
+ pp i
219
+ end
220
+ text ":"
221
+ pp obj[i]
222
+ end
223
+ pp obj[i]
224
+ end
225
+ end
226
+ when Regexp, Symbol, Numeric, Module, true, false, nil
227
+ text(obj.inspect)
228
+ when Struct
229
+ group(1, format("#<struct %s", CLASS.bind_call(obj)), ">") do
230
+ seplist(Struct.instance_method(:members).bind_call(obj), -> { text "," }) do |member|
231
+ breakable
232
+ text member.to_s
233
+ text "="
234
+ group(1) do
235
+ breakable ""
236
+ pp obj[member]
237
+ end
238
+ end
239
+ end
240
+ else
241
+ if ENV.equal?(obj)
242
+ pp_hash(ENV.sort.to_h)
243
+ elsif special_inspect?(obj)
244
+ text(obj.inspect)
245
+ else
246
+ pp_object(obj)
247
+ end
248
+ end
249
+ end
250
+
251
+ def pretty_print_cycle(obj)
252
+ case obj
253
+ when Array
254
+ text(obj.empty? ? "[]" : "[...]")
255
+ when Hash
256
+ text(obj.empty? ? "{}" : "{...}")
257
+ when Struct
258
+ text format("#<struct %s:...>", CLASS.bind_call(obj))
259
+ when Numeric, Symbol, FalseClass, TrueClass, NilClass, Module
260
+ text obj.inspect
261
+ else
262
+ object_address_group(obj) do
263
+ breakable
264
+ text "..."
265
+ end
266
+ end
267
+ end
268
+
269
+ METHOD = Object.instance_method(:method)
270
+ def special_inspect?(obj)
271
+ METHOD.bind_call(obj, :inspect).owner != Kernel
272
+ rescue NoMethodError
273
+ false
274
+ end
275
+
276
+ OBJECT_INSPECT = Object.instance_method(:inspect)
277
+
278
+ def inspect_object(obj)
279
+ obj.inspect
280
+ rescue NoMethodError # Basic Object etc.
281
+ OBJECT_INSPECT.bind_call(obj)
282
+ end
283
+ end
284
+
285
+ def initialize(config)
286
+ @config = config
287
+ end
288
+
289
+ using Compat::BindCall unless UnboundMethod.method_defined?(:bind_call)
290
+
291
+ def pretty_print(object)
292
+ case object
293
+ when Exception
294
+ [
295
+ "Class: <#{pp(object.class)}>",
296
+ "Message: <#{object.message.inspect}>",
297
+ "---Backtrace---",
298
+ *@config.backtrace.clean(object.backtrace),
299
+ "---------------",
300
+ ].join("\n")
301
+ else
302
+ out = "".dup
303
+ Printer.pp(object, out)
304
+ out.strip
305
+ end
306
+ end
307
+ alias_method :pp, :pretty_print
308
+ end
309
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :stopdoc:
4
+
5
+ module Megatest
6
+ class AbstractQueue
7
+ class << self
8
+ alias_method :build, :new
9
+ private :new
10
+ end
11
+
12
+ attr_reader :test_cases_index, :size
13
+
14
+ def initialize(config)
15
+ @config = config
16
+ @size = nil
17
+ @test_cases_index = nil
18
+ @populated = false
19
+ end
20
+
21
+ def retrying?
22
+ false
23
+ end
24
+
25
+ def sharded?
26
+ false
27
+ end
28
+
29
+ def summary
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def distributed?
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def empty?
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def remaining_size
42
+ raise NotImplementedError
43
+ end
44
+
45
+ def success?
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def populated?
50
+ @populated
51
+ end
52
+
53
+ def record_lost_test(test)
54
+ record_result(TestCaseResult.new(test).lost)
55
+ end
56
+
57
+ def pop_test
58
+ raise NotImplementedError
59
+ end
60
+
61
+ def record_result(result)
62
+ raise NotImplementedError
63
+ end
64
+
65
+ def populate(test_cases)
66
+ @test_cases_index = test_cases.to_h { |t| [t.id, t] }
67
+ @size = test_cases.size
68
+ @populated = true
69
+ end
70
+
71
+ def cleanup
72
+ end
73
+ end
74
+
75
+ module ShardeableQueue
76
+ def sharded?
77
+ @config.workers_count > 1
78
+ end
79
+
80
+ def populate(test_cases)
81
+ if sharded?
82
+ test_cases = test_cases.select.with_index { |_t, index| (index % @config.workers_count) == @config.worker_id }
83
+ end
84
+
85
+ super
86
+ end
87
+ end
88
+
89
+ class Queue < AbstractQueue
90
+ class Summary
91
+ attr_reader :results
92
+
93
+ def initialize(results = [])
94
+ @results = results
95
+ end
96
+
97
+ # When running distributed queues, it's possible
98
+ # that a test is considered lost and end up with both
99
+ # a successful and a failed result.
100
+ # In such case we turn the failed result into a retry
101
+ # after the fact.
102
+ def deduplicate!
103
+ success = {}
104
+ @results.each do |result|
105
+ if result.success?
106
+ success[result.test_id] = true
107
+ end
108
+ end
109
+
110
+ @results.map! do |result|
111
+ if result.bad? && success[result.test_id]
112
+ result.retry
113
+ else
114
+ result
115
+ end
116
+ end
117
+ end
118
+
119
+ def assertions_count
120
+ results.sum(0, &:assertions_count)
121
+ end
122
+
123
+ def runs_count
124
+ results.size
125
+ end
126
+
127
+ def total_time
128
+ results.sum(0.0, &:duration)
129
+ end
130
+
131
+ def retries_count
132
+ results.count(&:retried?)
133
+ end
134
+
135
+ def failures_count
136
+ results.count(&:failure?)
137
+ end
138
+
139
+ def errors_count
140
+ results.count(&:error?)
141
+ end
142
+
143
+ def skips_count
144
+ results.count(&:skipped?)
145
+ end
146
+
147
+ def failures
148
+ results.reject(&:success?)
149
+ end
150
+
151
+ def success?
152
+ !results.empty? && @results.all?(&:ok?)
153
+ end
154
+
155
+ def record_result(result)
156
+ @results << result
157
+ result
158
+ end
159
+ end
160
+
161
+ prepend ShardeableQueue
162
+
163
+ attr_reader :summary
164
+ alias_method :global_summary, :summary
165
+
166
+ def initialize(config)
167
+ super(config)
168
+
169
+ @queue = nil
170
+ @summary = Summary.new
171
+ @success = true
172
+ @retries = Hash.new(0)
173
+ @leases = {}
174
+ end
175
+
176
+ def distributed?
177
+ false
178
+ end
179
+
180
+ def sharded?
181
+ @config.workers_count > 1
182
+ end
183
+
184
+ def monitor
185
+ nil
186
+ end
187
+
188
+ def empty?
189
+ @queue.empty? && @leases.empty?
190
+ end
191
+
192
+ def populate(test_cases)
193
+ super
194
+ @queue = test_cases.reverse
195
+ end
196
+
197
+ def remaining_size
198
+ @queue.size + @leases.size
199
+ end
200
+
201
+ def success?
202
+ @success && @queue.empty?
203
+ end
204
+
205
+ def pop_test
206
+ if test = @queue.pop
207
+ @leases[test.id] = true
208
+ end
209
+ test
210
+ end
211
+
212
+ def record_result(result)
213
+ @leases.delete(result.test_id)
214
+ if result.failed?
215
+ if attempt_to_retry(result)
216
+ result = result.retry
217
+ else
218
+ @success &&= result.ok?
219
+ end
220
+ end
221
+ @summary.record_result(result)
222
+ result
223
+ end
224
+
225
+ private
226
+
227
+ def attempt_to_retry(result)
228
+ return false unless @config.retries?
229
+ return false unless @summary.retries_count < @config.total_max_retries(@size)
230
+ return false unless @retries[result.test_id] < @config.max_retries
231
+
232
+ @retries[result.test_id] += 1
233
+
234
+ index = @config.random.rand(0..@queue.size)
235
+ @queue.insert(index, test_cases_index.fetch(result.test_id))
236
+ true
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :stopdoc:
4
+
5
+ module Megatest
6
+ class QueueMonitor
7
+ class << self
8
+ def run(stdin, stdout)
9
+ config = Marshal.load(stdin)
10
+ stdout.puts("ready")
11
+ stdout.close
12
+ new(config, stdin).run
13
+ end
14
+ end
15
+
16
+ def initialize(config, stdin)
17
+ @config = config
18
+ @in = stdin
19
+ end
20
+
21
+ def run
22
+ queue = @config.build_queue
23
+ queue.heartbeat
24
+
25
+ queue.heartbeat until @in.wait_readable(@config.heartbeat_frequency)
26
+
27
+ 0
28
+ end
29
+ end
30
+ end
31
+
32
+ if __FILE__ == $PROGRAM_NAME
33
+ require "megatest"
34
+ exit(Megatest::QueueMonitor.run($stdin, $stdout))
35
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :stopdoc:
4
+
5
+ module Megatest
6
+ class QueueReporter
7
+ POLL_FREQUENCY = 1
8
+
9
+ def initialize(config, queue, out)
10
+ @config = config
11
+ @queue = queue
12
+ @out = out
13
+ end
14
+
15
+ def wall_time
16
+ nil
17
+ end
18
+
19
+ def wait
20
+ wait_for("Waiting for workers to start") { @queue.populated? }
21
+ wait_for("Waiting for tests to be ran") { @queue.empty? }
22
+ end
23
+
24
+ def run(reporters)
25
+ summary = @queue.global_summary
26
+ summary.deduplicate!
27
+ reporters.each { |r| r.summary(self, @queue, summary) }
28
+
29
+ @queue.populated? && @queue.empty? && summary.success?
30
+ end
31
+
32
+ private
33
+
34
+ def wait_for(label)
35
+ unless yield
36
+ @out.puts label
37
+ sleep POLL_FREQUENCY
38
+ sleep POLL_FREQUENCY until yield
39
+ end
40
+ end
41
+ end
42
+ end