test-queue 0.1.3 → 0.2.0.beta.1

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.
data/README.md CHANGED
@@ -7,13 +7,6 @@ Specifically optimized for CI environments: build statistics from each run
7
7
  are stored locally and used to sort the queue at the beginning of the
8
8
  next run.
9
9
 
10
- ### usage
11
-
12
- ```
13
- $ minitest-queue $(find test/ -name \*_test.rb)
14
- $ rspec-queue --format progress spec
15
- ```
16
-
17
10
  ### design
18
11
 
19
12
  test-queue uses a simple master + pre-fork worker model. The master
@@ -28,17 +21,45 @@ the queue.
28
21
  └─── 21562 minitest-queue worker [0] - UserTest
29
22
  ```
30
23
 
31
- ### customization
24
+ test-queue also has a distributed mode, where additional masters can share
25
+ the workload and relay results back to a central master.
26
+
27
+ ### environment variables
28
+
29
+ - `TEST_QUEUE_WORKERS`: number of workers to use per master (default: all available cores)
30
+ - `TEST_QUEUE_VERBOSE`: show results as they are available (default: `0`)
31
+ - `TEST_QUEUE_SOCKET`: unix socket `path` (or tcp `address:port` pair) used for communication (default: `/tmp/test_queue_XXXXX.sock`)
32
+ - `TEST_QUEUE_RELAY`: relay results back to a central master, specified as tcp `address:port`
33
+ - `TEST_QUEUE_STATS`: `path` to cache build stats in-build CI runs (default: `.test_queue_stats`)
34
+
35
+ ### usage
36
+
37
+ test-queue bundles `minitest-queue` and `rspec-queue` binaries which can be used directly:
38
+
39
+ ```
40
+ $ minitest-queue $(find test/ -name \*_test.rb)
41
+ $ rspec-queue --format progress spec
42
+ ```
43
+
44
+ But the underlying `TestQueue::Runner::MiniTest` and `TestQueue::Runner::Rspec` are
45
+ built to be subclassed by your application. I recommend checking a new
46
+ executable into your project using one of these superclasses.
47
+
48
+ ```
49
+ $ vim script/test-queue
50
+ $ chmod +x script/test-queue
51
+ $ git add script/test-queue
52
+ ```
32
53
 
33
54
  Since test-queue uses `fork(2)` to spawn off workers, you must ensure each worker
34
55
  runs in an isolated environment. Use the `after_fork` hook with a custom
35
- runner to reset any global state:
56
+ runner to reset any global state.
36
57
 
37
58
  ``` ruby
38
- class CustomMiniTestRunner < TestQueue::Runner::MiniTest
39
- def after_fork(num)
40
- super
59
+ #!/usr/bin/env ruby
41
60
 
61
+ class MyAppTestRunner < TestQueue::Runner::MiniTest
62
+ def after_fork(num)
42
63
  # use separate mysql database (we assume it exists and has the right schema already)
43
64
  ActiveRecord::Base.configurations['test']['database'] << num.to_s
44
65
  ActiveRecord::Base.establish_connection(:test)
@@ -47,11 +68,37 @@ class CustomMiniTestRunner < TestQueue::Runner::MiniTest
47
68
  $redis.client.db = num
48
69
  $redis.client.reconnect
49
70
  end
71
+
72
+ def prepare(concurrency)
73
+ # create mysql databases exists with correct schema
74
+ concurrency.times do |i|
75
+ # ...
76
+ end
77
+ end
78
+
79
+ def around_filter(suite)
80
+ $stats.timing("test.#{suite}.runtime") do
81
+ yield
82
+ end
83
+ end
50
84
  end
51
85
 
52
86
  CustomMiniTestRunner.new.execute
