going 0.0.3 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d65446460582cc05ff1e4b2a7f2dd75fa8e2de02
4
- data.tar.gz: d3bef60cf07e6f7142194c3a524aa7451e8fc46a
3
+ metadata.gz: e654db8fb9029e5b32303e284551f9182960c418
4
+ data.tar.gz: 62605bb00f54efe7dc250850f1341f10991cecb5
5
5
  SHA512:
6
- metadata.gz: e7ada7d6cfc7e6bbf816e546f84c1c5b42f0901d4d8ac6740dd13219c8c73ebecb7d30eaba45f4a08ff4e194edd12f3856253cb267200983d55a649b1eeb510f
7
- data.tar.gz: 5b353932fcd9107fddd2edde8fbcf2b950dbd5cc798100ccb1b4fbd7e2f58acf9ba2b6e6179665082232b46f0b55bc12ea6ed1949badffc501c97926a92d9ef9
6
+ metadata.gz: ed12b04c91fd266ef44da9ef91b36f62b117d7cb869d3a8af21f831e6dfee4cc2c361aaa6e768d90f264e1cc2c97f3ae256a3445c19a3313d90966384fa16918
7
+ data.tar.gz: d8a8b37ecc0f747f322ca779e08d59e8683dd62c11f873862ba80d7211de40f9667acfef1310b4206dd20ed1fda8ec5cbf096ac59e4c7ce9ea0f96cce60876c8
data/.travis.yml CHANGED
@@ -3,7 +3,6 @@ rvm:
3
3
  - 2.1.2
4
4
  - 2.0.0
5
5
  - 1.9.3
6
- - jruby-18mode # JRuby in 1.8 mode
7
- - jruby-19mode # JRuby in 1.9 mode
8
- - rbx-2.2.7
9
- - rbx-2.2.6
6
+ # - jruby-19mode # JRuby in 1.9 mode
7
+ # - rbx-2.2.7
8
+ # - rbx-2.2.6
@@ -0,0 +1,11 @@
1
+ module Going
2
+ module BooleanAttrReader
3
+ def battr_reader(*attrs)
4
+ attrs.map(&:to_s).each do |attr|
5
+ define_method(attr + '?') do
6
+ !!instance_variable_get('@' + attr)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
data/lib/going/channel.rb CHANGED
@@ -1,24 +1,24 @@
1
- require 'going'
2
-
3
1
  module Going
4
2
  #
5
3
  # This class represents message channels of specified capacity.
6
4
  # The push operation may be blocked if the capacity is full.
7
- # The pop operation may be blocked if no messages have been sent.
5
+ # The shift operation may be blocked if no messages have been sent.
8
6
  #
9
7
  class Channel
10
- extend Forwardable
8
+ extend BooleanAttrReader
11
9
 
12
10
  #
13
11
  # Creates a fixed-length channel with a capacity of +capacity+.
14
12
  #
15
13
  def initialize(capacity = 0)
16
- fail ArgumentError, 'channel capacity must be 0 or greater' unless capacity >= 0
14
+ fail ArgumentError, 'channel capacity must be 0 or greater' if capacity < 0
17
15
  @capacity = capacity
16
+
17
+ @pushes = []
18
+ @shifts = []
19
+
18
20
  @closed = false
19
21
  @mutex = Mutex.new
20
- @push_semaphore = ConditionVariable.new
21
- @pop_semaphore = ConditionVariable.new
22
22
 
23
23
  yield self if block_given?
24
24
  end
@@ -31,9 +31,7 @@ module Going
31
31
  #
32
32
  # Returns whether or not the channel is closed.
33
33
  #
34
- def closed?
35
- @closed
36
- end
34
+ battr_reader :closed
37
35
 
38
36
  #
39
37
  # Closes the channel. Any data in the buffer may still be retrieved.
@@ -41,23 +39,33 @@ module Going
41
39
  def close
42
40
  synchronize do
43
41
  return false if closed?
44
- @messages = messages.first(capacity)
45
- broadcast_close
42
+
43
+ shifts.each(&:close).clear
44
+ pushes_over_capacity!.each(&:close)
46
45
  @closed = true
47
46
  end
48
47
  end
49
48
 
50
49
  #
51
50
  # Pushes +obj+ to the channel. If the channel is already full, waits
52
- # until a thread pops from it.
51
+ # until a thread shifts from it.
53
52
  #
54
- def push(obj)
53
+ def push(obj, &on_complete)
55
54
  synchronize do
56
- fail 'cannot push to a closed channel' if closed?
57
- messages.push obj
58
- signal_push
59
- wait_for_pop if messages.length > capacity
60
- check_for_close
55
+ push = Push.new(message: obj, select_statement: select_statement, &on_complete)
56
+ pushes << push
57
+
58
+ pair_with_shift push
59
+
60
+ select_statement.when_complete(push, pushes, &method(:remove_operation)) if select_statement?
61
+
62
+ push.complete if under_capacity?
63
+ push.signal if select_statement?
64
+ push.close if closed?
65
+
66
+ push.wait(mutex)
67
+
68
+ fail 'cannot push to a closed channel' if closed? && !select_statement?
61
69
  self
