test-queue-split 0.3.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile-minitest4 +3 -0
  4. data/Gemfile-minitest4.lock +38 -0
  5. data/Gemfile-rspec3-0 +3 -0
  6. data/Gemfile-rspec3-0.lock +40 -0
  7. data/Gemfile-rspec3-1 +3 -0
  8. data/Gemfile-rspec3-1.lock +40 -0
  9. data/Gemfile-rspec3-2 +3 -0
  10. data/Gemfile-rspec3-2.lock +41 -0
  11. data/Gemfile-testunit +3 -0
  12. data/Gemfile-testunit.lock +44 -0
  13. data/Gemfile.lock +54 -0
  14. data/README.md +114 -0
  15. data/Thorfile +9 -0
  16. data/bin/cucumber-queue +4 -0
  17. data/bin/minitest-queue +5 -0
  18. data/bin/rspec-queue +4 -0
  19. data/bin/testunit-queue +5 -0
  20. data/features/bad.feature +5 -0
  21. data/features/sample.feature +25 -0
  22. data/features/sample2.feature +29 -0
  23. data/features/step_definitions/common.rb +15 -0
  24. data/lib/test-queue.rb +1 -0
  25. data/lib/test_queue/iterator.rb +72 -0
  26. data/lib/test_queue/runner/cucumber.rb +46 -0
  27. data/lib/test_queue/runner/minitest.rb +25 -0
  28. data/lib/test_queue/runner/minitest4.rb +65 -0
  29. data/lib/test_queue/runner/minitest5.rb +62 -0
  30. data/lib/test_queue/runner/puppet_lint.rb +31 -0
  31. data/lib/test_queue/runner/rspec.rb +35 -0
  32. data/lib/test_queue/runner/rspec2.rb +35 -0
  33. data/lib/test_queue/runner/rspec3.rb +45 -0
  34. data/lib/test_queue/runner/sample.rb +76 -0
  35. data/lib/test_queue/runner/testunit.rb +56 -0
  36. data/lib/test_queue/runner.rb +395 -0
  37. data/lib/test_queue/version.rb +4 -0
  38. data/lib/test_queue.rb +8 -0
  39. data/test/sample_minispec.rb +31 -0
  40. data/test/sample_minitest4.rb +23 -0
  41. data/test/sample_minitest5.rb +23 -0
  42. data/test/sample_spec.rb +23 -0
  43. data/test/sample_testunit.rb +23 -0
  44. data/test-multi.sh +8 -0
  45. data/test-queue-split.gemspec +28 -0
  46. data/test.sh +23 -0
  47. metadata +141 -0