53
87
  ```
54
88
 
89
+ ### distributed mode
90
+
91
+ To use distributed mode, the central master must listen on a tcp port. Additional masters can be booted
92
+ in relay mode to connect to the central master.
93
+
94
+ ```
95
+ $ TEST_QUEUE_SOCKET=0.0.0.0:12345 bundle exec minitest-queue ./test/sample_test.rb
96
+ $ TEST_QUEUE_RELAY=0.0.0.0:12345 bundle exec minitest-queue ./test/sample_test.rb
97
+ ```
98
+
99
+ See the [Parameterized Trigger Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Parameterized+Trigger+Plugin)
100
+ for a simple way to do this with jenkins.
101
+
55
102
  ### see also
56
103
 
57
104
  * https://github.com/Shopify/rails_parallel
@@ -1,30 +1,42 @@
1
1
  module TestQueue
2
2
  class Iterator
3
- attr_reader :stats
3
+ attr_reader :stats, :sock
4
4
 
5
- def initialize(sock)
6
- @sock = sock
5
+ def initialize(sock, suites, filter=nil)
7
6
  @done = false
8
7
  @stats = {}
9
8
  @procline = $0
9
+ @sock = sock
10
+ @suites = suites
11
+ @filter = filter
12
+ if @sock =~ /^(.+):(\d+)$/
13
+ @tcp_address = $1
14
+ @tcp_port = $2.to_i
15
+ end
10
16
  end
11
17
 
12
18
  def each
13
19
  fail 'already used this iterator' if @done
14
20
 
15
21
  while true
16
- client = UNIXSocket.new(@sock)
22
+ client = connect_to_master('POP')
17
23
  r, w, e = IO.select([client], nil, [client], nil)
18
24
  break if !e.empty?
19
25
 
20
- if data = client.read(16384)
26
+ if data = client.read(65536)
21
27
  client.close
22
28
  item = Marshal.load(data)
23
- $0 = "#{@procline} - #{item.respond_to?(:description) ? item.description : item}"
29
+ break if item.nil?
30
+ suite = @suites[item]
24
31
 
32
+ $0 = "#{@procline} - #{suite.respond_to?(:description) ? suite.description : suite}"
25
33
  start = Time.now
26
- yield item
27
- @stats[item] = Time.now - start
34
+ if @filter
35
+ @filter.call(suite){ yield suite }
36
+ else
37
+ yield suite
38
+ end
39
+ @stats[suite.to_s] = Time.now - start
28
40
  else
29
41
  break
30
42
  end
@@ -37,6 +49,17 @@ module TestQueue
37
49
  end
38
50
  end
39
51
 
52
+ def connect_to_master(cmd)
53
+ sock =
54
+ if @tcp_address
55
+ TCPSocket.new(@tcp_address, @tcp_port)
56
+ else
57
+ UNIXSocket.new(@sock)
58
+ end
59
+ sock.puts(cmd)
60
+ sock
61
+ end
62
+
40
63
  include Enumerable
41
64
 
42
65
  def empty?
@@ -3,7 +3,7 @@ require 'fileutils'
3
3
 
4
4
  module TestQueue
5
5
  class Worker
6
- attr_accessor :pid, :status, :output, :stats, :num
6
+ attr_accessor :pid, :status, :output, :stats, :num, :host
7
7
  attr_accessor :start_time, :end_time
8
8
 
9
9
  def initialize(pid, num)
@@ -11,6 +11,7 @@ module TestQueue
11
11
  @num = num
12
12
  @start_time = Time.now
13
13
  @output = ''
14
+ @stats = {}
14
15
  end
15
16
 
16
17
  def lines
@@ -21,10 +22,12 @@ module TestQueue
21
22
  class Runner
22
23
  attr_accessor :concurrency
23
24
 
24
- def initialize(queue, concurrency=nil)
25
+ def initialize(queue, concurrency=nil, socket=nil, relay=nil)
25
26
  raise ArgumentError, 'array required' unless Array === queue
26
27
 
28
+ @procline = $0
27
29
  @queue = queue
30
+ @suites = queue.inject(Hash.new){ |hash, suite| hash.update suite.to_s => suite }
28
31
 
29
32
  @workers = {}
30
33
  @completed = []
@@ -39,11 +42,27 @@ module TestQueue
39
42
  else
40
43
  2
41
44
  end
45
+
46
+ @socket =
47
+ socket ||
48
+ ENV['TEST_QUEUE_SOCKET'] ||
49
+ "/tmp/test_queue_#{$$}_#{object_id}.sock"
50
+
51
+ @relay =
52
+ relay ||
53
+ ENV['TEST_QUEUE_RELAY']
54
+
55
+ if @relay == @socket
56
+ STDERR.puts "*** Detected TEST_QUEUE_RELAY == TEST_QUEUE_SOCKET. Disabling relay mode."
57
+ @relay = nil
58
+ elsif @relay
59
+ @queue = []
60
+ end
42
61
  end
43
62
 
44
63
  def stats
45
64
  @stats ||=
46
- if File.exists?(file = '.test_queue_stats')
65
+ if File.exists?(file = stats_file)
47
66
  Marshal.load(IO.binread(file)) || {}
48
67
  else
49
68
  {}
@@ -52,13 +71,18 @@ module TestQueue
52
71
 
53
72
  def execute
54
73
  $stdout.sync = $stderr.sync = true
74
+ @start_time = Time.now
55
75
 
56
76
  @concurrency > 0 ?
57
77
  execute_parallel :
58
78
  execute_sequential
59
79
  ensure
80
+ summarize_internal
81
+ end
82
+
83
+ def summarize_internal
60
84
  puts
61
- puts "==> Summary"
85
+ puts "==> Summary (#{@completed.size} workers in %.4fs)" % (Time.now-@start_time)
62
86
  puts
63
87
 
64
88
  @failures = ''
@@ -66,12 +90,14 @@ module TestQueue
66
90
  summary, failures = summarize_worker(worker)
67
91
  @failures << failures if failures
68
92
 
69
- puts " [%2d] %55s in %.4fs (pid %d exit %d)" % [
93
+ puts " [%2d] %60s %4d suites in %.4fs (pid %d exit %d%s)" % [
70
94
  worker.num,
71
95
  summary,
96
+ worker.stats.size,
72
97
  worker.end_time - worker.start_time,
73
98
  worker.pid,
74
- worker.status.exitstatus
99
+ worker.status.exitstatus,
100
+ worker.host && " on #{worker.host.split('.').first}"
75
101
  ]
76
102
  end
77
103
 
@@ -85,14 +111,23 @@ module TestQueue
85
111
  puts
86
112
 
87
113
  if @stats
88
- File.open('.test_queue_stats', 'wb') do |f|
114
+ File.open(stats_file, 'wb') do |f|
89
115
  f.write Marshal.dump(stats)
90
116
  end
91
117
  end
92
118
 
119
+ summarize
93
120
  exit! @completed.inject(0){ |s, worker| s + worker.status.exitstatus }
94
121
  end
95
122
 
123
+ def summarize
124
+ end
125
+
126
+ def stats_file
127
+ ENV['TEST_QUEUE_STATS'] ||
128
+ '.test_queue_stats'
129
+ end
130
+
96
131
  def execute_sequential
97
132
  exit! run_worker(@queue)
98
133
  end
@@ -114,32 +149,59 @@ module TestQueue
114
149
  end
115
150
 
116
151
  def start_master
117
- @socket = "/tmp/test_queue_#{$$}_#{object_id}.sock"
118
- FileUtils.rm(@socket) if File.exists?(@socket)
119
- @server = UNIXServer.new(@socket)
152
+ if relay?
153
+ begin
154
+ sock = connect_to_relay
155
+ sock.puts("SLAVE #{@concurrency}")
156
+ sock.close
157
+ rescue Errno::ECONNREFUSED
158
+ STDERR.puts "*** Unable to connect to relay #{@relay}. Aborting.."
159
+ exit! 1
160
+ end
161
+ else
162
+ if @socket =~ /^(?:(.+):)?(\d+)$/
163
+ address = $1 || '0.0.0.0'
164
+ port = $2.to_i
165
+ @socket = "#$1:#$2"
166
+ @server = TCPServer.new(address, port)
167
+ else
168
+ FileUtils.rm(@socket) if File.exists?(@socket)
169
+ @server = UNIXServer.new(@socket)
170
+ end
171
+ end
172
+
173
+ desc = "test-queue master (#{relay?? "relaying to #{@relay}" : @socket})"
174
+ puts "Starting #{desc}"
175
+ $0 = "#{desc} - #{@procline}"
120
176
  end
121
177
 
122
178
  def stop_master
123
- FileUtils.rm_f(@socket) if @socket
179
+ return if relay?
180
+
181
+ FileUtils.rm_f(@socket) if @socket && @server.is_a?(UNIXServer)
124
182
  @server.close rescue nil if @server
125
183
  @socket = @server = nil
126
184
  end
127
185
 
128
186
  def spawn_workers
187
+ prepare(@concurrency)
188
+
129
189
  @concurrency.times do |i|
130
190
  num = i+1
131
191
 
132
192
  pid = fork do
133
- @server.close
134
- after_fork(num)
135
- exit! run_worker(iterator = Iterator.new(@socket)) || 0
193
+ @server.close if @server
194
+
195
+ iterator = Iterator.new(relay?? @relay : @socket, @suites, method(:around_filter))
196
+ after_fork_internal(num, iterator)
197
+ exit! run_worker(iterator) || 0
136
198
  end
137
199
 
138
200
  @workers[pid] = Worker.new(pid, num)
139
201
  end
140
202
  end
141
203
 
142
- def after_fork(num)
204
+ def after_fork_internal(num, iterator)
143
205
  srand
144
206
 
145
207
  output = File.open("/tmp/test_queue_worker_#{$$}_output", 'w')
@@ -150,8 +212,20 @@ module TestQueue
150
212
 
151
213
  $0 = "test-queue worker [#{num}]"
152
214
  puts
153
- puts "==> Starting #$0 (#{Process.pid})"
215
+ puts "==> Starting #$0 (#{Process.pid}) - iterating over #{iterator.sock}"
154
216
  puts
217
+
218
+ after_fork(num)
219
+ end
220
+
221
+ def prepare(concurrency)
222
+ end
223
+
224
+ def around_filter(suite)
225
+ yield
226
+ end
227
+
228
+ def after_fork(num)
155
229
  end
156
230
 
157
231
  def run_worker(iterator)
@@ -169,15 +243,13 @@ module TestQueue
169
243
  [ num_tests, failures ]
170
244
  end
171
245
 
172
- def cleanup_worker
173
- if pid = Process.waitpid and worker = @workers.delete(pid)
174
- @completed << worker
246
+ def cleanup_worker(blocking=true)
247
+ if pid = Process.waitpid(-1, blocking ? 0 : Process::WNOHANG) and worker = @workers.delete(pid)
175
248
  worker.status = $?
176
249
  worker.end_time = Time.now
177
250
 
178
251
  if File.exists?(file = "/tmp/test_queue_worker_#{pid}_output")
179
252
  worker.output = IO.binread(file)
180
- puts worker.output
181
253
  FileUtils.rm(file)
182
254
  end
183
255
 
@@ -185,16 +257,43 @@ module TestQueue
185
257
  worker.stats = Marshal.load(IO.binread(file))
186
258
  FileUtils.rm(file)
187
259
  end
260
+
261
+ relay_to_master(worker) if relay?
262
+ worker_completed(worker)
188
263
  end
189
264
  end
190
265
 
266
+ def worker_completed(worker)
267
+ @completed << worker
268
+ puts worker.output if ENV['TEST_QUEUE_VERBOSE']
269
+ end
270
+
191
271
  def distribute_queue
192
- until @queue.empty?
193
- IO.select([@server], nil, nil, nil)
272
+ return if relay?
273
+ remote_workers = 0
194
274
 
195
- sock = @server.accept
196
- sock.write(Marshal.dump(@queue.shift))
197
- sock.close
275
+ until @queue.empty? && remote_workers == 0
276
+ if IO.select([@server], nil, nil, 0.1).nil?
277
+ cleanup_worker(false) # check for worker deaths
278
+ else
279
+ sock = @server.accept
280
+ cmd = sock.gets.strip
281
+ case cmd
282
+ when 'POP'
283
+ data = Marshal.dump(@queue.shift.to_s)
284
+ sock.write(data)
285
+ when /^SLAVE (\d+)/
286
+ num = $1.to_i
287
+ remote_workers += num
288
+ STDERR.puts "*** slave connected with additional #{num} workers"
289
+ when /^WORKER (\d+)/
290
+ data = sock.read($1.to_i)
291
+ worker = Marshal.load(data)
292
+ worker_completed(worker)
293
+ remote_workers -= 1
294
+ end
295
+ sock.close
296
+ end
198
297
  end
199
298
  ensure
200
299
  stop_master
@@ -203,5 +302,24 @@ module TestQueue
203
302
  cleanup_worker
204
303
  end
205
304
  end
305
+
306
+ def relay?
307
+ !!@relay
308
+ end
309
+
310
+ def connect_to_relay
311
+ TCPSocket.new(*@relay.split(':'))
312
+ end
313
+
314
+ def relay_to_master(worker)
315
+ worker.host = Socket.gethostname
316
+ data = Marshal.dump(worker)
317
+
318
+ sock = connect_to_relay
319
+ sock.puts("WORKER #{data.bytesize}")
320
+ sock.write(data)
321
+ ensure
322
+ sock.close if sock
323
+ end
206
324
  end
207
325
  end
@@ -45,7 +45,8 @@ module TestQueue
45
45
  class Runner
46
46
  class MiniTest < Runner
47
47
  def initialize
48
- super(::MiniTest::Unit::TestCase.original_test_suites.sort_by{ |s| -(stats[s.to_s] || 0) })
48
+ tests = ::MiniTest::Unit::TestCase.original_test_suites.sort_by{ |s| -(stats[s.to_s] || 0) }
49
+ super(tests)
49
50
  end
50
51
 
51
52
  def run_worker(iterator)
@@ -61,7 +62,8 @@ module TestQueue
61
62
  num_tests = worker.lines.grep(/ errors?, /).first
62
63
  failures = worker.lines.select{ |line|
63
64
  line if (line =~ /^Finished/) ... (line =~ / errors?, /)
64
- }[1..-2].join("\n")
65
+ }[1..-2]
66
+ failures = failures.join("\n") if failures
65
67
 
66
68
  [ num_tests, failures ]
67
69
  end
data/test-queue.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  spec = Gem::Specification.new do |s|
2
2
  s.name = 'test-queue'
3
- s.version = '0.1.3'
3
+ s.version = '0.2.0.beta.1'
4
4
  s.summary = 'parallel test runner'
5
5
 
6
6
  s.homepage = "http://github.com/tmm1/test-queue"
@@ -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
data/test/sample_spec.rb CHANGED
@@ -6,11 +6,13 @@ describe 'RSpecEqual' do
6
6
  end
7
7
  end
8
8
 
9
- describe 'RSpecSleep' do
10
- it 'sleeps' do
11
- start = Time.now
12
- sleep 0.25
13
- (Time.now-start).should be_within(0.02).of(0.25)
9
+ 50.times do |i|
10
+ describe "RSpecSleep(#{i})" do
11
+ it "sleeps" do
12
+ start = Time.now
13
+ sleep(0.25)
14
+ (Time.now-start).should be_within(0.02).of(0.25)
15
+ end
14
16
  end
15
17
  end
16
18
 
data/test/sample_test.rb CHANGED
@@ -6,12 +6,14 @@ class MiniTestEqual < MiniTest::Unit::TestCase
6
6
  end
7
7
  end
8
8
 
9
- class MiniTestSleep < MiniTest::Unit::TestCase
10
- def test_sleep
11
- start = Time.now
12
- sleep 0.25
13
- assert_in_delta Time.now-start, 0.25, 0.02
14
- end
9
+ 5.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)
15
17
  end
16
18
 
17
19
  class MiniTestFailure < MiniTest::Unit::TestCase
metadata CHANGED
@@ -1,63 +1,56 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: test-queue
3
- version: !ruby/object:Gem::Version
4
- hash: 29
5
- prerelease:
6
- segments:
7
- - 0
8
- - 1
9
- - 3
10
- version: 0.1.3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0.beta.1
5
+ prerelease: 6
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Aman Gupta
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2013-04-29 00:00:00 Z
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
12
+ date: 2013-11-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
21
15
  name: rspec
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
24
17
  none: false
25
- requirements:
18
+ requirements:
26
19
  - - ~>
27
- - !ruby/object:Gem::Version
28
- hash: 25
29
- segments:
30
- - 2
31
- - 13
32
- version: "2.13"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.13'
33
22
  type: :development
34
- version_requirements: *id001
35
- - !ruby/object:Gem::Dependency
36
- name: minitest
37
23
  prerelease: false
38
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '2.13'
30
+ - !ruby/object:Gem::Dependency
31
+ name: minitest
32
+ requirement: !ruby/object:Gem::Requirement
39
33
  none: false
40
- requirements:
34
+ requirements:
41
35
  - - ~>
42
- - !ruby/object:Gem::Version
43
- hash: 37
44
- segments:
45
- - 4
46
- - 7
47
- - 3
36
+ - !ruby/object:Gem::Version
48
37
  version: 4.7.3
49
38
  type: :development
50
- version_requirements: *id002
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 4.7.3
51
46
  description:
52
47
  email: ruby@tmm1.net
53
- executables:
48
+ executables:
54
49
  - rspec-queue
55
50
  - minitest-queue
56
51
  extensions: []
57
-
58
52
  extra_rdoc_files: []
59
-
60
- files:
53
+ files:
61
54
  - Gemfile
62
55
  - Gemfile.lock
63
56
  - README.md
@@ -72,41 +65,32 @@ files:
72
65
  - lib/test_queue/runner/rspec.rb
73
66
  - lib/test_queue/runner/sample.rb
74
67
  - test-queue.gemspec
68
+ - test/sample_minispec.rb
75
69
  - test/sample_spec.rb
76
70
  - test/sample_test.rb
77
71
  homepage: http://github.com/tmm1/test-queue
78
72
  licenses: []
79
-
80
73
  post_install_message:
81
74
  rdoc_options: []
82
-
83
- require_paths:
75
+ require_paths:
84
76
  - lib
85
- required_ruby_version: !ruby/object:Gem::Requirement
77
+ required_ruby_version: !ruby/object:Gem::Requirement
86
78
  none: false
87
- requirements:
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- hash: 3
91
- segments:
92
- - 0
93
- version: "0"
94
- required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
84
  none: false
96
- requirements:
97
- - - ">="
98
- - !ruby/object:Gem::Version
99
- hash: 3
100
- segments:
101
- - 0
102
- version: "0"
85
+ requirements:
86
+ - - ! '>'
87
+ - !ruby/object:Gem::Version
88
+ version: 1.3.1
103
89
  requirements: []
104
-
105
90
  rubyforge_project:
106
- rubygems_version: 1.8.24
91
+ rubygems_version: 1.8.23
107
92
  signing_key:
108
93
  specification_version: 3
109
94
  summary: parallel test runner
110
95
  test_files: []
111
-
112
96
  has_rdoc: false