62
70
  end
63
71
  end
@@ -72,34 +80,52 @@ module Going
72
80
  # Receives data from the channel. If the channel is already empty,
73
81
  # waits until a thread pushes to it.
74
82
  #
75
- def pop
83
+ def shift(&on_complete)
76
84
  synchronize do
77
- return if closed?
78
- wait_for_push if messages.empty?
79
- signal_pop
80
- check_for_close
81
- messages.shift
85
+ shift = Shift.new(select_statement: select_statement, &on_complete)
86
+ shifts << shift
87
+
88
+ pair_with_push shift
89
+
90
+ select_statement.when_complete(shift, shifts, &method(:remove_operation)) if select_statement?
91
+
92
+ shift.signal if select_statement?
93
+ shift.close if closed?
94
+
95
+ shift.wait(mutex)
96
+
97
+ throw :close if closed? && !select_statement? && shift.incomplete?
98
+ shift.message
82
99
  end
83
100
  end
84
101
 
85
102
  #
86
- # Alias of pop
103
+ # Alias of shift
87
104
  #
88
- alias_method :receive, :pop
89
- alias_method :next, :pop
105
+ alias_method :receive, :shift
106
+ alias_method :next, :shift
90
107
 
91
108
  #
92
- # Delegate size, length, and empty? to the messages queue
109
+ # Returns the number of messages in the channel
93
110
  #
94
- def_delegators :messages, :size, :empty?
111
+ def size
112
+ [capacity, pushes.size].min
113
+ end
95
114
 
96
115
  #
97
116
  # Alias of size
98
117
  #
99
118
  alias_method :length, :size
100
119
 
120
+ #
121
+ # Returns whether the channel is empty.
122
+ #
123
+ def empty?
124
+ size == 0
125
+ end
126
+
101
127
  def inspect
102
- inspection = [:capacity, :messages].map do |attr|
128
+ inspection = [:capacity, :size].map do |attr|
103
129
  "#{attr}: #{send(attr).inspect}"
104
130
  end
105
131
  "#<#{self.class} #{inspection.join(', ')}>"
@@ -107,35 +133,59 @@ module Going
107
133
 
108
134
  private
109
135
 
110
- def_delegators :@mutex, :synchronize
136
+ attr_reader :mutex, :pushes, :shifts
137
+
138
+ def synchronize(&blk)
139
+ mutex.synchronize(&blk)
140
+ end
111
141
 
112
- def messages
113
- @messages ||= []
142
+ def pair_with_push(shift)
143
+ pushes.each_with_index.any? do |push, index|
144
+ if push.select_statement != select_statement && shift.complete(push)
145
+ complete_next_push_now_that_channel_under_capacity
146
+ shifts.pop
147
+ pushes.delete_at index
148
+ true
149
+ end
150
+ end
114
151
  end
115
152
 
116
- def signal_pop
117
- @push_semaphore.signal
153
+ def pair_with_shift(push)
154
+ shifts.each_with_index.any? do |shift, index|
155
+ if shift.select_statement != select_statement && shift.complete(push)
156
+ pushes.pop
157
+ shifts.delete_at index
158
+ true
159
+ end
160
+ end
161
+ end
162
+
163
+ def remove_operation(operation, queue)
164
+ synchronize do
165
+ index = queue.index(operation)
166
+ queue.delete_at index if index
167
+ end
118
168
  end
119
169
 
120
- def wait_for_pop
121
- @push_semaphore.wait(@mutex)
170
+ def complete_next_push_now_that_channel_under_capacity
171
+ push = pushes[capacity]
172
+ push.complete if push && push.incomplete?
122
173
  end
123
174
 
124
- def signal_push
125
- @pop_semaphore.signal
175
+ def pushes_over_capacity!
176
+ pushes.slice!(capacity, pushes.size) || []
126
177
  end
127
178
 
128
- def wait_for_push
129
- @pop_semaphore.wait(@mutex)
179
+ def under_capacity?
180
+ pushes.size <= capacity
130
181
  end
131
182
 
132
- def broadcast_close
133
- @push_semaphore.broadcast
134
- @pop_semaphore.broadcast
183
+ def select_statement
184
+ SelectStatement.instance || NilSelectStatement.instance
135
185
  end
136
186
 
137
- def check_for_close
138
- throw :close if closed?
187
+ def select_statement?
188
+ SelectStatement.instance?
139
189
  end
140
190
  end
141
191
  end
