test-queue 0.1.3 → 0.2.0.beta.1

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