@@ -0,0 +1,395 @@
1
+ require 'socket'
2
+ require 'fileutils'
3
+ require 'securerandom'
4
+
5
+ module TestQueue
6
+ class Worker
7
+ attr_accessor :pid, :status, :output, :stats, :num, :host
8
+ attr_accessor :start_time, :end_time
9
+ attr_accessor :summary, :failure_output
10
+
11
+ def initialize(pid, num)
12
+ @pid = pid
13
+ @num = num
14
+ @start_time = Time.now
15
+ @output = ''
16
+ @stats = {}
17
+ end
18
+
19
+ def lines
20
+ @output.split("\n")
21
+ end
22
+ end
23
+
24
+ class Runner
25
+ attr_accessor :concurrency
26
+
27
+ def initialize(queue, concurrency=nil, socket=nil, relay=nil)
28
+ raise ArgumentError, 'array required' unless Array === queue
29
+
30
+ if forced = ENV['TEST_QUEUE_FORCE']
31
+ forced = forced.split(/\s*,\s*/)
32
+ whitelist = Set.new(forced)
33
+ queue = queue.select{ |s| whitelist.include?(s.to_s) }
34
+ queue.sort_by!{ |s| forced.index(s.to_s) }
35
+ end
36
+
37
+ @procline = $0
38
+ @queue = queue
39
+ @suites = queue.inject(Hash.new){ |hash, suite| hash.update suite.to_s => suite }
40
+
41
+ @workers = {}
42
+ @completed = []
43
+
44
+ @concurrency =
45
+ concurrency ||
46
+ (ENV['TEST_QUEUE_WORKERS'] && ENV['TEST_QUEUE_WORKERS'].to_i) ||
47
+ if File.exists?('/proc/cpuinfo')
48
+ File.read('/proc/cpuinfo').split("\n").grep(/processor/).size
49
+ elsif RUBY_PLATFORM =~ /darwin/
50
+ `/usr/sbin/sysctl -n hw.activecpu`.to_i
51
+ else
52
+ 2
53
+ end
54
+
55
+ @slave_connection_timeout =
56
+ (ENV['TEST_QUEUE_RELAY_TIMEOUT'] && ENV['TEST_QUEUE_RELAY_TIMEOUT'].to_i) ||
57
+ 30
58
+
59
+ @run_token = ENV['TEST_QUEUE_RELAY_TOKEN'] || SecureRandom.hex(8)
60
+
61
+ @socket =
62
+ socket ||
63
+ ENV['TEST_QUEUE_SOCKET'] ||
64
+ "/tmp/test_queue_#{$$}_#{object_id}.sock"
65
+
66
+ @relay =
67
+ relay ||
68
+ ENV['TEST_QUEUE_RELAY']
69
+
70
+ @slave_message = ENV["TEST_QUEUE_SLAVE_MESSAGE"] if ENV.has_key?("TEST_QUEUE_SLAVE_MESSAGE")
71
+
72
+ if @relay == @socket
73
+ STDERR.puts "*** Detected TEST_QUEUE_RELAY == TEST_QUEUE_SOCKET. Disabling relay mode."
74
+ @relay = nil
75
+ elsif @relay
76
+ @queue = []
77
+ end
78
+ end
79
+
80
+ def stats
81
+ @stats ||=
82
+ if File.exists?(file = stats_file)
83
+ Marshal.load(IO.binread(file)) || {}
84
+ else
85
+ {}
86
+ end
87
+ end
88
+
89
+ def execute
90
+ $stdout.sync = $stderr.sync = true
91
+ @start_time = Time.now
92
+
93
+ @concurrency > 0 ?
94
+ execute_parallel :
95
+ execute_sequential
96
+ ensure
97
+ summarize_internal unless $!
98
+ end
99
+
100
+ def summarize_internal
101
+ puts
102
+ puts "==> Summary (#{@completed.size} workers in %.4fs)" % (Time.now-@start_time)
103
+ puts
104
+
105
+ @failures = ''
106
+ @completed.each do |worker|
107
+ summarize_worker(worker)
108
+ @failures << worker.failure_output if worker.failure_output
109
+
110
+ puts " [%2d] %60s %4d suites in %.4fs (pid %d exit %d%s)" % [
111
+ worker.num,
112
+ worker.summary,
113
+ worker.stats.size,
114
+ worker.end_time - worker.start_time,
115
+ worker.pid,
116
+ worker.status.exitstatus,
117
+ worker.host && " on #{worker.host.split('.').first}"
118
+ ]
119
+ end
120
+
121
+ unless @failures.empty?
122
+ puts
123
+ puts "==> Failures"
124
+ puts
125
+ puts @failures
126
+ end
127
+
128
+ puts
129
+
130
+ if @stats
131
+ File.open(stats_file, 'wb') do |f|
132
+ f.write Marshal.dump(stats)
133
+ end
134
+ end
135
+
136
+ summarize
137
+
138
+ estatus = @completed.inject(0){ |s, worker| s + worker.status.exitstatus }
139
+ estatus = 255 if estatus > 255
140
+ exit!(estatus)
141
+ end
142
+
143
+ def summarize
144
+ end
145
+
146
+ def stats_file
147
+ ENV['TEST_QUEUE_STATS'] ||
148
+ '.test_queue_stats'
149
+ end
150
+
151
+ def execute_sequential
152
+ exit! run_worker(@queue)
153
+ end
154
+
155
+ def execute_parallel
156
+ start_master
157
+ prepare(@concurrency)
158
+ @prepared_time = Time.now
159
+ start_relay if relay?
160
+ spawn_workers
161
+ distribute_queue
162
+ ensure
163
+ stop_master
164
+
165
+ @workers.each do |pid, worker|
166
+ Process.kill 'KILL', pid
167
+ end
168
+
169
+ until @workers.empty?
170
+ reap_worker
171
+ end
172
+ end
173
+
174
+ def start_master
175
+ if !relay?
176
+ if @socket =~ /^(?:(.+):)?(\d+)$/
177
+ address = $1 || '0.0.0.0'
178
+ port = $2.to_i
179
+ @socket = "#$1:#$2"
180
+ @server = TCPServer.new(address, port)
181
+ else
182
+ FileUtils.rm(@socket) if File.exists?(@socket)
183
+ @server = UNIXServer.new(@socket)
184
+ end
185
+ end
186
+
187
+ desc = "test-queue master (#{relay?? "relaying to #{@relay}" : @socket})"
188
+ puts "Starting #{desc}"
189
+ $0 = "#{desc} - #{@procline}"
190
+ end
191
+
192
+ def start_relay
193
+ return unless relay?
194
+
195
+ sock = connect_to_relay
196
+ message = @slave_message ? " #{@slave_message}" : ""
197
+ message.gsub!(/(\r|\n)/, "") # Our "protocol" is newline-separated
198
+ sock.puts("SLAVE #{@concurrency} #{Socket.gethostname} #{@run_token}#{message}")
199
+ response = sock.gets.strip
200
+ unless response == "OK"
201
+ STDERR.puts "*** Got non-OK response from master: #{response}"
202
+ sock.close
203
+ exit! 1
204
+ end
205
+ sock.close
206
+ rescue Errno::ECONNREFUSED
207
+ STDERR.puts "*** Unable to connect to relay #{@relay}. Aborting.."
208
+ exit! 1
209
+ end
210
+
211
+ def stop_master
212
+ return if relay?
213
+
214
+ FileUtils.rm_f(@socket) if @socket && @server.is_a?(UNIXServer)
215
+ @server.close rescue nil if @server
216
+ @socket = @server = nil
217
+ end
218
+
219
+ def spawn_workers
220
+ @concurrency.times do |i|
221
+ num = i+1
222
+
223
+ pid = fork do
224
+ @server.close if @server
225
+
226
+ iterator = Iterator.new(relay?? @relay : @socket, @suites, method(:around_filter))
227
+ after_fork_internal(num, iterator)
228
+ ret = run_worker(iterator) || 0
229
+ cleanup_worker
230
+ Kernel.exit! ret
231
+ end
232
+
233
+ @workers[pid] = Worker.new(pid, num)
234
+ end
235
+ end
236
+
237
+ def after_fork_internal(num, iterator)
238
+ srand
239
+
240
+ output = File.open("/tmp/test_queue_worker_#{$$}_output", 'w')
241
+
242
+ $stdout.reopen(output)
243
+ $stderr.reopen($stdout)
244
+ $stdout.sync = $stderr.sync = true
245
+
246
+ $0 = "test-queue worker [#{num}]"
247
+ puts
248
+ puts "==> Starting #$0 (#{Process.pid} on #{Socket.gethostname}) - iterating over #{iterator.sock}"
249
+ puts
250
+
251
+ after_fork(num)
252
+ end
253
+
254
+ # Run in the master before the fork. Used to create
255
+ # concurrency copies of any databases required by the
256
+ # test workers.
257
+ def prepare(concurrency)
258
+ end
259
+
260
+ def around_filter(suite)
261
+ yield
262
+ end
263
+
264
+ # Prepare a worker for executing jobs after a fork.
265
+ def after_fork(num)
266
+ end
267
+
268
+ # Entry point for internal runner implementations. The iterator will yield
269
+ # jobs from the shared queue on the master.
270
+ #
271
+ # Returns nothing. exits 0 on success.
272
+ # exits N on error, where N is the number of failures.
273
+ def run_worker(iterator)
274
+ iterator.each do |item|
275
+ puts " #{item.inspect}"
276
+ end
277
+
278
+ return 0 # exit status
279
+ end
280
+
281
+ def cleanup_worker
282
+ end
283
+
284
+ def summarize_worker(worker)
285
+ worker.summary = ''
286
+ worker.failure_output = ''
287
+ end
288
+
289
+ def reap_worker(blocking=true)
290
+ if pid = Process.waitpid(-1, blocking ? 0 : Process::WNOHANG) and worker = @workers.delete(pid)
291
+ worker.status = $?
292
+ worker.end_time = Time.now
293
+
294
+ if File.exists?(file = "/tmp/test_queue_worker_#{pid}_output")
295
+ worker.output = IO.binread(file)
296
+ FileUtils.rm(file)
297
+ end
298
+
299
+ if File.exists?(file = "/tmp/test_queue_worker_#{pid}_stats")
300
+ worker.stats = Marshal.load(IO.binread(file))
301
+ FileUtils.rm(file)
302
+ end
303
+
304
+ relay_to_master(worker) if relay?
305
+ worker_completed(worker)
306
+ end
307
+ end
308
+
309
+ def worker_completed(worker)
310
+ @completed << worker
311
+ puts worker.output if ENV['TEST_QUEUE_VERBOSE'] || worker.status.exitstatus != 0
312
+ end
313
+
314
+ def distribute_queue
315
+ return if relay?
316
+ remote_workers = 0
317
+
318
+ until @queue.empty? && remote_workers == 0
319
+ if IO.select([@server], nil, nil, 0.1).nil?
320
+ reap_worker(false) if @workers.any? # check for worker deaths
321
+ else
322
+ sock = @server.accept
323
+ cmd = sock.gets.strip
324
+ case cmd
325
+ when /^POP/
326
+ # If we have a slave from a different test run, don't respond, and it will consider the test run done.
327
+ if obj = @queue.shift
328
+ data = Marshal.dump(obj.to_s)
329
+ sock.write(data)
330
+ end
331
+ when /^SLAVE (\d+) ([\w\.-]+) (\w+)(?: (.+))?/
332
+ num = $1.to_i
333
+ slave = $2
334
+ run_token = $3
335
+ slave_message = $4
336
+ if run_token == @run_token
337
+ # If we have a slave from a different test run, don't respond, and it will consider the test run done.
338
+ sock.write("OK\n")
339
+ remote_workers += num
340
+ else
341
+ STDERR.puts "*** Worker from run #{run_token} connected to master for run #{@run_token}; ignoring."
342
+ sock.write("WRONG RUN\n")
343
+ end
344
+ message = "*** #{num} workers connected from #{slave} after #{Time.now-@start_time}s"
345
+ message << " " + slave_message if slave_message
346
+ STDERR.puts message
347
+ when /^WORKER (\d+)/
348
+ data = sock.read($1.to_i)
349
+ worker = Marshal.load(data)
350
+ worker_completed(worker)
351
+ remote_workers -= 1
352
+ end
353
+ sock.close
354
+ end
355
+ end
356
+ ensure
357
+ stop_master
358
+
359
+ until @workers.empty?
360
+ reap_worker
361
+ end
362
+ end
363
+
364
+ def relay?
365
+ !!@relay
366
+ end
367
+
368
+ def connect_to_relay
369
+ sock = nil
370
+ start = Time.now
371
+ puts "Attempting to connect for #{@slave_connection_timeout}s..."
372
+ while sock.nil?
373
+ begin
374
+ sock = TCPSocket.new(*@relay.split(':'))
375
+ rescue Errno::ECONNREFUSED => e
376
+ raise e if Time.now - start > @slave_connection_timeout
377
+ puts "Master not yet available, sleeping..."
378
+ sleep 0.5
379
+ end
380
+ end
381
+ sock
382
+ end
383
+
384
+ def relay_to_master(worker)
385
+ worker.host = Socket.gethostname
386
+ data = Marshal.dump(worker)
387
+
388
+ sock = connect_to_relay
389
+ sock.puts("WORKER #{data.bytesize}")
390
+ sock.write(data)
391
+ ensure
392
+ sock.close if sock
393
+ end
394
+ end
395
+ end
@@ -0,0 +1,4 @@
1
+ module TestQueue
2
+ VERSION = '0.3.0' unless defined? ::TestQueue::VERSION
3
+ DATE = '2015-08-31' unless defined? ::TestQueue::DATE
4
+ end
data/lib/test_queue.rb ADDED
@@ -0,0 +1,8 @@
1
+ if !IO.respond_to?(:binread)
2
+ class << IO
3
+ alias :binread :read
4
+ end
5
+ end
6
+
7
+ require 'test_queue/iterator'
8
+ require 'test_queue/runner'
@@ -0,0 +1,31 @@
1
+ require 'minitest/spec'
2
+
3
+ class Meme
4
+ def i_can_has_cheezburger?
5
+ "OHAI!"
6
+ end
7
+
8
+ def will_it_blend?
9
+ "YES!"
10
+ end
11
+ end
12
+
13
+ describe Meme do
14
+ before do
15
+ @meme = Meme.new
16
+ end
17
+
18
+ describe "when asked about cheeseburgers" do
19
+ it "must respond positively" do
20
+ sleep 0.1
21
+ @meme.i_can_has_cheezburger?.must_equal "OHAI!"
22
+ end
23
+ end
24
+
25
+ describe "when asked about blending possibilities" do
26
+ it "won't say no" do
27
+ sleep 0.1
28
+ @meme.will_it_blend?.wont_match /^no/i
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ require 'minitest/unit'
2
+
3
+ class MiniTestEqual < MiniTest::Unit::TestCase
4
+ def test_equal
5
+ assert_equal 1, 1
6
+ end
7
+ end
8
+
9
+ 30.times do |i|
10
+ Object.const_set("MiniTestSleep#{i}", Class.new(MiniTest::Unit::TestCase) do
11
+ define_method('test_sleep') do
12
+ start = Time.now
13
+ sleep(0.25)
14
+ assert_in_delta Time.now-start, 0.25, 0.02
15
+ end
16
+ end)
17
+ end
18
+
19
+ class MiniTestFailure < MiniTest::Unit::TestCase
20
+ def test_fail
21
+ assert_equal 0, 1
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require 'minitest/autorun'
2
+
3
+ class MiniTestEqual < MiniTest::Test
4
+ def test_equal
5
+ assert_equal 1, 1
6
+ end
7
+ end
8
+
9
+ 30.times do |i|
10
+ Object.const_set("MiniTestSleep#{i}", Class.new(MiniTest::Test) do
11
+ define_method('test_sleep') do
12
+ start = Time.now
13
+ sleep(0.25)
14
+ assert_in_delta Time.now-start, 0.25, 0.02
15
+ end
16
+ end)
17
+ end
18
+
19
+ class MiniTestFailure < MiniTest::Test
20
+ def test_fail
21
+ assert_equal 0, 1
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require 'rspec'
2
+
3
+ describe 'RSpecEqual' do
4
+ it 'checks equality' do
5
+ expect(1).to eq 1
6
+ end
7
+ end
8
+
9
+ 30.times do |i|
10
+ describe "RSpecSleep(#{i})" do
11
+ it "sleeps" do
12
+ start = Time.now
13
+ sleep(0.25)
14
+ expect(Time.now-start).to be_within(0.02).of(0.25)
15
+ end
16
+ end
17
+ end
18
+
19
+ describe 'RSpecFailure' do
20
+ it 'fails' do
21
+ expect(:foo).to eq :bar
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require 'test/unit'
2
+
3
+ class TestUnitEqual < Test::Unit::TestCase
4
+ def test_equal
5
+ assert_equal 1, 1
6
+ end
7
+ end
8
+
9
+ 30.times do |i|
10
+ Object.const_set("TestUnitSleep#{i}", Class.new(Test::Unit::TestCase) do
11
+ define_method('test_sleep') do
12
+ start = Time.now
13
+ sleep(0.25)
14
+ assert_in_delta Time.now-start, 0.25, 0.02
15
+ end
16
+ end)
17
+ end
18
+
19
+ class TestUnitFailure < Test::Unit::TestCase
20
+ def test_fail
21
+ assert_equal 0, 1
22
+ end
23
+ end
data/test-multi.sh ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+ set -x
3
+
4
+ # export TEST_QUEUE_VERBOSE=1
5
+ TEST_QUEUE_SOCKET=0.0.0.0:12345 bundle exec minitest-queue ./test/sample_minitest5.rb &
6
+ sleep 0.1
7
+ TEST_QUEUE_RELAY=0.0.0.0:12345 bundle exec minitest-queue ./test/sample_minitest5.rb
8
+ wait
@@ -0,0 +1,28 @@
1
+ require_relative 'lib/test_queue/version'
2
+
3
+ spec = Gem::Specification.new do |s|
4
+ s.name = 'test-queue-split'
5
+ s.version = TestQueue::VERSION
6
+ s.date = TestQueue::DATE
7
+ s.summary = 'parallel test runner'
8
+ s.description = 'minitest/rspec parallel test runner for CI environments'
9
+
10
+ s.homepage = 'http://github.com/tmm1/test-queue'
11
+
12
+ s.authors = ['Aman Gupta']
13
+ s.email = 'ruby@tmm1.net'
14
+ s.license = 'MIT'
15
+
16
+ s.has_rdoc = false
17
+ s.bindir = 'bin'
18
+ s.executables << 'rspec-queue'
19
+ s.executables << 'minitest-queue'
20
+ s.executables << 'testunit-queue'
21
+ s.executables << 'cucumber-queue'
22
+
23
+ s.add_development_dependency 'rspec', '>= 2.13', '< 4.0'
24
+ s.add_development_dependency 'cucumber', '~> 1.3.10'
25
+ s.add_development_dependency 'appium_thor', '~> 1.0.1'
26
+
27
+ s.files = `git ls-files`.split("\n")
28
+ end
data/test.sh ADDED
@@ -0,0 +1,23 @@
1
+ #!/bin/sh
2
+ set -x
3
+
4
+ export TEST_QUEUE_WORKERS=2 TEST_QUEUE_VERBOSE=1
5
+
6
+ export BUNDLE_GEMFILE=Gemfile-testunit
7
+ bundle install
8
+ bundle exec testunit-queue ./test/*_testunit.rb
9
+
10
+ export BUNDLE_GEMFILE=Gemfile-minitest4
11
+ bundle install
12
+ bundle exec minitest-queue ./test/*_minitest4.rb
13
+ bundle exec minitest-queue ./test/*_minispec.rb
14
+
15
+ export BUNDLE_GEMFILE=Gemfile
16
+ bundle install
17
+ bundle exec minitest-queue ./test/*_minitest4.rb
18
+ bundle exec minitest-queue ./test/*_minitest5.rb
19
+ bundle exec minitest-queue ./test/*_minispec.rb
20
+ bundle exec rspec-queue test
21
+ bundle exec cucumber-queue
22
+
23
+ TEST_QUEUE_WORKERS=1 TEST_QUEUE_FORCE="MiniTestSleep21,MiniTestSleep8,MiniTestFailure" bundle exec minitest-queue ./test/*_minitest5.rb