@@ -0,0 +1,27 @@
1
+ module Going
2
+ class NilSelectStatement
3
+ include Singleton
4
+
5
+ def !=(other)
6
+ true
7
+ end
8
+
9
+ def !
10
+ true
11
+ end
12
+
13
+ def nil?
14
+ true
15
+ end
16
+
17
+ def once
18
+ yield
19
+ end
20
+
21
+ def complete(*args)
22
+ end
23
+
24
+ def secondary_complete(*args)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module Going
2
+ class Push < Operation
3
+ def complete
4
+ super
5
+ select_statement.complete(&on_complete)
6
+ end
7
+
8
+ def close
9
+ super
10
+ select_statement.secondary_complete do
11
+ fail 'cannot push to a closed channel'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ module Going
2
+ class Shift < Operation
3
+ def complete(push)
4
+ select_statement.once do
5
+ push.select_statement.once do
6
+ self.message = push.message
7
+
8
+ super()
9
+ push.complete
10
+ notify_select_statement
11
+ true
12
+ end
13
+ end
14
+ end
15
+
16
+ def close
17
+ super
18
+ notify_select_statement
19
+ end
20
+
21
+ private
22
+
23
+ def notify_select_statement
24
+ select_statement.complete(message, ok: ok?, &on_complete)
25
+ end
26
+
27
+ def ok?
28
+ !closed?
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ module Going
2
+ class Operation
3
+ extend BooleanAttrReader
4
+
5
+ attr_accessor :message
6
+ attr_reader :select_statement
7
+
8
+ def initialize(opts = {}, &on_complete)
9
+ @message = opts[:message]
10
+ @select_statement = opts[:select_statement]
11
+ @on_complete = on_complete
12
+
13
+ @completed = false
14
+ @closed = false
15
+ @signaled = false
16
+
17
+ @semaphore = ConditionVariable.new
18
+ end
19
+
20
+ def wait(mutex)
21
+ semaphore.wait(mutex) until wake?
22
+ end
23
+
24
+ def signal
25
+ @signaled = true
26
+ semaphore.signal
27
+ end
28
+
29
+ def complete
30
+ @completed = true
31
+ signal
32
+ end
33
+
34
+ def close
35
+ @closed = true
36
+ signal
37
+ end
38
+
39
+ def incomplete?
40
+ !completed?
41
+ end
42
+
43
+ private
44
+
45
+ def wake?
46
+ signaled? || completed? || closed?
47
+ end
48
+
49
+ attr_reader :semaphore, :on_complete
50
+ battr_reader :signaled, :completed, :closed, :select_statement
51
+ end
52
+ end
@@ -0,0 +1,30 @@
1
+ module Going
2
+ #
3
+ # Helper methods to emulate Go's Select Cases.
4
+ #
5
+ class SelectHelper
6
+ include Singleton
7
+
8
+ #
9
+ # A case statement that will succeed immediately.
10
+ #
11
+ def default(&blk)
12
+ Channel.new(1) do |ch|
13
+ ch.push(nil, &blk)
14
+ end
15
+ end
16
+
17
+ #
18
+ # A case statement that will succeed after +seconds+ seconds.
19
+ #
20
+ def timeout(seconds, &blk)
21
+ Channel.new do |ch|
22
+ Going.go do
23
+ sleep seconds
24
+ ch.receive
25
+ end
26
+ ch.push(nil, &blk)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,121 @@
1
+ module Going
2
+
3
+ class SelectStatement
4
+ extend BooleanAttrReader
5
+
6
+ class << self
7
+ def instance
8
+ Thread.current[global_key]
9
+ end
10
+
11
+ def instance?
12
+ !instance.nil?
13
+ end
14
+
15
+ def new_instance
16
+ self.instance = new
17
+ end
18
+
19
+ def reset
20
+ self.instance = nil
21
+ end
22
+
23
+ private
24
+
25
+ def instance=(select_statement)
26
+ Thread.current[global_key] = select_statement
27
+ end
28
+
29
+ def global_key
30
+ @global_key ||= "Going_#{Going::SelectStatement.object_id}"
31
+ end
32
+ end
33
+
34
+ def initialize
35
+ @completed = false
36
+ @once_mutex = Mutex.new
37
+ @complete_mutex = Mutex.new
38
+ @semaphore = ConditionVariable.new
39
+ @when_completes = []
40
+
41
+ @args = nil
42
+ @on_complete = nil
43
+ end
44
+
45
+ def select(&blk)
46
+ select_helper = SelectHelper.instance
47
+ if blk.arity == 1
48
+ yield select_helper
49
+ else
50
+ select_helper.instance_eval(&blk)
51
+ end
52
+
53
+ wait
54
+ cleanup
55
+ call_completion_block
56
+ end
57
+
58
+ def when_complete(*args, &callback)
59
+ when_completes << proc { callback.call(*args) }
60
+ end
61
+
62
+ def complete(*args, &on_complete)
63
+ complete_mutex.synchronize do
64
+ if !completed?
65
+ @args = args
66
+ @on_complete = on_complete
67
+ @completed = true
68
+ @secondary_completed = true
69
+ semaphore.signal
70
+ end
71
+ end
72
+ end
73
+
74
+ def secondary_complete(*args, &on_complete)
75
+ complete_mutex.synchronize do
76
+ if !secondary_completed?
77
+ @args = args
78
+ @on_complete = on_complete
79
+ @secondary_completed = true
80
+ semaphore.signal
81
+ end
82
+ end
83
+ end
84
+
85
+ def once(*args, &blk)
86
+ once_mutex.synchronize do
87
+ yield(*args) if block_given? && incomplete?
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :semaphore, :once_mutex, :complete_mutex, :when_completes
94
+ attr_reader :on_complete, :args
95
+ battr_reader :completed, :secondary_completed
96
+
97
+ def wait
98
+ complete_mutex.synchronize do
99
+ semaphore.wait(complete_mutex) until wake?
100
+ end
101
+ end
102
+
103
+ def incomplete?
104
+ complete_mutex.synchronize do
105
+ !completed?
106
+ end
107
+ end
108
+
109
+ def wake?
110
+ completed? || secondary_completed?
111
+ end
112
+
113
+ def cleanup
114
+ when_completes.each(&:call)
115
+ end
116
+
117
+ def call_completion_block
118
+ on_complete.call(*args) if on_complete
119
+ end
120
+ end
121
+ end
data/lib/going/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Going
2
- VERSION = '0.0.3'
2
+ VERSION = '1.0.0'
3
3
  end
