test-queue-patched 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.travis.yml +18 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile-cucumber1-3 +4 -0
  6. data/Gemfile-cucumber1-3.lock +33 -0
  7. data/Gemfile-cucumber2-4 +4 -0
  8. data/Gemfile-cucumber2-4.lock +37 -0
  9. data/Gemfile-minitest4 +3 -0
  10. data/Gemfile-minitest4.lock +19 -0
  11. data/Gemfile-minitest5 +3 -0
  12. data/Gemfile-minitest5.lock +19 -0
  13. data/Gemfile-rspec2-1 +3 -0
  14. data/Gemfile-rspec2-1.lock +27 -0
  15. data/Gemfile-rspec3-0 +3 -0
  16. data/Gemfile-rspec3-0.lock +31 -0
  17. data/Gemfile-rspec3-1 +3 -0
  18. data/Gemfile-rspec3-1.lock +31 -0
  19. data/Gemfile-rspec3-2 +3 -0
  20. data/Gemfile-rspec3-2.lock +32 -0
  21. data/Gemfile-testunit +3 -0
  22. data/Gemfile-testunit.lock +21 -0
  23. data/Gemfile.lock +41 -0
  24. data/README.md +126 -0
  25. data/bin/cucumber-queue +4 -0
  26. data/bin/minitest-queue +4 -0
  27. data/bin/rspec-queue +4 -0
  28. data/bin/testunit-queue +4 -0
  29. data/lib/test-queue.rb +1 -0
  30. data/lib/test_queue/iterator.rb +107 -0
  31. data/lib/test_queue/runner/cucumber.rb +115 -0
  32. data/lib/test_queue/runner/minitest.rb +21 -0
  33. data/lib/test_queue/runner/minitest4.rb +88 -0
  34. data/lib/test_queue/runner/minitest5.rb +87 -0
  35. data/lib/test_queue/runner/puppet_lint.rb +31 -0
  36. data/lib/test_queue/runner/rspec.rb +79 -0
  37. data/lib/test_queue/runner/rspec2.rb +44 -0
  38. data/lib/test_queue/runner/rspec3.rb +54 -0
  39. data/lib/test_queue/runner/sample.rb +74 -0
  40. data/lib/test_queue/runner/testunit.rb +74 -0
  41. data/lib/test_queue/runner.rb +632 -0
  42. data/lib/test_queue/stats.rb +95 -0
  43. data/lib/test_queue/test_framework.rb +29 -0
  44. data/lib/test_queue.rb +8 -0
  45. data/script/bootstrap +12 -0
  46. data/script/cibuild +19 -0
  47. data/script/spec +7 -0
  48. data/spec/stats_spec.rb +76 -0
  49. data/test/cucumber.bats +57 -0
  50. data/test/minitest4.bats +34 -0
  51. data/test/minitest5.bats +194 -0
  52. data/test/rspec.bats +46 -0
  53. data/test/samples/features/bad.feature +5 -0
  54. data/test/samples/features/sample.feature +25 -0
  55. data/test/samples/features/sample2.feature +29 -0
  56. data/test/samples/features/step_definitions/common.rb +19 -0
  57. data/test/samples/sample_minispec.rb +37 -0
  58. data/test/samples/sample_minitest4.rb +25 -0
  59. data/test/samples/sample_minitest5.rb +33 -0
  60. data/test/samples/sample_rspec_helper.rb +1 -0
  61. data/test/samples/sample_shared_examples_for_spec.rb +5 -0
  62. data/test/samples/sample_spec.rb +25 -0
  63. data/test/samples/sample_split_spec.rb +17 -0
  64. data/test/samples/sample_testunit.rb +25 -0
  65. data/test/samples/sample_use_shared_example1_spec.rb +8 -0
  66. data/test/samples/sample_use_shared_example2_spec.rb +8 -0
  67. data/test/sleepy_runner.rb +14 -0
  68. data/test/testlib.bash +89 -0
  69. data/test/testunit.bats +20 -0
  70. data/test-queue-patched.gemspec +21 -0
  71. metadata +117 -0
