cql-rb 1.0.0.pre0
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 +13 -0
- data/bin/cqlexec +135 -0
- data/lib/cql.rb +11 -0
- data/lib/cql/client.rb +196 -0
- data/lib/cql/future.rb +176 -0
- data/lib/cql/io.rb +13 -0
- data/lib/cql/io/io_reactor.rb +351 -0
- data/lib/cql/protocol.rb +39 -0
- data/lib/cql/protocol/decoding.rb +156 -0
- data/lib/cql/protocol/encoding.rb +109 -0
- data/lib/cql/protocol/request_frame.rb +228 -0
- data/lib/cql/protocol/response_frame.rb +551 -0
- data/lib/cql/uuid.rb +46 -0
- data/lib/cql/version.rb +5 -0
- data/spec/cql/client_spec.rb +368 -0
- data/spec/cql/future_spec.rb +297 -0
- data/spec/cql/io/io_reactor_spec.rb +290 -0
- data/spec/cql/protocol/decoding_spec.rb +464 -0
- data/spec/cql/protocol/encoding_spec.rb +338 -0
- data/spec/cql/protocol/request_frame_spec.rb +359 -0
- data/spec/cql/protocol/response_frame_spec.rb +746 -0
- data/spec/cql/uuid_spec.rb +40 -0
- data/spec/integration/client_spec.rb +101 -0
- data/spec/integration/protocol_spec.rb +326 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/fake_io_reactor.rb +55 -0
- data/spec/support/fake_server.rb +95 -0
- metadata +87 -0
data/lib/cql/uuid.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Cql
|
4
|
+
class Uuid
|
5
|
+
def initialize(n)
|
6
|
+
case n
|
7
|
+
when String
|
8
|
+
@n = from_s(n)
|
9
|
+
else
|
10
|
+
@n = n
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@s ||= begin
|
16
|
+
parts = []
|
17
|
+
parts << (@n >> (24 * 4)).to_s(16).rjust(8, '0')
|
18
|
+
parts << ((@n >> (20 * 4)) & 0xffff).to_s(16).rjust(4, '0')
|
19
|
+
parts << ((@n >> (16 * 4)) & 0xffff).to_s(16).rjust(4, '0')
|
20
|
+
parts << ((@n >> (12 * 4)) & 0xffff).to_s(16).rjust(4, '0')
|
21
|
+
parts << (@n & 0xffffffffffff).to_s(16).rjust(12, '0')
|
22
|
+
parts.join('-').force_encoding(::Encoding::ASCII)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def value
|
27
|
+
@n
|
28
|
+
end
|
29
|
+
|
30
|
+
def eql?(other)
|
31
|
+
self.value == other.value
|
32
|
+
end
|
33
|
+
alias_method :==, :eql?
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def from_s(str)
|
38
|
+
str = str.gsub('-', '')
|
39
|
+
n = 0
|
40
|
+
(str.length/2).times do |i|
|
41
|
+
n = (n << 8) | str[i * 2, 2].to_i(16)
|
42
|
+
end
|
43
|
+
n
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/cql/version.rb
ADDED
@@ -0,0 +1,368 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
|
6
|
+
module Cql
|
7
|
+
describe Client do
|
8
|
+
let :connection_options do
|
9
|
+
{:host => 'example.com', :port => 12321, :io_reactor => io_reactor}
|
10
|
+
end
|
11
|
+
|
12
|
+
let :io_reactor do
|
13
|
+
FakeIoReactor.new
|
14
|
+
end
|
15
|
+
|
16
|
+
let :client do
|
17
|
+
described_class.new(connection_options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def connections
|
21
|
+
io_reactor.connections
|
22
|
+
end
|
23
|
+
|
24
|
+
def connection
|
25
|
+
connections.first
|
26
|
+
end
|
27
|
+
|
28
|
+
def requests
|
29
|
+
connection[:requests]
|
30
|
+
end
|
31
|
+
|
32
|
+
def last_request
|
33
|
+
requests.last
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#start!' do
|
37
|
+
it 'connects' do
|
38
|
+
client.start!
|
39
|
+
connections.should have(1).item
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'connects only once' do
|
43
|
+
client.start!
|
44
|
+
client.start!
|
45
|
+
connections.should have(1).item
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'connects to all hosts' do
|
49
|
+
client.shutdown!
|
50
|
+
io_reactor.stop.get
|
51
|
+
io_reactor.start.get
|
52
|
+
|
53
|
+
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
54
|
+
c.start!
|
55
|
+
connections.should have(3).items
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'returns itself' do
|
59
|
+
client.start!.should equal(client)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'forwards the host and port' do
|
63
|
+
client.start!
|
64
|
+
connection[:host].should == 'example.com'
|
65
|
+
connection[:port].should == 12321
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'sends a startup request' do
|
69
|
+
client.start!
|
70
|
+
last_request.should be_a(Protocol::StartupRequest)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'sends a startup request to each connection' do
|
74
|
+
client.shutdown!
|
75
|
+
io_reactor.stop.get
|
76
|
+
io_reactor.start.get
|
77
|
+
|
78
|
+
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
79
|
+
c.start!
|
80
|
+
connections.each do |cc|
|
81
|
+
cc[:requests].last.should be_a(Protocol::StartupRequest)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'is not in a keyspace' do
|
86
|
+
client.start!
|
87
|
+
client.keyspace.should be_nil
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'changes to the keyspace given as an option' do
|
91
|
+
c = described_class.new(connection_options.merge(:keyspace => 'hello_world'))
|
92
|
+
c.start!
|
93
|
+
last_request.should == Protocol::QueryRequest.new('USE hello_world', :one)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'validates the keyspace name before sending the USE command' do
|
97
|
+
c = described_class.new(connection_options.merge(:keyspace => 'system; DROP KEYSPACE system'))
|
98
|
+
expect { c.start! }.to raise_error(InvalidKeyspaceNameError)
|
99
|
+
requests.should_not include(Protocol::QueryRequest.new('USE system; DROP KEYSPACE system', :one))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#shutdown!' do
|
104
|
+
it 'closes the connection' do
|
105
|
+
client.start!
|
106
|
+
client.shutdown!
|
107
|
+
io_reactor.should_not be_running
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'accepts multiple calls to #shutdown!' do
|
111
|
+
client.start!
|
112
|
+
client.shutdown!
|
113
|
+
client.shutdown!
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'returns itself' do
|
117
|
+
client.start!.shutdown!.should equal(client)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe '#use' do
|
122
|
+
before do
|
123
|
+
client.start!
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'executes a USE query' do
|
127
|
+
io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
|
128
|
+
client.use('system')
|
129
|
+
last_request.should == Protocol::QueryRequest.new('USE system', :one)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'executes a USE query for each connection' do
|
133
|
+
client.shutdown!
|
134
|
+
io_reactor.stop.get
|
135
|
+
io_reactor.start.get
|
136
|
+
|
137
|
+
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
138
|
+
c.start!
|
139
|
+
|
140
|
+
c.use('system')
|
141
|
+
last_requests = connections.select { |c| c[:host] =~ /^h\d\.example\.com$/ }.sort_by { |c| c[:host] }.map { |c| c[:requests].last }
|
142
|
+
last_requests.should == [
|
143
|
+
Protocol::QueryRequest.new('USE system', :one),
|
144
|
+
Protocol::QueryRequest.new('USE system', :one),
|
145
|
+
Protocol::QueryRequest.new('USE system', :one)
|
146
|
+
]
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'knows which keyspace it changed to' do
|
150
|
+
io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
|
151
|
+
client.use('system')
|
152
|
+
client.keyspace.should == 'system'
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'raises an error if the keyspace name is not valid' do
|
156
|
+
expect { client.use('system; DROP KEYSPACE system') }.to raise_error(InvalidKeyspaceNameError)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '#execute' do
|
161
|
+
before do
|
162
|
+
client.start!
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'asks the connection to execute the query' do
|
166
|
+
client.execute('UPDATE stuff SET thing = 1 WHERE id = 3')
|
167
|
+
last_request.should == Protocol::QueryRequest.new('UPDATE stuff SET thing = 1 WHERE id = 3', :quorum)
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'uses the specified consistency' do
|
171
|
+
client.execute('UPDATE stuff SET thing = 1 WHERE id = 3', :three)
|
172
|
+
last_request.should == Protocol::QueryRequest.new('UPDATE stuff SET thing = 1 WHERE id = 3', :three)
|
173
|
+
end
|
174
|
+
|
175
|
+
context 'with a void CQL query' do
|
176
|
+
it 'returns nil' do
|
177
|
+
io_reactor.queue_response(Protocol::VoidResultResponse.new)
|
178
|
+
result = client.execute('UPDATE stuff SET thing = 1 WHERE id = 3')
|
179
|
+
result.should be_nil
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context 'with a USE query' do
|
184
|
+
it 'returns nil' do
|
185
|
+
io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
|
186
|
+
result = client.execute('USE system')
|
187
|
+
result.should be_nil
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'knows which keyspace it changed to' do
|
191
|
+
io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
|
192
|
+
client.execute('USE system')
|
193
|
+
client.keyspace.should == 'system'
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'detects that one connection changed to a keyspace and changes the others too' do
|
197
|
+
client.shutdown!
|
198
|
+
io_reactor.stop.get
|
199
|
+
io_reactor.start.get
|
200
|
+
|
201
|
+
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
202
|
+
c.start!
|
203
|
+
|
204
|
+
io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'), connections.find { |c| c[:host] == 'h1.example.com' }[:host])
|
205
|
+
io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'), connections.find { |c| c[:host] == 'h2.example.com' }[:host])
|
206
|
+
io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'), connections.find { |c| c[:host] == 'h3.example.com' }[:host])
|
207
|
+
|
208
|
+
c.execute('USE system', :one)
|
209
|
+
c.keyspace.should == 'system'
|
210
|
+
|
211
|
+
last_requests = connections.select { |c| c[:host] =~ /^h\d\.example\.com$/ }.sort_by { |c| c[:host] }.map { |c| c[:requests].last }
|
212
|
+
last_requests.should == [
|
213
|
+
Protocol::QueryRequest.new('USE system', :one),
|
214
|
+
Protocol::QueryRequest.new('USE system', :one),
|
215
|
+
Protocol::QueryRequest.new('USE system', :one)
|
216
|
+
]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
context 'with an SELECT query' do
|
221
|
+
let :rows do
|
222
|
+
[['xyz', 'abc'], ['abc', 'xyz'], ['123', 'xyz']]
|
223
|
+
end
|
224
|
+
|
225
|
+
let :metadata do
|
226
|
+
[['thingies', 'things', 'thing', :text], ['thingies', 'things', 'item', :text]]
|
227
|
+
end
|
228
|
+
|
229
|
+
let :result do
|
230
|
+
io_reactor.queue_response(Protocol::RowsResultResponse.new(rows, metadata))
|
231
|
+
client.execute('SELECT * FROM things')
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'returns an Enumerable of rows' do
|
235
|
+
row_count = 0
|
236
|
+
result.each do |row|
|
237
|
+
row_count += 1
|
238
|
+
end
|
239
|
+
row_count.should == 3
|
240
|
+
end
|
241
|
+
|
242
|
+
context 'with metadata that' do
|
243
|
+
it 'has keyspace, table and type information' do
|
244
|
+
result.metadata['item'].keyspace.should == 'thingies'
|
245
|
+
result.metadata['item'].table.should == 'things'
|
246
|
+
result.metadata['item'].column_name.should == 'item'
|
247
|
+
result.metadata['item'].type.should == :text
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'is an Enumerable' do
|
251
|
+
result.metadata.map(&:type).should == [:text, :text]
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'is splattable' do
|
255
|
+
ks, table, col, type = result.metadata['thing']
|
256
|
+
ks.should == 'thingies'
|
257
|
+
table.should == 'things'
|
258
|
+
col.should == 'thing'
|
259
|
+
type.should == :text
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
context 'when the response is an error' do
|
265
|
+
it 'raises an error' do
|
266
|
+
io_reactor.queue_response(Protocol::ErrorResponse.new(0xabcd, 'Blurgh'))
|
267
|
+
expect { client.execute('SELECT * FROM things') }.to raise_error(QueryError, 'Blurgh')
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe '#prepare' do
|
273
|
+
before do
|
274
|
+
client.start!
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'sends a prepare request' do
|
278
|
+
client.prepare('SELECT * FROM system.peers')
|
279
|
+
last_request.should == Protocol::PrepareRequest.new('SELECT * FROM system.peers')
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'returns a prepared statement' do
|
283
|
+
io_reactor.queue_response(Protocol::PreparedResultResponse.new('A' * 32, [['stuff', 'things', 'item', :varchar]]))
|
284
|
+
statement = client.prepare('SELECT * FROM stuff.things WHERE item = ?')
|
285
|
+
statement.should_not be_nil
|
286
|
+
end
|
287
|
+
|
288
|
+
it 'executes a prepared statement' do
|
289
|
+
id = 'A' * 32
|
290
|
+
metadata = [['stuff', 'things', 'item', :varchar]]
|
291
|
+
io_reactor.queue_response(Protocol::PreparedResultResponse.new(id, metadata))
|
292
|
+
statement = client.prepare('SELECT * FROM stuff.things WHERE item = ?')
|
293
|
+
statement.execute('foo')
|
294
|
+
last_request.should == Protocol::ExecuteRequest.new(id, metadata, ['foo'], :quorum)
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'executes a prepared statement using the right connection' do
|
298
|
+
client.shutdown!
|
299
|
+
io_reactor.stop.get
|
300
|
+
io_reactor.start.get
|
301
|
+
|
302
|
+
metadata = [['stuff', 'things', 'item', :varchar]]
|
303
|
+
|
304
|
+
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
305
|
+
c.start!
|
306
|
+
|
307
|
+
io_reactor.queue_response(Protocol::PreparedResultResponse.new('A' * 32, metadata))
|
308
|
+
io_reactor.queue_response(Protocol::PreparedResultResponse.new('B' * 32, metadata))
|
309
|
+
io_reactor.queue_response(Protocol::PreparedResultResponse.new('C' * 32, metadata))
|
310
|
+
|
311
|
+
statement1 = c.prepare('SELECT * FROM stuff.things WHERE item = ?')
|
312
|
+
statement1_connection = io_reactor.last_used_connection
|
313
|
+
statement2 = c.prepare('SELECT * FROM stuff.things WHERE item = ?')
|
314
|
+
statement2_connection = io_reactor.last_used_connection
|
315
|
+
statement3 = c.prepare('SELECT * FROM stuff.things WHERE item = ?')
|
316
|
+
statement3_connection = io_reactor.last_used_connection
|
317
|
+
|
318
|
+
io_reactor.queue_response(Protocol::RowsResultResponse.new([{'thing' => 'foo1'}], metadata), statement1_connection[:host])
|
319
|
+
io_reactor.queue_response(Protocol::RowsResultResponse.new([{'thing' => 'foo2'}], metadata), statement2_connection[:host])
|
320
|
+
io_reactor.queue_response(Protocol::RowsResultResponse.new([{'thing' => 'foo3'}], metadata), statement3_connection[:host])
|
321
|
+
|
322
|
+
statement1.execute('foo').first.should == {'thing' => 'foo1'}
|
323
|
+
statement2.execute('foo').first.should == {'thing' => 'foo2'}
|
324
|
+
statement3.execute('foo').first.should == {'thing' => 'foo3'}
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
context 'when not connected' do
|
329
|
+
it 'complains when #use is called before #start!' do
|
330
|
+
expect { client.use('system') }.to raise_error(NotConnectedError)
|
331
|
+
end
|
332
|
+
|
333
|
+
it 'complains when #use is called after #shutdown!' do
|
334
|
+
client.start!
|
335
|
+
client.shutdown!
|
336
|
+
expect { client.use('system') }.to raise_error(NotConnectedError)
|
337
|
+
end
|
338
|
+
|
339
|
+
it 'complains when #execute is called before #start!' do
|
340
|
+
expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'complains when #execute is called after #shutdown!' do
|
344
|
+
client.start!
|
345
|
+
client.shutdown!
|
346
|
+
expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'complains when #prepare is called before #start!' do
|
350
|
+
expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'complains when #prepare is called after #shutdown!' do
|
354
|
+
client.start!
|
355
|
+
client.shutdown!
|
356
|
+
expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
|
357
|
+
end
|
358
|
+
|
359
|
+
it 'complains when #execute of a prepared statement is called after #shutdown!' do
|
360
|
+
client.start!
|
361
|
+
io_reactor.queue_response(Protocol::PreparedResultResponse.new('A' * 32, []))
|
362
|
+
statement = client.prepare('DELETE FROM stuff WHERE id = 3')
|
363
|
+
client.shutdown!
|
364
|
+
expect { statement.execute }.to raise_error(NotConnectedError)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
|
6
|
+
module Cql
|
7
|
+
describe Future do
|
8
|
+
let :future do
|
9
|
+
described_class.new
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'when completed' do
|
13
|
+
it 'is complete' do
|
14
|
+
future.complete!('foo')
|
15
|
+
future.should be_complete
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'has a value' do
|
19
|
+
future.complete!('foo')
|
20
|
+
future.value.should == 'foo'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'is complete even when the value is falsy' do
|
24
|
+
future.complete!(nil)
|
25
|
+
future.should be_complete
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns the value from #get, too' do
|
29
|
+
future.complete!('foo')
|
30
|
+
future.get.should == 'foo'
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'has the value nil by default' do
|
34
|
+
future.complete!
|
35
|
+
future.value.should be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'notifies all completion listeners' do
|
39
|
+
v1, v2 = nil, nil
|
40
|
+
future.on_complete { |v| v1 = v }
|
41
|
+
future.on_complete { |v| v2 = v }
|
42
|
+
future.complete!('bar')
|
43
|
+
v1.should == 'bar'
|
44
|
+
v2.should == 'bar'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'notifies new listeners even when already completed' do
|
48
|
+
v1, v2 = nil, nil
|
49
|
+
future.complete!('bar')
|
50
|
+
future.on_complete { |v| v1 = v }
|
51
|
+
future.on_complete { |v| v2 = v }
|
52
|
+
v1.should == 'bar'
|
53
|
+
v2.should == 'bar'
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'blocks on #value until completed' do
|
57
|
+
Thread.start(future) do |f|
|
58
|
+
sleep 0.1
|
59
|
+
future.complete!('bar')
|
60
|
+
end
|
61
|
+
future.value.should == 'bar'
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'blocks on #value until completed, when value is nil' do
|
65
|
+
Thread.start(future) do |f|
|
66
|
+
sleep 0.1
|
67
|
+
future.complete!
|
68
|
+
end
|
69
|
+
future.value.should be_nil
|
70
|
+
future.value.should be_nil
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'blocks on #value until failed' do
|
74
|
+
Thread.start(future) do |f|
|
75
|
+
sleep 0.1
|
76
|
+
future.fail!(StandardError.new('FAIL!'))
|
77
|
+
end
|
78
|
+
expect { future.value }.to raise_error('FAIL!')
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'cannot be completed again' do
|
82
|
+
future.complete!('bar')
|
83
|
+
expect { future.complete!('foo') }.to raise_error(FutureError)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'cannot be failed again' do
|
87
|
+
future.complete!('bar')
|
88
|
+
expect { future.fail!(StandardError.new('FAIL!')) }.to raise_error(FutureError)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when failed' do
|
93
|
+
it 'is failed' do
|
94
|
+
future.fail!(StandardError.new('FAIL!'))
|
95
|
+
future.should be_failed
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'raises the error from #value' do
|
99
|
+
future.fail!(StandardError.new('FAIL!'))
|
100
|
+
expect { future.value }.to raise_error('FAIL!')
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'notifies all failure listeners' do
|
104
|
+
e1, e2 = nil, nil
|
105
|
+
future.on_failure { |e| e1 = e }
|
106
|
+
future.on_failure { |e| e2 = e }
|
107
|
+
future.fail!(StandardError.new('FAIL!'))
|
108
|
+
e1.message.should == 'FAIL!'
|
109
|
+
e2.message.should == 'FAIL!'
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'notifies new listeners even when already failed' do
|
113
|
+
e1, e2 = nil, nil
|
114
|
+
future.fail!(StandardError.new('FAIL!'))
|
115
|
+
future.on_failure { |e| e1 = e }
|
116
|
+
future.on_failure { |e| e2 = e }
|
117
|
+
e1.message.should == 'FAIL!'
|
118
|
+
e2.message.should == 'FAIL!'
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'cannot be failed again' do
|
122
|
+
future.fail!(StandardError.new('FAIL!'))
|
123
|
+
expect { future.fail!(StandardError.new('FAIL!')) }.to raise_error(FutureError)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'cannot be completed' do
|
127
|
+
future.fail!(StandardError.new('FAIL!'))
|
128
|
+
expect { future.complete!('hurgh') }.to raise_error(FutureError)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe '.combine' do
|
133
|
+
context 'returns a new future which' do
|
134
|
+
it 'is complete when the source futures are complete' do
|
135
|
+
f1 = Future.new
|
136
|
+
f2 = Future.new
|
137
|
+
f3 = Future.combine(f1, f2)
|
138
|
+
f1.complete!
|
139
|
+
f3.should_not be_complete
|
140
|
+
f2.complete!
|
141
|
+
f3.should be_complete
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'completes when the source futures have completed' do
|
145
|
+
sequence = []
|
146
|
+
f1 = Future.new
|
147
|
+
f2 = Future.new
|
148
|
+
f3 = Future.new
|
149
|
+
f4 = Future.combine(f1, f2, f3)
|
150
|
+
f1.on_complete { sequence << 1 }
|
151
|
+
f2.on_complete { sequence << 2 }
|
152
|
+
f3.on_complete { sequence << 3 }
|
153
|
+
f2.complete!
|
154
|
+
f1.complete!
|
155
|
+
f3.complete!
|
156
|
+
sequence.should == [2, 1, 3]
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'returns an array of the values of the source futures, in order' do
|
160
|
+
f1 = Future.new
|
161
|
+
f2 = Future.new
|
162
|
+
f3 = Future.new
|
163
|
+
f4 = Future.combine(f1, f2, f3)
|
164
|
+
f2.complete!(2)
|
165
|
+
f1.complete!(1)
|
166
|
+
f3.complete!(3)
|
167
|
+
f4.get.should == [1, 2, 3]
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'fails if any of the source futures fail' do
|
171
|
+
f1 = Future.new
|
172
|
+
f2 = Future.new
|
173
|
+
f3 = Future.new
|
174
|
+
f4 = Future.new
|
175
|
+
f5 = Future.combine(f1, f2, f3, f4)
|
176
|
+
f2.complete!
|
177
|
+
f1.fail!(StandardError.new('hurgh'))
|
178
|
+
f3.fail!(StandardError.new('murgasd'))
|
179
|
+
f4.complete!
|
180
|
+
expect { f5.get }.to raise_error('hurgh')
|
181
|
+
f5.should be_failed
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'raises an error when #complete! is called' do
|
185
|
+
f = Future.combine(Future.new, Future.new)
|
186
|
+
expect { f.complete! }.to raise_error(FutureError)
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'raises an error when #fail! is called' do
|
190
|
+
f = Future.combine(Future.new, Future.new)
|
191
|
+
expect { f.fail!(StandardError.new('Blurgh')) }.to raise_error(FutureError)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe '#map' do
|
197
|
+
context 'returns a new future that' do
|
198
|
+
it 'will complete with the result of the given block' do
|
199
|
+
mapped_value = nil
|
200
|
+
f1 = Future.new
|
201
|
+
f2 = f1.map { |v| v * 2 }
|
202
|
+
f2.on_complete { |v| mapped_value = v }
|
203
|
+
f1.complete!(3)
|
204
|
+
mapped_value.should == 3 * 2
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'fails when the original future fails' do
|
208
|
+
failed = false
|
209
|
+
f1 = Future.new
|
210
|
+
f2 = f1.map { |v| v * 2 }
|
211
|
+
f2.on_failure { failed = true }
|
212
|
+
f1.fail!(StandardError.new('Blurgh'))
|
213
|
+
failed.should be_true
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'fails when the block raises an error' do
|
217
|
+
f1 = Future.new
|
218
|
+
f2 = f1.map { |v| raise 'Blurgh' }
|
219
|
+
Thread.start do
|
220
|
+
sleep(0.01)
|
221
|
+
f1.complete!
|
222
|
+
end
|
223
|
+
expect { f2.get }.to raise_error('Blurgh')
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
describe '#flat_map' do
|
229
|
+
it 'works like #map, but expects that the block returns a future' do
|
230
|
+
f1 = Future.new
|
231
|
+
f2 = f1.flat_map { |v| Future.completed(v * 2) }
|
232
|
+
f1.complete!(3)
|
233
|
+
f2.value.should == 3 * 2
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'fails when the block raises an error' do
|
237
|
+
f1 = Future.new
|
238
|
+
f2 = f1.flat_map { |v| raise 'Hurgh' }
|
239
|
+
f1.complete!(3)
|
240
|
+
expect { f2.get }.to raise_error('Hurgh')
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
describe '.completed' do
|
245
|
+
let :future do
|
246
|
+
described_class.completed('hello world')
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'is complete when created' do
|
250
|
+
future.should be_complete
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'calls callbacks immediately' do
|
254
|
+
value = nil
|
255
|
+
future.on_complete { |v| value = v }
|
256
|
+
value.should == 'hello world'
|
257
|
+
end
|
258
|
+
|
259
|
+
it 'does not block on #value' do
|
260
|
+
future.value.should == 'hello world'
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'defaults to the value nil' do
|
264
|
+
described_class.completed.value.should be_nil
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'handles #map' do
|
268
|
+
described_class.completed('foo').map(&:upcase).value.should == 'FOO'
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'handles #map' do
|
272
|
+
f = described_class.completed('foo').map { |v| raise 'Blurgh' }
|
273
|
+
f.should be_failed
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
describe '.failed' do
|
278
|
+
let :future do
|
279
|
+
described_class.failed(StandardError.new('Blurgh'))
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'is failed when created' do
|
283
|
+
future.should be_failed
|
284
|
+
end
|
285
|
+
|
286
|
+
it 'calls callbacks immediately' do
|
287
|
+
error = nil
|
288
|
+
future.on_failure { |e| error = e }
|
289
|
+
error.message.should == 'Blurgh'
|
290
|
+
end
|
291
|
+
|
292
|
+
it 'does not block on #value' do
|
293
|
+
expect { future.value }.to raise_error('Blurgh')
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|