data/lib/going.rb CHANGED
@@ -1,5 +1,14 @@
1
1
  require 'thread'
2
+ require 'singleton'
3
+
4
+ require 'going/boolean_attr_reader'
2
5
  require 'going/channel'
6
+ require 'going/select_statement'
7
+ require 'going/nil_select_statement'
8
+ require 'going/select_helper'
9
+ require 'going/operation'
10
+ require 'going/operation/shift'
11
+ require 'going/operation/push'
3
12
  require 'going/version'
4
13
 
5
14
  module Going
@@ -9,4 +18,17 @@ module Going
9
18
  def self.go(*args, &blk)
10
19
  Thread.new(*args, &blk)
11
20
  end
21
+
22
+ #
23
+ # Creates a synchronous block that will select the first
24
+ # channel operation to complete. Only one operation inside
25
+ # the block will complete and any operations that are
26
+ # incomplete will be removed afterwards.
27
+ #
28
+ def self.select(&blk)
29
+ fail 'a block must be passed' unless block_given?
30
+ select = SelectStatement.new_instance
31
+ select.select(&blk)
32
+ SelectStatement.reset
33
+ end
12
34
  end
@@ -1,9 +1,22 @@
1
+ require 'going'
2
+
1
3
  describe Going::Channel do
2
4
  subject(:channel) { Going::Channel.new }
5
+ let(:buffered_channel) { Going::Channel.new 1 }
6
+
3
7
  def elapsed_time(original_time)
4
8
  (Time.now - original_time)
5
9
  end
6
10
 
11
+ def sleeper(channel, queue, size)
12
+ fail "channel does not respond to #{queue}" unless channel.respond_to? queue, true
13
+ begin
14
+ Thread.pass
15
+ sleep 0.1
16
+ end until channel.send(queue).size == size
17
+ expect(channel.send(queue).size).to eq(size)
18
+ end
19
+
7
20
  describe '.new' do
8
21
  it 'defaults capacity to 0' do
9
22
  expect(channel.capacity).to eq(0)
@@ -48,36 +61,35 @@ describe Going::Channel do
48
61
 
49
62
  it 'will wake a blocked push' do
50
63
  Going.go do
51
- sleep 0.1
64
+ sleeper channel, :pushes, 1
52
65
  channel.close
53
66
  end
54
- expect { channel.push 1 }.to throw_symbol(:close)
67
+ expect { channel.push 1 }.to raise_error
55
68
  end
56
69
 
57
- it 'will wake a blocked push' do
70
+ it 'will wake a blocked shift' do
58
71
  Going.go do
59
- sleep 0.1
72
+ sleeper channel, :shifts, 1
60
73
  channel.close
61
74
  end
62
75
  expect { channel.receive }.to throw_symbol(:close)
63
76
  end
64
77
 
65
78
  it 'will reject all but the first #capacity pushes' do
66
- channel = Going::Channel.new 2
67
- Going.go do
68
- sleep 0.1
69
- channel.close
70
- end
71
- catch :close do
79
+ begin
80
+ channel = Going::Channel.new 2
81
+ Going.go do
82
+ sleeper channel, :pushes, 3
83
+ channel.close
84
+ end
72
85
  3.times { |i| channel.push i }
86
+ rescue
87
+ expect(channel.size).to eq(2)
73
88
  end
74
- expect(channel.size).to eq(2)
75
89
  end
76
90
  end
77
91
 
78
92
  describe '#push' do
79
- subject(:channel) { Going::Channel.new 1 }
80
-
81
93
  it 'is aliased as #<<' do
82
94
  expect(channel.method(:<<)).to eq(channel.method(:push))
83
95
  end
@@ -93,13 +105,13 @@ describe Going::Channel do
93
105
 