@@ -0,0 +1,632 @@
1
+ require 'set'
2
+ require 'socket'
3
+ require 'fileutils'
4
+ require 'securerandom'
5
+ require 'test_queue/stats'
6
+ require 'test_queue/test_framework'
7
+
8
+ module TestQueue
9
+ class Worker
10
+ attr_accessor :pid, :status, :output, :num, :host
11
+ attr_accessor :start_time, :end_time
12
+ attr_accessor :summary, :failure_output
13
+
14
+ # Array of TestQueue::Stats::Suite recording all the suites this worker ran.
15
+ attr_reader :suites
16
+
17
+ def initialize(pid, num)
18
+ @pid = pid
19
+ @num = num
20
+ @start_time = Time.now
21
+ @output = ''
22
+ @suites = []
23
+ end
24
+
25
+ def lines
26
+ @output.split("\n")
27
+ end
28
+ end
29
+
30
+ class Runner
31
+ attr_accessor :concurrency, :exit_when_done
32
+ attr_reader :stats
33
+
34
+ TOKEN_REGEX = /^TOKEN=(\w+)/
35
+
36
+ def initialize(test_framework, concurrency=nil, socket=nil, relay=nil)
37
+ @test_framework = test_framework
38
+ @stats = Stats.new(stats_file)
39
+
40
+ if ENV['TEST_QUEUE_EARLY_FAILURE_LIMIT']
41
+ begin
42
+ @early_failure_limit = Integer(ENV['TEST_QUEUE_EARLY_FAILURE_LIMIT'])
43
+ rescue ArgumentError
44
+ raise ArgumentError, 'TEST_QUEUE_EARLY_FAILURE_LIMIT could not be parsed as an integer'
45
+ end
46
+ end
47
+
48
+ @procline = $0
49
+
50
+ @whitelist = if forced = ENV['TEST_QUEUE_FORCE']
51
+ forced.split(/\s*,\s*/)
52
+ else
53
+ []
54
+ end
55
+ @whitelist.freeze
56
+
57
+ all_files = @test_framework.all_suite_files.to_set
58
+ @queue = @stats.all_suites
59
+ .select { |suite| all_files.include?(suite.path) }
60
+ .sort_by { |suite| -suite.duration }
61
+ .map { |suite| [suite.name, suite.path] }
62
+
63
+ if @whitelist.any?
64
+ @queue.select! { |suite_name, path| @whitelist.include?(suite_name) }
65
+ @queue.sort_by! { |suite_name, path| @whitelist.index(suite_name) }
66
+ end
67
+
68
+ @awaited_suites = Set.new(@whitelist)
69
+ @original_queue = Set.new(@queue).freeze
70
+
71
+ @workers = {}
72
+ @completed = []
73
+
74
+ @concurrency =
75
+ concurrency ||
76
+ (ENV['TEST_QUEUE_WORKERS'] && ENV['TEST_QUEUE_WORKERS'].to_i) ||
77
+ if File.exist?('/proc/cpuinfo')
78
+ File.read('/proc/cpuinfo').split("\n").grep(/processor/).size
79
+ elsif RUBY_PLATFORM =~ /darwin/
80
+ `/usr/sbin/sysctl -n hw.activecpu`.to_i
81
+ else
82
+ 2
83
+ end
84
+ unless @concurrency > 0
85
+ raise ArgumentError, "Worker count (#{@concurrency}) must be greater than 0"
86
+ end
87
+
88
+ @relay_connection_timeout =
89
+ (ENV['TEST_QUEUE_RELAY_TIMEOUT'] && ENV['TEST_QUEUE_RELAY_TIMEOUT'].to_i) ||
90
+ 30
91
+
92
+ @run_token = ENV['TEST_QUEUE_RELAY_TOKEN'] || SecureRandom.hex(8)
93
+
94
+ @socket =
95
+ socket ||
96
+ ENV['TEST_QUEUE_SOCKET'] ||
97
+ "/tmp/test_queue_#{$$}_#{object_id}.sock"
98
+
99
+ @relay =
100
+ relay ||
101
+ ENV['TEST_QUEUE_RELAY']
102
+
103
+ @remote_master_message = ENV["TEST_QUEUE_REMOTE_MASTER_MESSAGE"] if ENV.has_key?("TEST_QUEUE_REMOTE_MASTER_MESSAGE")
104
+
105
+ if @relay == @socket
106
+ STDERR.puts "*** Detected TEST_QUEUE_RELAY == TEST_QUEUE_SOCKET. Disabling relay mode."
107
+ @relay = nil
108
+ elsif @relay
109
+ @queue = []
110
+ end
111
+
112
+ @discovered_suites = Set.new
113
+ @assignments = {}
114
+
115
+ @exit_when_done = true
116
+
117
+ @aborting = false
118
+ end
119
+
120
+ # Run the tests.
121
+ #
122
+ # If exit_when_done is true, exit! will be called before this method
123
+ # completes. If exit_when_done is false, this method will return an Integer
124
+ # number of failures.
125
+ def execute
126
+ $stdout.sync = $stderr.sync = true
127
+ @start_time = Time.now
128
+
129
+ execute_internal
130
+ exitstatus = summarize_internal
131
+
132
+ if exit_when_done
133
+ exit! exitstatus
134
+ else
135
+ exitstatus
136
+ end
137
+ end
138
+
139
+ def summarize_internal
140
+ puts
141
+ puts "==> Summary (#{@completed.size} workers in %.4fs)" % (Time.now-@start_time)
142
+ puts
143
+
144
+ estatus = 0
145
+ misrun_suites = []
146
+ unassigned_suites = []
147
+ @failures = ''
148
+ @completed.each do |worker|
149
+ estatus += (worker.status.exitstatus || 1)
150
+ @stats.record_suites(worker.suites)
151
+ worker.suites.each do |suite|
152
+ assignment = @assignments.delete([suite.name, suite.path])
153
+ host = worker.host || Socket.gethostname
154
+ if assignment.nil?
155
+ unassigned_suites << [suite.name, suite.path]
156
+ elsif assignment != [host, worker.pid]
157
+ misrun_suites << [suite.name, suite.path] + assignment + [host, worker.pid]
158
+ end
159
+ @discovered_suites.delete([suite.name, suite.path])
160
+ end
161
+
162
+ summarize_worker(worker)
163
+
164
+ @failures << worker.failure_output if worker.failure_output
165
+
166
+ puts " [%2d] %60s %4d suites in %.4fs (%s %s)" % [
167
+ worker.num,
168
+ worker.summary,
169
+ worker.suites.size,
170
+ worker.end_time - worker.start_time,
171
+ worker.status.to_s,
172
+ worker.host && " on #{worker.host.split('.').first}"
173
+ ]
174
+ end
175
+
176
+ unless @failures.empty?
177
+ puts
178
+ puts "==> Failures"
179
+ puts
180
+ puts @failures
181
+ end
182
+
183
+ if !relay?
184
+ unless @discovered_suites.empty?
185
+ estatus += 1
186
+ puts
187
+ puts "The following suites were discovered but were not run:"
188
+ puts
189
+
190
+ @discovered_suites.sort.each do |suite_name, path|
191
+ puts "#{suite_name} - #{path}"
192
+ end
193
+ end
194
+ unless unassigned_suites.empty?
195
+ estatus += 1
196
+ puts
197
+ puts "The following suites were not discovered but were run anyway:"
198
+ puts
199
+ unassigned_suites.sort.each do |suite_name, path|
200
+ puts "#{suite_name} - #{path}"
201
+ end
202
+ end
203
+ unless misrun_suites.empty?
204
+ estatus += 1
205
+ puts
206
+ puts "The following suites were run on the wrong workers:"
207
+ puts
208
+ misrun_suites.each do |suite_name, path, target_host, target_pid, actual_host, actual_pid|
209
+ puts "#{suite_name} - #{path}: #{actual_host} (#{actual_pid}) - assigned to #{target_host} (#{target_pid})"
210
+ end
211
+ end
212
+ end
213
+
214
+ puts
215
+
216
+ @stats.save
217
+
218
+ summarize
219
+
220
+ estatus = @completed.inject(0){ |s, worker| s + (worker.status.exitstatus || 1)}
221
+ [estatus, 255].min
222
+ end
223
+
224
+ def summarize
225
+ end
226
+
227
+ def stats_file
228
+ ENV['TEST_QUEUE_STATS'] ||
229
+ '.test_queue_stats'
230
+ end
231
+
232
+ def execute_internal
233
+ start_master
234
+ prepare(@concurrency)
235
+ @prepared_time = Time.now
236
+ start_relay if relay?
237
+ discover_suites
238
+ spawn_workers
239
+ distribute_queue
240
+ ensure
241
+ stop_master
242
+
243
+ kill_subprocesses
244
+ end
245
+
246
+ def start_master
247
+ if !relay?
248
+ if @socket =~ /^(?:(.+):)?(\d+)$/
249
+ address = $1 || '0.0.0.0'
250
+ port = $2.to_i
251
+ @socket = "#$1:#$2"
252
+ @server = TCPServer.new(address, port)
253
+ else
254
+ FileUtils.rm(@socket) if File.exist?(@socket)
255
+ @server = UNIXServer.new(@socket)
256
+ end
257
+ end
258
+
259
+ desc = "test-queue master (#{relay?? "relaying to #{@relay}" : @socket})"
260
+ puts "Starting #{desc}"
261
+ $0 = "#{desc} - #{@procline}"
262
+ end
263
+
264
+ def start_relay
265
+ return unless relay?
266
+
267
+ sock = connect_to_relay
268
+ message = @remote_master_message ? " #{@remote_master_message}" : ""
269
+ message.gsub!(/(\r|\n)/, "") # Our "protocol" is newline-separated
270
+ sock.puts("TOKEN=#{@run_token}")
271
+ sock.puts("REMOTE MASTER #{@concurrency} #{Socket.gethostname} #{message}")
272
+ response = sock.gets.strip
273
+ unless response == "OK"
274
+ STDERR.puts "*** Got non-OK response from master: #{response}"
275
+ sock.close
276
+ exit! 1
277
+ end
278
+ sock.close
279
+ rescue Errno::ECONNREFUSED
280
+ STDERR.puts "*** Unable to connect to relay #{@relay}. Aborting.."
281
+ exit! 1
282
+ end
283
+
284
+ def stop_master
285
+ return if relay?
286
+
287
+ FileUtils.rm_f(@socket) if @socket && @server.is_a?(UNIXServer)
288
+ @server.close rescue nil if @server
289
+ @socket = @server = nil
290
+ end
291
+
292
+ def spawn_workers
293
+ @concurrency.times do |i|
294
+ num = i+1
295
+
296
+ pid = fork do
297
+ @server.close if @server
298
+
299
+ iterator = Iterator.new(@test_framework, relay?? @relay : @socket, method(:around_filter), early_failure_limit: @early_failure_limit, run_token: @run_token)
300
+ after_fork_internal(num, iterator)
301
+ ret = run_worker(iterator) || 0
302
+ cleanup_worker
303
+ Kernel.exit! ret
304
+ end
305
+
306
+ @workers[pid] = Worker.new(pid, num)
307
+ end
308
+ end
309
+
310
+ def discover_suites
311
+ # Remote masters don't discover suites; the central master does and
312
+ # distributes them to remote masters.
313
+ return if relay?
314
+
315
+ # No need to discover suites if all whitelisted suites are already
316
+ # queued.
317
+ return if @whitelist.any? && @awaited_suites.empty?
318
+
319
+ @discovering_suites_pid = fork do
320
+ terminate = false
321
+ Signal.trap("INT") { terminate = true }
322
+
323
+ $0 = "test-queue suite discovery process"
324
+
325
+ @test_framework.all_suite_files.each do |path|
326
+ @test_framework.suites_from_file(path).each do |suite_name, suite|
327
+ Kernel.exit!(0) if terminate
328
+
329
+ @server.connect_address.connect do |sock|
330
+ sock.puts("TOKEN=#{@run_token}")
331
+ sock.puts("NEW SUITE #{Marshal.dump([suite_name, path])}")
332
+ end
333
+ end
334
+ end
335
+
336
+ Kernel.exit! 0
337
+ end
338
+ end
339
+
340
+ def awaiting_suites?
341
+ case
342
+ when @awaited_suites.any?
343
+ # We're waiting to find all the whitelisted suites so we can run them
344
+ # in the correct order.
345
+ true
346
+ when @queue.empty? && !!@discovering_suites_pid
347
+ # We don't have any suites yet, but we're working on it.
348
+ true
349
+ else
350
+ # It's fine to run any queued suites now.
351
+ false
352
+ end
353
+ end
354
+
355
+ def enqueue_discovered_suite(suite_name, path)
356
+ if @whitelist.any? && !@whitelist.include?(suite_name)
357
+ return
358
+ end
359
+
360
+ @discovered_suites << [suite_name, path]
361
+
362
+ if @original_queue.include?([suite_name, path])
363
+ # This suite was already added to the queue some other way.
364
+ @awaited_suites.delete(suite_name)
365
+ return
366
+ end
367
+
368
+ # We don't know how long new suites will take to run, so we put them at
369
+ # the front of the queue. It's better to run a fast suite early than to
370
+ # run a slow suite late.
371
+ @queue.unshift [suite_name, path]
372
+
373
+ if @awaited_suites.delete?(suite_name) && @awaited_suites.empty?
374
+ # We've found all the whitelisted suites. Sort the queue to match the
375
+ # whitelist.
376
+ @queue.sort_by! { |suite_name, path| @whitelist.index(suite_name) }
377
+
378
+ kill_suite_discovery_process("INT")
379
+ end
380
+ end
381
+
382
+ def after_fork_internal(num, iterator)
383
+ srand
384
+
385
+ output = File.open("/tmp/test_queue_worker_#{$$}_output", 'w')
386
+
387
+ $stdout.reopen(output)
388
+ $stderr.reopen($stdout)
389
+ $stdout.sync = $stderr.sync = true
390
+
391
+ $0 = "test-queue worker [#{num}]"
392
+ puts
393
+ puts "==> Starting #$0 (#{Process.pid} on #{Socket.gethostname}) - iterating over #{iterator.sock}"
394
+ puts
395
+
396
+ after_fork(num)
397
+ end
398
+
399
+ # Run in the master before the fork. Used to create
400
+ # concurrency copies of any databases required by the
401
+ # test workers.
402
+ def prepare(concurrency)
403
+ end
404
+
405
+ def around_filter(suite)
406
+ yield
407
+ end
408
+
409
+ # Prepare a worker for executing jobs after a fork.
410
+ def after_fork(num)
411
+ end
412
+
413
+ # Entry point for internal runner implementations. The iterator will yield
414
+ # jobs from the shared queue on the master.
415
+ #
416
+ # Returns an Integer number of failures.
417
+ def run_worker(iterator)
418
+ iterator.each do |item|
419
+ puts " #{item.inspect}"
420
+ end
421
+
422
+ return 0 # exit status
423
+ end
424
+
425
+ def cleanup_worker
426
+ end
427
+
428
+ def summarize_worker(worker)
429
+ worker.summary = ''
430
+ worker.failure_output = ''
431
+ end
432
+
433
+ def reap_workers(blocking=true)
434
+ @workers.delete_if do |_, worker|
435
+ if Process.waitpid(worker.pid, blocking ? 0 : Process::WNOHANG).nil?
436
+ next false
437
+ end
438
+
439
+ worker.status = $?
440
+ worker.end_time = Time.now
441
+
442
+ collect_worker_data(worker)
443
+ relay_to_master(worker) if relay?
444
+ worker_completed(worker)
445
+
446
+ true
447
+ end
448
+ end
449
+
450
+ def collect_worker_data(worker)
451
+ if File.exist?(file = "/tmp/test_queue_worker_#{worker.pid}_output")
452
+ worker.output = IO.binread(file)
453
+ FileUtils.rm(file)
454
+ end
455
+
456
+ if File.exist?(file = "/tmp/test_queue_worker_#{worker.pid}_suites")
457
+ worker.suites.replace(Marshal.load(IO.binread(file)))
458
+ FileUtils.rm(file)
459
+ end
460
+ end
461
+
462
+ def worker_completed(worker)
463
+ return if @aborting
464
+ @completed << worker
465
+ puts worker.output if ENV['TEST_QUEUE_VERBOSE'] || worker.status.exitstatus != 0
466
+ end
467
+
468
+ def distribute_queue
469
+ return if relay?
470
+ remote_workers = 0
471
+
472
+ until !awaiting_suites? && @queue.empty? && remote_workers == 0
473
+ queue_status(@start_time, @queue.size, @workers.size, remote_workers)
474
+
475
+ if status = reap_suite_discovery_process(false)
476
+ abort("Discovering suites failed.") unless status.success?
477
+ abort("Failed to discover #{@awaited_suites.sort.join(", ")} specified in TEST_QUEUE_FORCE") if @awaited_suites.any?
478
+ end
479
+
480
+ if IO.select([@server], nil, nil, 0.1).nil?
481
+ reap_workers(false) # check for worker deaths
482
+ else
483
+ sock = @server.accept
484
+ token = sock.gets.strip
485
+ cmd = sock.gets.strip
486
+
487
+ token = token[TOKEN_REGEX, 1]
488
+ # If we have a remote master from a different test run, respond with "WRONG RUN", and it will consider the test run done.
489
+ if token != @run_token
490
+ message = token.nil? ? "Worker sent no token to master" : "Worker from run #{token} connected to master"
491
+ STDERR.puts "*** #{message} for run #{@run_token}; ignoring."
492
+ sock.write("WRONG RUN\n")
493
+ next
494
+ end
495
+
496
+ case cmd
497
+ when /^POP (\S+) (\d+)/
498
+ hostname = $1
499
+ pid = Integer($2)
500
+ if awaiting_suites?
501
+ sock.write(Marshal.dump("WAIT"))
502
+ elsif obj = @queue.shift
503
+ data = Marshal.dump(obj)
504
+ sock.write(data)
505
+ @assignments[obj] = [hostname, pid]
506
+ end
507
+ when /^REMOTE MASTER (\d+) ([\w\.-]+)(?: (.+))?/
508
+ num = $1.to_i
509
+ remote_master = $2
510
+ remote_master_message = $3
511
+
512
+ sock.write("OK\n")
513
+ remote_workers += num
514
+
515
+ message = "*** #{num} workers connected from #{remote_master} after #{Time.now-@start_time}s"
516
+ message << " " + remote_master_message if remote_master_message
517
+ STDERR.puts message
518
+ when /^WORKER (\d+)/
519
+ data = sock.read($1.to_i)
520
+ worker = Marshal.load(data)
521
+ worker_completed(worker)
522
+ remote_workers -= 1
523
+ when /^NEW SUITE (.+)/
524
+ suite_name, path = Marshal.load($1)
525
+ enqueue_discovered_suite(suite_name, path)
526
+ when /^KABOOM/
527
+ # worker reporting an abnormal number of test failures;
528
+ # stop everything immediately and report the results.
529
+ break
530
+ else
531
+ STDERR.puts("Ignoring unrecognized command: \"#{cmd}\"")
532
+ end
533
+ sock.close
534
+ end
535
+ end
536
+ ensure
537
+ stop_master
538
+ reap_workers
539
+ end
540
+
541
+ def relay?
542
+ !!@relay
543
+ end
544
+
545
+ def connect_to_relay
546
+ sock = nil
547
+ start = Time.now
548
+ puts "Attempting to connect for #{@relay_connection_timeout}s..."
549
+ while sock.nil?
550
+ begin
551
+ sock = TCPSocket.new(*@relay.split(':'))
552
+ rescue Errno::ECONNREFUSED => e
553
+ raise e if Time.now - start > @relay_connection_timeout
554
+ puts "Master not yet available, sleeping..."
555
+ sleep 0.5
556
+ end
557
+ end
558
+ sock
559
+ end
560
+
561
+ def relay_to_master(worker)
562
+ worker.host = Socket.gethostname
563
+ data = Marshal.dump(worker)
564
+
565
+ sock = connect_to_relay
566
+ sock.puts("TOKEN=#{@run_token}")
567
+ sock.puts("WORKER #{data.bytesize}")
568
+ sock.write(data)
569
+ ensure
570
+ sock.close if sock
571
+ end
572
+
573
+ def kill_subprocesses
574
+ kill_workers
575
+ kill_suite_discovery_process
576
+ end
577
+
578
+ def kill_workers
579
+ @workers.each do |pid, worker|
580
+ Process.kill 'KILL', pid
581
+ end
582
+
583
+ reap_workers
584
+ end
585
+
586
+ def kill_suite_discovery_process(signal="KILL")
587
+ return unless @discovering_suites_pid
588
+ Process.kill signal, @discovering_suites_pid
589
+ reap_suite_discovery_process
590
+ end
591
+
592
+ def reap_suite_discovery_process(blocking=true)
593
+ return unless @discovering_suites_pid
594
+ _, status = Process.waitpid2(@discovering_suites_pid, blocking ? 0 : Process::WNOHANG)
595
+ return unless status
596
+
597
+ @discovering_suites_pid = nil
598
+ status
599
+ end
600
+
601
+ # Stop the test run immediately.
602
+ #
603
+ # message - String message to print to the console when exiting.
604
+ #
605
+ # Doesn't return.
606
+ def abort(message)
607
+ @aborting = true
608
+ kill_subprocesses
609
+ Kernel::abort("Aborting: #{message}")
610
+ end
611
+
612
+ # Subclasses can override to monitor the status of the queue.
613
+ #
614
+ # For example, you may want to record metrics about how quickly remote
615
+ # workers connect, or abort the build if not enough connect.
616
+ #
617
+ # This method is called very frequently during the test run, so don't do
618
+ # anything expensive/blocking.
619
+ #
620
+ # This method is not called on remote masters when using remote workers,
621
+ # only on the central master.
622
+ #
623
+ # start_time - Time when the test run began
624
+ # queue_size - Integer number of suites left in the queue
625
+ # local_worker_count - Integer number of active local workers
626
+ # remote_worker_count - Integer number of active remote workers
627
+ #
628
+ # Returns nothing.
629
+ def queue_status(start_time, queue_size, local_worker_count, remote_worker_count)
630
+ end
631
+ end
632
+ end
@@ -0,0 +1,95 @@
1
+ module TestQueue
2
+ class Stats
3
+ class Suite
4
+ attr_reader :name, :path, :duration, :last_seen_at
5
+
6
+ def initialize(name, path, duration, last_seen_at)
7
+ @name = name
8
+ @path = path
9
+ @duration = duration
10
+ @last_seen_at = last_seen_at
11
+
12
+ freeze
13
+ end
14
+
15
+ def ==(other)
16
+ other &&
17
+ name == other.name &&
18
+ path == other.path &&
19
+ duration == other.duration &&
20
+ last_seen_at == other.last_seen_at
21
+ end
22
+ alias_method :eql?, :==
23
+
24
+ def to_h
25
+ { :name => name, :path => path, :duration => duration, :last_seen_at => last_seen_at.to_i }
26
+ end
27
+
28
+ def self.from_hash(hash)
29
+ self.new(hash.fetch(:name),
30
+ hash.fetch(:path),
31
+ hash.fetch(:duration),
32
+ Time.at(hash.fetch(:last_seen_at)))
33
+ end
34
+ end
35
+
36
+ def initialize(path)
37
+ @path = path
38
+ @suites = {}
39
+ load
40
+ end
41
+
42
+ def all_suites
43
+ @suites.values
44
+ end
45
+
46
+ def suite(name)
47
+ @suites[name]
48
+ end
49
+
50
+ def record_suites(suites)
51
+ suites.each do |suite|
52
+ @suites[suite.name] = suite
53
+ end
54
+ end
55
+
56
+ def save
57
+ prune
58
+
59
+ File.open(@path, "wb") do |f|
60
+ Marshal.dump(to_h, f)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ CURRENT_VERSION = 2
67
+
68
+ def to_h
69
+ suites = @suites.each_value.map(&:to_h)
70
+
71
+ { :version => CURRENT_VERSION, :suites => suites }
72
+ end
73
+
74
+ def load
75
+ data = begin
76
+ File.open(@path, "rb") { |f| Marshal.load(f) }
77
+ rescue Errno::ENOENT, EOFError, TypeError, ArgumentError
78
+ end
79
+ return unless data && data.is_a?(Hash) && data[:version] == CURRENT_VERSION
80
+ data[:suites].each do |suite_hash|
81
+ suite = Suite.from_hash(suite_hash)
82
+ @suites[suite.name] = suite
83
+ end
84
+ end
85
+
86
+ EIGHT_DAYS_S = 8 * 24 * 60 * 60
87
+
88
+ def prune
89
+ earliest = Time.now - EIGHT_DAYS_S
90
+ @suites.delete_if do |name, suite|
91
+ suite.last_seen_at < earliest
92
+ end
93
+ end
94
+ end
95
+ end