thread_order 1.0.0 → 1.1.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.
- checksums.yaml +13 -5
- data/.gitignore +1 -0
- data/Readme.md +51 -0
- data/lib/thread_order.rb +64 -45
- data/lib/thread_order/mutex.rb +10 -10
- data/lib/thread_order/version.rb +1 -1
- data/spec/run +11 -11
- data/spec/thread_order_spec.rb +137 -14
- data/thread_order.gemspec +2 -2
- metadata +10 -10
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZmFjZjVkN2Y3Y2Y0M2UwMzNiODEwMzM5YWMwZjA2NjBkMzhkZGU2Zg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZThiYjJhMDE4YzFkMzZkNWM2YWIxNzMyOTExNDgyZjJlM2MzYmRjYw==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NDhhN2IyYzA0Y2UyZmZkZWI5NDU1MDdmYWE0Y2E3ZDE3YTFkZjRlZjgwODIy
|
10
|
+
OTIxMjliZDc1NmZkMTU4NjA5OTYzYjliYmI1N2QxNjdhNGVjMTZlZDc5OGFj
|
11
|
+
N2UwY2U3ZjQ5ZDdlY2RkMWE4OThjYzNlMjU0MTEwZTc4MjM0NGQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MTI1ZDZkMGE1NGM0MzIzZTdiMGRjMDAyNjJhMTlmNWE4NWY5MWFjOTQyOTg5
|
14
|
+
NDJiMTI5YzJhNzE3MTIxNzYzNzRkOGEzMTcwNzQ4ZTBjMTM3YTZiZWVlODk3
|
15
|
+
YWRlMjM5YTg3YmY0ZTdmYTVlYjYxYWQ1ZmMwZmUwMDkwM2E0OTQ=
|
data/.gitignore
CHANGED
data/Readme.md
CHANGED
@@ -9,3 +9,54 @@ Its purpose is to enable reasoning about thread order.
|
|
9
9
|
* Tested on 1.8.7 - 2.2, JRuby, Rbx
|
10
10
|
* It has no external dependencies
|
11
11
|
* It does not depend on the stdlib.
|
12
|
+
|
13
|
+
Example
|
14
|
+
-------
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
# A somewhat contrived class we're going to test.
|
18
|
+
class MyQueue
|
19
|
+
attr_reader :array
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@array, @mutex = [], Mutex.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def enqueue
|
26
|
+
@mutex.synchronize { @array << yield }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
require 'rspec/autorun'
|
33
|
+
require 'thread_order'
|
34
|
+
|
35
|
+
RSpec.describe MyQueue do
|
36
|
+
let(:queue) { described_class.new }
|
37
|
+
let(:order) { ThreadOrder.new }
|
38
|
+
after { order.apocalypse! } # ensure everything gets cleaned up (technically redundant for our one example, but it's a good practice)
|
39
|
+
|
40
|
+
it 'is threadsafe on enqueue' do
|
41
|
+
# will execute in a thread, can be invoked by name
|
42
|
+
order.declare :concurrent_enqueue do
|
43
|
+
queue.enqueue { :concurrent }
|
44
|
+
end
|
45
|
+
|
46
|
+
# this enqueue will block until the mutex puts the other one to sleep
|
47
|
+
queue.enqueue do
|
48
|
+
order.pass_to :concurrent_enqueue, resume_on: :sleep
|
49
|
+
:main
|
50
|
+
end
|
51
|
+
|
52
|
+
order.join_all # concurrent_enqueue may still be asleep
|
53
|
+
expect(queue.array).to eq [:main, :concurrent]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# >> MyQueue
|
58
|
+
# >> is threadsafe on enqueue
|
59
|
+
# >>
|
60
|
+
# >> Finished in 0.00131 seconds (files took 0.08687 seconds to load)
|
61
|
+
# >> 1 example, 0 failures
|
62
|
+
```
|
data/lib/thread_order.rb
CHANGED
@@ -1,73 +1,102 @@
|
|
1
1
|
require 'thread_order/mutex'
|
2
2
|
|
3
3
|
class ThreadOrder
|
4
|
+
Error = Class.new RuntimeError
|
5
|
+
CannotResume = Class.new Error
|
6
|
+
|
7
|
+
# Note that this must tbe initialized in a threadsafe environment
|
8
|
+
# Otherwise, syncing may occur before the mutex is set
|
4
9
|
def initialize
|
10
|
+
@mutex = Mutex.new
|
5
11
|
@bodies = {}
|
6
12
|
@threads = []
|
7
|
-
@queue = [] #
|
8
|
-
@
|
9
|
-
|
10
|
-
|
13
|
+
@queue = [] # Queue is in stdlib, but half the purpose of this lib is to avoid such deps, so using an array in a Mutex
|
14
|
+
@worker = Thread.new do
|
15
|
+
Thread.current.abort_on_exception = true
|
16
|
+
Thread.current[:thread_order_name] = :internal_worker
|
17
|
+
loop { break if :shutdown == work() }
|
18
|
+
end
|
11
19
|
end
|
12
20
|
|
13
21
|
def declare(name, &block)
|
14
|
-
@bodies[name] = block
|
22
|
+
sync { @bodies[name] = block }
|
15
23
|
end
|
16
24
|
|
17
25
|
def current
|
18
26
|
Thread.current[:thread_order_name]
|
19
27
|
end
|
20
28
|
|
21
|
-
def pass_to(name, options)
|
22
|
-
parent = Thread.current
|
29
|
+
def pass_to(name, options={})
|
23
30
|
child = nil
|
24
|
-
|
25
|
-
|
26
|
-
return unless event == sync { resume_event }
|
27
|
-
parent.wakeup
|
28
|
-
end
|
29
|
-
|
31
|
+
parent = Thread.current
|
32
|
+
resume_event = extract_resume_event!(options)
|
30
33
|
enqueue do
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
34
|
+
sync do
|
35
|
+
@threads << Thread.new {
|
36
|
+
child = Thread.current
|
37
|
+
child[:thread_order_name] = name
|
38
|
+
body = sync { @bodies.fetch(name) }
|
39
|
+
wait_until { parent.stop? }
|
40
|
+
:run == resume_event && parent.wakeup
|
41
|
+
wake_on_sleep = lambda do
|
42
|
+
child.status == 'sleep' ? parent.wakeup :
|
43
|
+
child.status == nil ? :noop :
|
44
|
+
child.status == false ? parent.raise(CannotResume.new "#{name} exited instead of sleeping") :
|
45
|
+
enqueue(&wake_on_sleep)
|
46
|
+
end
|
47
|
+
:sleep == resume_event && enqueue(&wake_on_sleep)
|
48
|
+
begin
|
49
|
+
body.call parent
|
50
|
+
rescue Exception => e
|
51
|
+
enqueue { parent.raise e }
|
52
|
+
raise
|
53
|
+
ensure
|
54
|
+
:exit == resume_event && enqueue { parent.wakeup }
|
55
|
+
end
|
56
|
+
}
|
45
57
|
end
|
46
58
|
end
|
47
|
-
|
48
59
|
sleep
|
49
60
|
child
|
50
61
|
end
|
51
62
|
|
63
|
+
def join_all
|
64
|
+
sync { @threads }.each { |th| th.join }
|
65
|
+
end
|
66
|
+
|
52
67
|
def apocalypse!(thread_method=:kill)
|
53
68
|
enqueue do
|
54
69
|
@threads.each(&thread_method)
|
55
70
|
@queue.clear
|
56
|
-
|
71
|
+
:shutdown
|
57
72
|
end
|
58
73
|
@worker.join
|
59
74
|
end
|
60
75
|
|
76
|
+
def enqueue(&block)
|
77
|
+
sync { @queue << block if @worker.alive? }
|
78
|
+
end
|
79
|
+
|
80
|
+
def wait_until(&condition)
|
81
|
+
return if condition.call
|
82
|
+
thread = Thread.current
|
83
|
+
wake_when_true = lambda do
|
84
|
+
if thread.stop? && condition.call
|
85
|
+
thread.wakeup
|
86
|
+
else
|
87
|
+
enqueue(&wake_when_true)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
enqueue(&wake_when_true)
|
91
|
+
sleep
|
92
|
+
end
|
93
|
+
|
61
94
|
private
|
62
95
|
|
63
96
|
def sync(&block)
|
64
97
|
@mutex.synchronize(&block)
|
65
98
|
end
|
66
99
|
|
67
|
-
def enqueue(&block)
|
68
|
-
sync { @queue << block }
|
69
|
-
end
|
70
|
-
|
71
100
|
def work
|
72
101
|
task = sync { @queue.shift }
|
73
102
|
task ||= lambda { Thread.pass }
|
@@ -78,18 +107,8 @@ class ThreadOrder
|
|
78
107
|
resume_on = options.delete :resume_on
|
79
108
|
options.any? &&
|
80
109
|
raise(ArgumentError, "Unknown options: #{options.inspect}")
|
81
|
-
resume_on && ![:run, :exit, :sleep].include?(resume_on) and
|
110
|
+
resume_on && ![:run, :exit, :sleep, nil].include?(resume_on) and
|
82
111
|
raise(ArgumentError, "Unknown status: #{resume_on.inspect}")
|
83
|
-
resume_on
|
84
|
-
end
|
85
|
-
|
86
|
-
def watch_for_sleep(thread, &cb)
|
87
|
-
if thread.status == false || thread.status == nil
|
88
|
-
# noop, dead threads dream no dreams
|
89
|
-
elsif thread.status == 'sleep'
|
90
|
-
cb.call
|
91
|
-
else
|
92
|
-
enqueue { watch_for_sleep(thread, &cb) }
|
93
|
-
end
|
112
|
+
resume_on || :none
|
94
113
|
end
|
95
114
|
end
|
data/lib/thread_order/mutex.rb
CHANGED
@@ -1,21 +1,21 @@
|
|
1
|
-
# On > 1.9, this is in core.
|
2
|
-
# On 1.8.7, it's in the stdlib.
|
3
|
-
# We don't want to load the stdlib, b/c this is a test tool, and can affect the test environment,
|
4
|
-
# causing tests to pass where they should fail.
|
5
|
-
#
|
6
|
-
# So we're transcribing it here, from.
|
7
|
-
# It is based on this implementation: https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56
|
8
|
-
# If it's not already defined. Some methods we don't need are deleted.
|
9
|
-
# Anything I don't understand is left in.
|
10
1
|
class ThreadOrder
|
11
2
|
Mutex = if defined? ::Mutex
|
3
|
+
# On 1.9 and up, this is in core, so we just use the real one
|
12
4
|
::Mutex
|
13
5
|
else
|
6
|
+
|
7
|
+
# On 1.8.7, it's in the stdlib.
|
8
|
+
# We don't want to load the stdlib, b/c this is a test tool, and can affect the test environment,
|
9
|
+
# causing tests to pass where they should fail.
|
10
|
+
#
|
11
|
+
# So we're transcribing/modifying it from https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56
|
12
|
+
# Some methods we don't need are deleted.
|
13
|
+
# Anything I don't understand (there's quite a bit, actually) is left in.
|
14
14
|
Class.new do
|
15
15
|
def initialize
|
16
16
|
@waiting = []
|
17
17
|
@locked = false;
|
18
|
-
@waiting.taint
|
18
|
+
@waiting.taint
|
19
19
|
self.taint
|
20
20
|
end
|
21
21
|
|
data/lib/thread_order/version.rb
CHANGED
data/spec/run
CHANGED
@@ -34,25 +34,25 @@ get_gem "rspec-core" "https://rubygems.org/downloads/rspec-core-3.2.1.ge
|
|
34
34
|
get_gem "rspec-support" "https://rubygems.org/downloads/rspec-support-3.2.2.gem" &&
|
35
35
|
get_gem "rspec-expectations" "https://rubygems.org/downloads/rspec-expectations-3.2.0.gem" &&
|
36
36
|
get_gem "rspec-mocks" "https://rubygems.org/downloads/rspec-mocks-3.2.1.gem" &&
|
37
|
-
get_gem "diff-lcs" "https://rubygems.org/downloads/diff-lcs-1.2.5.gem"
|
37
|
+
get_gem "diff-lcs" "https://rubygems.org/downloads/diff-lcs-1.2.5.gem" || exit 1
|
38
38
|
|
39
39
|
|
40
40
|
# run specs
|
41
|
-
cd $project_root
|
41
|
+
cd "$project_root"
|
42
42
|
|
43
43
|
export PATH="$project_root/tmp/rspec-core/exe:$PATH"
|
44
44
|
|
45
|
-
opts=
|
46
|
-
opts
|
47
|
-
opts
|
48
|
-
opts
|
49
|
-
opts
|
50
|
-
opts
|
51
|
-
opts
|
45
|
+
opts=()
|
46
|
+
opts+=(-I "$project_root/tmp/diff-lcs/lib")
|
47
|
+
opts+=(-I "$project_root/tmp/rspec/lib")
|
48
|
+
opts+=(-I "$project_root/tmp/rspec-core/lib")
|
49
|
+
opts+=(-I "$project_root/tmp/rspec-expectations/lib")
|
50
|
+
opts+=(-I "$project_root/tmp/rspec-mocks/lib")
|
51
|
+
opts+=(-I "$project_root/tmp/rspec-support/lib")
|
52
52
|
|
53
53
|
if `ruby -e "exit RUBY_VERSION != '1.8.7'"`
|
54
54
|
then
|
55
|
-
opts
|
55
|
+
opts+=(--disable-gems)
|
56
56
|
fi
|
57
57
|
|
58
|
-
ruby $opts -S rspec
|
58
|
+
ruby "${opts[@]}" -S rspec --colour --fail-fast --format documentation
|
data/spec/thread_order_spec.rb
CHANGED
@@ -45,35 +45,75 @@ RSpec.describe ThreadOrder do
|
|
45
45
|
order.declare(:t) { Thread.exit }
|
46
46
|
order.pass_to :t, :resume_on => :exit
|
47
47
|
end
|
48
|
+
|
49
|
+
it 'passes the parent to the thread' do
|
50
|
+
parent = nil
|
51
|
+
order.declare(:t) { |p| parent = p }
|
52
|
+
order.pass_to :t, :resume_on => :exit
|
53
|
+
expect(parent).to eq Thread.current
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'sleeps until woken if it does not provide a :resume_on key' do
|
57
|
+
order.declare(:t) { |parent|
|
58
|
+
order.enqueue {
|
59
|
+
expect(parent.status).to eq 'sleep'
|
60
|
+
parent.wakeup
|
61
|
+
}
|
62
|
+
}
|
63
|
+
order.pass_to :t
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'blows up if it is waiting on another thread to sleep and that thread exits instead' do
|
67
|
+
expect {
|
68
|
+
order.declare(:t1) { :exits_instead_of_sleeping }
|
69
|
+
order.pass_to :t1, :resume_on => :sleep
|
70
|
+
}.to raise_error ThreadOrder::CannotResume, /t1 exited/
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'error types' do
|
75
|
+
it 'has a toplevel lib error: ThreadOrder::Error which is a RuntimeError' do
|
76
|
+
expect(ThreadOrder::Error.superclass).to eq RuntimeError
|
77
|
+
end
|
78
|
+
|
79
|
+
specify 'all behavioural errors it raises inherit from ThreadOrder::Error' do
|
80
|
+
expect(ThreadOrder::CannotResume.superclass).to eq ThreadOrder::Error
|
81
|
+
end
|
48
82
|
end
|
49
83
|
|
50
84
|
describe 'errors in children' do
|
51
85
|
specify 'are raised in the child' do
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
86
|
+
order.declare(:err) { sleep }
|
87
|
+
child = order.pass_to :err, :resume_on => :sleep
|
88
|
+
begin
|
89
|
+
child.raise RuntimeError.new('the roof')
|
90
|
+
sleep
|
91
|
+
rescue RuntimeError => e
|
92
|
+
expect(e.message).to eq 'the roof'
|
93
|
+
else
|
94
|
+
raise 'expected an error'
|
95
|
+
end
|
57
96
|
end
|
58
97
|
|
59
98
|
specify 'are raised in the parent' do
|
60
|
-
order.declare(:err) { raise Exception, 'to the rules' }
|
61
99
|
expect {
|
62
|
-
order.
|
63
|
-
|
100
|
+
order.declare(:err) { raise Exception, "to the rules" }
|
101
|
+
order.pass_to :err, :resume_on => :exit
|
102
|
+
sleep
|
64
103
|
}.to raise_error Exception, 'to the rules'
|
65
104
|
end
|
66
105
|
|
67
106
|
specify 'even if the parent is asleep' do
|
107
|
+
order.declare(:err) { sleep }
|
68
108
|
parent = Thread.current
|
69
|
-
order.
|
70
|
-
:noop until parent.status == 'sleep'
|
71
|
-
raise 'the roof'
|
72
|
-
}
|
109
|
+
child = order.pass_to :err, :resume_on => :sleep
|
73
110
|
expect {
|
74
|
-
order.
|
111
|
+
order.enqueue {
|
112
|
+
expect(parent.status).to eq 'sleep'
|
113
|
+
child.raise Exception.new 'to the rules'
|
114
|
+
}
|
75
115
|
sleep
|
76
|
-
}.to raise_error
|
116
|
+
}.to raise_error Exception, 'to the rules'
|
77
117
|
end
|
78
118
|
end
|
79
119
|
|
@@ -127,4 +167,87 @@ RSpec.describe ThreadOrder do
|
|
127
167
|
to raise_error(ArgumentError, /bad_key/)
|
128
168
|
end
|
129
169
|
end
|
170
|
+
|
171
|
+
describe 'join_all' do
|
172
|
+
it 'joins with all the child threads' do
|
173
|
+
parent = Thread.current
|
174
|
+
children = []
|
175
|
+
|
176
|
+
order.declare(:t1) do
|
177
|
+
order.pass_to :t2, :resume_on => :run
|
178
|
+
children << Thread.current
|
179
|
+
end
|
180
|
+
|
181
|
+
order.declare(:t2) do
|
182
|
+
children << Thread.current
|
183
|
+
end
|
184
|
+
|
185
|
+
order.pass_to :t1, :resume_on => :run
|
186
|
+
order.join_all
|
187
|
+
statuses = children.map { |th| th.status }
|
188
|
+
expect(statuses).to eq [false, false] # none are alive
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe 'synchronization' do
|
193
|
+
it 'allows any thread to enqueue work' do
|
194
|
+
seen = []
|
195
|
+
|
196
|
+
order.declare :enqueueing do |parent|
|
197
|
+
order.enqueue do
|
198
|
+
order.enqueue { seen << 2 }
|
199
|
+
order.enqueue { seen << 3 }
|
200
|
+
order.enqueue { parent.wakeup }
|
201
|
+
seen << 1
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
order.pass_to :enqueueing
|
206
|
+
expect(seen).to eq [1, 2, 3]
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'allows a thread to put itself to sleep until some condition is met' do
|
210
|
+
i = 0
|
211
|
+
increment = lambda do
|
212
|
+
i += 1
|
213
|
+
order.enqueue(&increment)
|
214
|
+
end
|
215
|
+
increment.call
|
216
|
+
order.wait_until { i > 20_000 } # 100k is too slow on 1.8.7, but 10k is too fast on 2.2.0
|
217
|
+
expect(i).to be > 20_000
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
describe 'apocalypse!' do
|
222
|
+
it 'kills threads that are still alive' do
|
223
|
+
order.declare(:t) { sleep }
|
224
|
+
child = order.pass_to :t, :resume_on => :sleep
|
225
|
+
expect(child).to receive(:kill).and_call_original
|
226
|
+
expect(child).to_not receive(:join)
|
227
|
+
order.apocalypse!
|
228
|
+
end
|
229
|
+
|
230
|
+
it 'can be overridden to call a different method than kill' do
|
231
|
+
# for some reason, the mock calling original join doesn't work
|
232
|
+
order.declare(:t) { sleep }
|
233
|
+
child = order.pass_to :t, :resume_on => :run
|
234
|
+
expect(child).to_not receive(:kill)
|
235
|
+
joiner = Thread.new { order.apocalypse! :join }
|
236
|
+
Thread.pass until child.status == 'sleep' # can't use wait_until b/c that occurs within the worker, which is apocalypsizing
|
237
|
+
child.wakeup
|
238
|
+
joiner.join
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'can call apocalypse! any number of times without harm' do
|
242
|
+
order.declare(:t) { sleep }
|
243
|
+
order.pass_to :t, :resume_on => :sleep
|
244
|
+
100.times { order.apocalypse! }
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'does not enqueue events after the apocalypse' do
|
248
|
+
order.apocalypse!
|
249
|
+
thread = Thread.current
|
250
|
+
order.enqueue { thread.raise "Should not happen" }
|
251
|
+
end
|
252
|
+
end
|
130
253
|
end
|
data/thread_order.gemspec
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
|
1
|
+
require File.expand_path('../lib/thread_order/version', __FILE__)
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = 'thread_order'
|
5
5
|
s.version = ThreadOrder::VERSION
|
6
6
|
s.licenses = ['MIT']
|
7
7
|
s.summary = "Test helper for ordering threaded code"
|
8
|
-
s.description = "Test helper for ordering threaded code (does not depend on gems or stdlib, tested on 1.8.7 - 2.2)."
|
8
|
+
s.description = "Test helper for ordering threaded code (does not depend on gems or stdlib, tested on 1.8.7 - 2.2, rbx, jruby)."
|
9
9
|
s.authors = ["Josh Cheek"]
|
10
10
|
s.email = 'josh.cheek@gmail.com'
|
11
11
|
s.files = `git ls-files`.split("\n")
|
metadata
CHANGED
@@ -1,38 +1,38 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: thread_order
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Cheek
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ~>
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '3.0'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ~>
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.0'
|
27
27
|
description: Test helper for ordering threaded code (does not depend on gems or stdlib,
|
28
|
-
tested on 1.8.7 - 2.2).
|
28
|
+
tested on 1.8.7 - 2.2, rbx, jruby).
|
29
29
|
email: josh.cheek@gmail.com
|
30
30
|
executables: []
|
31
31
|
extensions: []
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
|
-
-
|
35
|
-
-
|
34
|
+
- .gitignore
|
35
|
+
- .travis.yml
|
36
36
|
- Gemfile
|
37
37
|
- License.txt
|
38
38
|
- Readme.md
|
@@ -52,17 +52,17 @@ require_paths:
|
|
52
52
|
- lib
|
53
53
|
required_ruby_version: !ruby/object:Gem::Requirement
|
54
54
|
requirements:
|
55
|
-
- -
|
55
|
+
- - ! '>='
|
56
56
|
- !ruby/object:Gem::Version
|
57
57
|
version: '0'
|
58
58
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- -
|
60
|
+
- - ! '>='
|
61
61
|
- !ruby/object:Gem::Version
|
62
62
|
version: '0'
|
63
63
|
requirements: []
|
64
64
|
rubyforge_project:
|
65
|
-
rubygems_version: 2.4.
|
65
|
+
rubygems_version: 2.4.1
|
66
66
|
signing_key:
|
67
67
|
specification_version: 4
|
68
68
|
summary: Test helper for ordering threaded code
|