94
106
  it 'will not block if channel is under capacity' do
95
107
  now = Time.now
96
- channel.push 1
108
+ buffered_channel.push 1
97
109
  expect(elapsed_time(now)).to be < 0.2
98
110
  end
99
111
 
100
112
  it 'will block if channel is over capacity' do
101
- channel.push 1
102
113
  Going.go do
114
+ sleeper channel, :pushes, 1
103
115
  sleep 0.25
104
116
  channel.receive
105
117
  end
@@ -109,50 +121,73 @@ describe Going::Channel do
109
121
  end
110
122
 
111
123
  it 'will push messages in order' do
112
- channel.push 1
124
+ buffered_channel.push 1
113
125
  Going.go do
114
- channel.push 2
126
+ sleeper buffered_channel, :pushes, 1
127
+ buffered_channel.push 2
115
128
  end
116
129
  Going.go do
117
- sleep 0.05
118
- channel.push 3
130
+ sleeper buffered_channel, :pushes, 2
131
+ buffered_channel.push 3
119
132
  end
120
- sleep 0.1
133
+
134
+ sleeper buffered_channel, :pushes, 3
121
135
  1.upto(3).each do |i|
122
- expect(channel.receive).to eq(i)
136
+ expect(buffered_channel.receive).to eq(i)
123
137
  end
124
138
  end
125
139
 
126
140
  it 'returns the channel' do
127
- expect(channel.push(1)).to be(channel)
141
+ expect(buffered_channel.push(1)).to be(buffered_channel)
128
142
  end
129
- end
130
143
 
131
- describe '#pop' do
132
- subject(:channel) { Going::Channel.new 1 }
144
+ context 'when a shift is from a select_statement' do
145
+ context 'when select_statement is already completed' do
146
+ it 'attempts to complete with next shift' do
147
+ i = nil
148
+ Going.select do |s|
149
+ channel.receive
150
+ s.default
151
+ th = Going.go do
152
+ i = channel.receive
153
+ end
154
+ Going.go do
155
+ sleeper channel, :shifts, 2
156
+ channel.push 1
157
+ th.join
158
+ end.join
159
+ end
160
+
161
+ expect(i).to be(1)
162
+ end
163
+ end
164
+ end
165
+ end
133
166
 
167
+ describe '#shift' do
134
168
  it 'is aliased as #receive' do
135
- expect(channel.method(:receive)).to eq(channel.method(:pop))
169
+ expect(channel.method(:receive)).to eq(channel.method(:shift))
136
170
  end
137
171
 
138
172
  it 'is aliased as #next' do
139
- expect(channel.method(:next)).to eq(channel.method(:pop))
173
+ expect(channel.method(:next)).to eq(channel.method(:shift))
140
174
  end
141
175
 
142
176
  it 'returns the next message' do
143
- channel.push 1
144
- expect(channel.receive).to eq(1)
177
+ buffered_channel.push 1
178
+ expect(buffered_channel.receive).to eq(1)
145
179
  end
146
180
 
147
181
  it 'will not block if channel is not empty' do
148
- channel.push 1
182
+ buffered_channel.push 1
149
183
  now = Time.now
150
- channel.receive
184
+ buffered_channel.receive
151
185
  expect(elapsed_time(now)).to be < 0.2
152
186
  end
153
187
 
154
188
  it 'will block if channel is empty' do
155
189
  Going.go do
190
+ sleeper channel, :shifts, 1
156
191
  sleep 0.25
157
192
  channel.push 1
158
193
  end
@@ -161,16 +196,39 @@ describe Going::Channel do
161
196
  expect(elapsed_time(now)).to be > 0.2
162
197
  end
163
198
 
164
- it 'returns nil if closed' do
165
- channel.close
166
- expect(channel.receive).to be_nil
199
+ context 'when closed' do
200
+ it 'returns next message if any' do
201
+ buffered_channel.push 1
202
+ buffered_channel.close
203
+ expect(buffered_channel.receive).to eq(1)
204
+ end
205
+
206
+ it 'throws :close if no messages' do
207
+ channel.close
208
+ expect { channel.receive }.to throw_symbol(:close)
209
+ end
167
210
  end
168
211
 
169
- it 'does not block if closed' do
170
- channel.close
171
- now = Time.now
172
- channel.receive
173
- expect(elapsed_time(now)).to be < 0.2
212
+ context 'when a push is from a select_statement' do
213
+ context 'when select_statement is already completed' do
214
+ it 'attempts to complete with next push' do
215
+ i = nil
216
+
217
+ Going.select do |s|
218
+ channel.push 1
219
+ s.default
220
+ Going.go do
221
+ channel.push 2
222
+ end
223
+ Going.go do
224
+ sleeper channel, :pushes, 2
225
+ i = channel.receive
226
+ end.join
227
+ end
228
+
229
+ expect(i).to be(2)
230
+ end
231
+ end
174
232
  end
175
233
  end
176
234
 
@@ -191,23 +249,39 @@ describe Going::Channel do
191
249
  channel.receive
