test-queue-split 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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