192
250
  expect(channel.size).to eq(0)
193
251
  end
252
+
253
+ it 'returns 0 for unbuffered channel' do
254
+ Going.go do
255
+ channel.push 1
256
+ end
257
+ sleeper channel, :pushes, 1
258
+ expect(channel.size).to eq(0)
259
+ end
194
260
  end
195
261
 
196
262
  describe '#empty?' do
197
- it 'returns true when no messages in channel' do
198
- expect(channel).to be_empty
263
+ context 'when capacity is 0' do
264
+ it 'returns true when no messages in channel' do
265
+ expect(channel).to be_empty
266
+ end
267
+
268
+ it 'returns true even if blocked pushes' do
269
+ Going.go do
270
+ channel.push 1
271
+ end
272
+ sleeper channel, :pushes, 1
273
+ expect(channel).to be_empty
274
+ end
199
275
  end
200
276
 
201
277
  context 'when capacity is greater than 0' do
202
- subject(:channel) { Going::Channel.new 1 }
203
-
204
- it 'returns true when messages in channel' do
205
- expect(channel).to be_empty
278
+ it 'returns true when no messages in channel' do
279
+ expect(buffered_channel).to be_empty
206
280
  end
207
281
 
208
282
  it 'returns false when messages in channel' do
209
- channel.push 1
210
- expect(channel).not_to be_empty
283
+ buffered_channel.push 1
284
+ expect(buffered_channel).not_to be_empty
211
285
  end
212
286
  end
213
287
  end
@@ -0,0 +1,364 @@
1
+ require 'going'
2
+
3
+ describe Going::SelectStatement do
4
+ let(:channel) { Going::Channel.new }
5
+ let(:buffered_channel) { Going::Channel.new 1 }
6
+ let(:spy) { Spy.new }
7
+ let(:dont_call) { Spy.new }
8
+
9
+ def elapsed_time(original_time)
10
+ (Time.now - original_time)
11
+ end
12
+
13
+ def sleeper(channel, queue, size)
14
+ fail "channel does not respond to #{queue}" unless channel.respond_to? queue, true
15
+ begin
16
+ Thread.pass
17
+ sleep 0.1
18
+ end until channel.send(queue).size == size
19
+ expect(channel.send(queue).size).to eq(size)
20
+ end
21
+
22
+ describe 'Going.select' do
23
+ it 'blocks until a channel operation succeeds' do
24
+ now = Time.now
25
+ Going.select do |s|
26
+ channel.receive
27
+
28
+ Going.go do
29
+ sleep 0.25
30
+ channel.push 1
31
+ end
32
+ end
33
+ expect(elapsed_time(now)).to be > 0.2
34
+ end
35
+
36
+ it 'does not block if a channel operation immediately succeeds' do
37
+ now = Time.now
38
+ Going.select do |s|
39
+ buffered_channel.push 1
40
+ end
41
+ expect(elapsed_time(now)).to be < 0.2
42
+ end
43
+
44
+ it 'calls block on operation that succeeds' do
45
+ third_channel = Going::Channel.new 1
46
+ Going.select do |s|
47
+ channel.receive(&dont_call)
48
+ buffered_channel.push(1, &spy)
49
+ third_channel.push(2, &dont_call)
50
+ end
51
+ expect(spy).to be_called
52
+ expect(dont_call).not_to be_called
53
+ end
54
+
55
+ it 'does not call other blocks' do
56
+ Going.select do |s|
57
+ channel.push(1, &dont_call)
58
+ buffered_channel.push 2
59
+ Going.go do
60
+ channel.receive
61
+ end
62
+ end
63
+ sleeper channel, :shifts, 1
64
+ expect(dont_call).not_to be_called
65
+ end
66
+
67
+ it 'can not succeed from own operations' do
68
+ now = Time.now
69
+ Going.select do |s|
70
+ channel.push(1, &dont_call)
71
+ channel.receive(&spy)
72
+ channel.push(2, &dont_call)
73
+ Going.go do
74
+ sleep 0.25
75
+ channel.push(3)
76
+ end
77
+ end
78
+ expect(elapsed_time(now)).to be > 0.2
79
+ expect(spy.args.first).to eq(3)
80
+ expect(dont_call).not_to be_called
81
+ end
82
+
83
+ it 'does not complete other operations if already succeeded' do
84
+ Going.select do |s|
85
+ buffered_channel.receive(&dont_call)
86
+ s.default
87
+ Going.go do
88
+ buffered_channel.push 1
89
+ end.join
90
+ end
91
+ expect(dont_call).not_to be_called
92
+ end
93
+
94
+ it 'does not preserve channel operations that are not selected' do
95
+ Going.select do |s|
96
+ buffered_channel.push 1
97
+ buffered_channel.push 2
98
+ Going.go do
99
+ buffered_channel.receive
100
+ end
101
+ end
102
+ expect(buffered_channel.size).to eq(0)
103
+ end
104
+
105
+ context 'buffered channels' do
106
+ it 'succeeds when blocked push is now under capacity' do
107
+ buffered_channel.push 1
108
+ Going.select do |s|
109
+ buffered_channel.push(2, &spy)
110
+ Going.go do
111
+ buffered_channel.receive
112
+ end
113
+ end
114
+ expect(spy).to be_called
115
+ end
116
+ end
117
+ end
118
+
119
+ describe 'succeeding push' do
120
+ it 'passes nothing to the block' do
121
+ Going.select do |s|
122
+ buffered_channel.push(1, &spy)
123
+ end
124
+
125
+ expect(spy.args).to eq([])
126
+ end
127
+ end
128
+
129
+ describe 'succeeding receive' do
130
+ it 'passes message as arg to the block' do
131
+ buffered_channel.push 1
132
+ Going.select do |s|
133
+ buffered_channel.receive(&spy)
134
+ end
135
+
136
+ expect(spy.args.first).to eq(1)
137
+ end
138
+
139
+ it "passes true as `ok` param to the block" do
140
+ buffered_channel.push 1
141
+ Going.select do |s|
142
+ buffered_channel.receive(&spy)
143
+ end
144
+
145
+ expect(spy.args.last).to eq({ ok: true })
146
+ end
147
+ end
148
+
149
+ describe 'closed channel' do
150
+ before(:each) { channel.close }
151
+
152
+ describe 'push' do
153
+ context 'when select fails' do
154
+ it 'raises error' do
155
+ expect do
156
+ Going.select do |s|
157
+ channel.push 1
158
+ end
159
+ end.to raise_error
160
+ end
161
+
162
+ it 'does not call block' do
163
+ begin
164
+ Going.select do |s|
165
+ channel.push(1, &dont_call)
166
+ end
167
+ rescue
168
+ expect(dont_call).not_to be_called
169
+ end
170
+ end
171
+ end
172
+
173
+ context 'when select succeeds' do
174
+ it 'does not raise error' do
175
+ expect do
176
+ Going.select do |s|
177
+ channel.push 1
178
+ s.default
179
+ end
180
+ end.not_to raise_error
181
+ end
182
+
183
+ it 'does not call block' do
184
+ Going.select do |s|
185
+ channel.push(1, &dont_call)
186
+ s.default
187
+ end
188
+ expect(dont_call).not_to be_called
189
+ end
190
+ end
191
+ end
192
+
193
+ describe 'receive' do
194
+ it 'can receive buffered messages' do
195
+ buffered_channel.push 1
196
+ buffered_channel.close
197
+
198
+ Going.select do |s|
199
+ buffered_channel.receive(&spy)
200
+ end
201
+ expect(spy.args.first).to eq(1)
202
+ end
203
+
204
+ it 'is considered a success' do
205
+ Going.select do |s|
206
+ channel.receive(&spy)
207
+ end
208
+ expect(spy).to be_called
209
+ end
210
+
211
+ it 'passes nil as arg to the block' do
212
+ Going.select do |s|
213
+ channel.receive(&spy)
214
+ end
215
+ expect(spy.args.first).to be_nil
216
+ end
217
+
218
+ it "passes false as `ok` param to the block" do
219
+ Going.select do |s|
220
+ channel.receive(&spy)
221
+ end
222
+ expect(spy.args.last).to eq({ ok: false })
223
+ end
224
+ end
225
+ end
226
+
227
+ describe 'closing channel' do
228
+ describe 'push' do
229
+ context 'when select fails' do
230
+ it 'blocked push raises error' do
231
+ expect do
232
+ Going.select do |s|
233
+ channel.push 1
234
+ channel.close
235
+ end
236
+ end.to raise_error
237
+ end
238
+
239
+ it 'does not call block' do
240
+ begin
241
+ Going.select do |s|
242
+ channel.push(1, &dont_call)
243
+ channel.close
244
+ end
245
+ rescue
246
+ expect(dont_call).not_to be_called
247
+ end
248
+ end
249
+ end
250
+
251
+ context 'when select succeeds' do
252
+ it 'does not raise error if select succeeds' do
253
+ Going.select do |s|
254
+ channel.push 1
255
+ channel.receive
256
+ channel.close
257
+ end
258
+ end
259
+
260
+ it 'does not call block' do
261
+ Going.select do |s|
262
+ channel.push(1, &dont_call)
263
+ channel.receive
264
+ channel.close
265
+ end
266
+ expect(dont_call).not_to be_called
267
+ end
268
+ end
269
+ end
270
+
271
+ describe 'receive' do
272
+ it 'is considered a success' do
273
+ Going.select do |s|
274
+ channel.receive(&spy)
275
+ channel.close
276
+ end
277
+ expect(spy).to be_called
278
+ end
279
+
280
+ it 'passes nil as arg to the block' do
281
+ Going.select do |s|
282
+ channel.receive(&spy)
283
+ channel.close
284
+ end
285
+ expect(spy.args.first).to be_nil
286
+ end
287
+
288
+ it "passes false as `ok` param to the block" do
289
+ Going.select do |s|
290
+ channel.receive(&spy)
291
+ channel.close
292
+ end
293
+ expect(spy.args.last).to eq({ ok: false })
294
+ end
295
+ end
296
+ end
297
+
298
+
299
+ describe '#default' do
300
+ it 'never blocks' do
301
+ now = Time.now
302
+ Going.select do |s|
303
+ s.default
304
+ end
305
+ expect(elapsed_time(now)).to be < 0.2
306
+ end
307
+
308
+ it 'is not called if select statement is already succeeded' do
309
+ Going.select do |s|
310
+ buffered_channel.push 1
311
+ s.default(&dont_call)
312
+ end
313
+ expect(dont_call).not_to be_called
314
+ end
315
+
316
+ it 'calls its block on success' do
317
+ Going.select do |s|
318
+ s.default(&spy)
319
+ end
320
+ expect(spy).to be_called
321
+ end
322
+
323
+ it 'passes nothing to the block' do
324
+ Going.select do |s|
325
+ s.default(&spy)
326
+ end
327
+ expect(spy.args).to eq([])
328
+ end
329
+ end
330
+
331
+ describe '#timeout' do
332
+ it 'completes the select statement after given time' do
333
+ now = Time.now
334
+ Going.select do |s|
335
+ s.timeout(0.25)
336
+ end
337
+ expect(elapsed_time(now)).to be > 0.2
338
+ end
339
+
340
+ it 'is not called if select statement is already completed' do
341
+ ch = nil
342
+ Going.select do |s|
343
+ ch = s.timeout(0.1, &dont_call)
344
+ s.default
345
+ end
346
+ sleeper ch, :pushes, 0
347
+ expect(dont_call).not_to be_called
348
+ end
349
+
350
+ it 'calls its block on success' do
351
+ Going.select do |s|
352
+ s.timeout(0, &spy)
353
+ end
354
+ expect(spy).to be_called
355
+ end
356
+
357
+ it 'passes nothing to the block' do
358
+ Going.select do |s|
359
+ s.timeout(0, &spy)
360
+ end
361
+ expect(spy.args).to eq([])
362
+ end
363
+ end
364
+ end
data/spec/spec_helper.rb CHANGED
@@ -16,6 +16,11 @@
16
16
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
17
17
 
18
18
  require 'going'
19
+ require 'timeout'
20
+
21
+ Thread.abort_on_exception = true
22
+
23
+ Dir.glob(File.join(File.dirname(File.absolute_path(__FILE__)), 'support', '*.rb')) { |file| require file}
19
24
 
20
25
  RSpec.configure do |config|
21
26
  config.filter_run :focus
@@ -37,4 +42,10 @@ RSpec.configure do |config|
37
42
  mocks.syntax = :expect
38
43
  mocks.verify_partial_doubles = true
39
44
  end
45
+
46
+ config.around(:each) do |example|
47
+ Timeout::timeout(5) do
48
+ example.run
49
+ end
50
+ end
40
51
  end
@@ -0,0 +1,22 @@
1
+ class Spy
2
+ attr_reader :args
3
+
4
+ def initialize
5
+ @called = false
6
+ @args = nil
7
+ end
8
+
9
+ def call(*args)
10
+ @called = true
11
+ @args = args
12
+ end
13
+
14
+ def called?
15
+ !!@called
16
+ end
17
+
18
+ def to_proc
19
+ method(:call).to_proc
20
+ end
21
+ end
22
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: going
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Ridgewell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-22 00:00:00.000000000 Z
11
+ date: 2014-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -68,13 +68,22 @@ files:
68
68
  - Rakefile
69
69
  - going.gemspec
70
70
  - lib/going.rb
71
+ - lib/going/boolean_attr_reader.rb
71
72
  - lib/going/channel.rb
72
73
  - lib/going/kernel.rb
74
+ - lib/going/nil_select_statement.rb
75
+ - lib/going/operation.rb
76
+ - lib/going/operation/push.rb
77
+ - lib/going/operation/shift.rb
78
+ - lib/going/select_helper.rb
79
+ - lib/going/select_statement.rb
73
80
  - lib/going/version.rb
74
81
  - spec/going/channel_spec.rb
75
82
  - spec/going/kernel_spec.rb
83
+ - spec/going/select_statement_spec.rb
76
84
  - spec/going_spec.rb
77
85
  - spec/spec_helper.rb
86
+ - spec/support/spy.rb
78
87
  homepage: https://github.com/jridgewell/going
79
88
  licenses:
80
89
  - MIT
@@ -102,5 +111,7 @@ summary: Go for Ruby
102
111
  test_files:
103
112
  - spec/going/channel_spec.rb
104
113
  - spec/going/kernel_spec.rb
114
+ - spec/going/select_statement_spec.rb
105
115
  - spec/going_spec.rb
106
116
  - spec/spec_helper.rb
117
+ - spec/support/spy.rb