riak-client 1.2.0 → 1.4.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 +7 -0
- data/.gitignore +1 -0
- data/Gemfile +1 -7
- data/README.markdown +66 -0
- data/RELEASE_NOTES.md +27 -0
- data/lib/riak/bucket.rb +24 -5
- data/lib/riak/client.rb +42 -7
- data/lib/riak/client/beefcake/message_codes.rb +56 -0
- data/lib/riak/client/beefcake/messages.rb +190 -18
- data/lib/riak/client/beefcake_protobuffs_backend.rb +143 -10
- data/lib/riak/client/feature_detection.rb +26 -1
- data/lib/riak/client/http_backend.rb +58 -9
- data/lib/riak/client/http_backend/bucket_streamer.rb +15 -0
- data/lib/riak/client/http_backend/chunked_json_streamer.rb +42 -0
- data/lib/riak/client/http_backend/configuration.rb +17 -1
- data/lib/riak/client/http_backend/key_streamer.rb +4 -32
- data/lib/riak/client/protobuffs_backend.rb +12 -34
- data/lib/riak/counter.rb +101 -0
- data/lib/riak/index_collection.rb +71 -0
- data/lib/riak/list_buckets.rb +28 -0
- data/lib/riak/locale/en.yml +14 -0
- data/lib/riak/multiget.rb +123 -0
- data/lib/riak/node.rb +2 -0
- data/lib/riak/node/configuration.rb +32 -21
- data/lib/riak/node/defaults.rb +2 -0
- data/lib/riak/node/generation.rb +19 -7
- data/lib/riak/node/version.rb +2 -16
- data/lib/riak/robject.rb +1 -0
- data/lib/riak/secondary_index.rb +67 -0
- data/lib/riak/version.rb +1 -1
- data/riak-client.gemspec +3 -2
- data/spec/integration/riak/counters_spec.rb +51 -0
- data/spec/integration/riak/http_backends_spec.rb +24 -14
- data/spec/integration/riak/node_spec.rb +6 -28
- data/spec/riak/beefcake_protobuffs_backend_spec.rb +84 -0
- data/spec/riak/bucket_spec.rb +55 -5
- data/spec/riak/client_spec.rb +34 -0
- data/spec/riak/counter_spec.rb +122 -0
- data/spec/riak/index_collection_spec.rb +50 -0
- data/spec/riak/list_buckets_spec.rb +41 -0
- data/spec/riak/multiget_spec.rb +76 -0
- data/spec/riak/robject_spec.rb +4 -1
- data/spec/riak/secondary_index_spec.rb +225 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/sometimes.rb +2 -2
- data/spec/support/unified_backend_examples.rb +4 -0
- metadata +41 -47
@@ -37,6 +37,90 @@ describe Riak::Client::BeefcakeProtobuffsBackend do
|
|
37
37
|
backend.list_keys(exp_bucket).should == exp_keys
|
38
38
|
end
|
39
39
|
|
40
|
+
context "secondary index" do
|
41
|
+
before :each do
|
42
|
+
@socket = mock(:socket).as_null_object
|
43
|
+
TCPSocket.stub(:new => @socket)
|
44
|
+
end
|
45
|
+
context 'when streaming' do
|
46
|
+
it "should stream when a block is given" do
|
47
|
+
backend.should_receive(:write_protobuff) do |msg, req|
|
48
|
+
msg.should == :IndexReq
|
49
|
+
req[:stream].should == true
|
50
|
+
end
|
51
|
+
backend.should_receive(:decode_index_response)
|
52
|
+
|
53
|
+
blk = proc{:asdf}
|
54
|
+
|
55
|
+
backend.get_index('bucket', 'words', 'asdf'..'hjkl', &blk)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should send batches of results to the block" do
|
59
|
+
backend.should_receive(:write_protobuff)
|
60
|
+
|
61
|
+
response_sets = [%w{asdf asdg asdh}, %w{gggg gggh gggi}]
|
62
|
+
response_messages = response_sets.map do |s|
|
63
|
+
Riak::Client::BeefcakeProtobuffsBackend::RpbIndexResp.new keys: s
|
64
|
+
end
|
65
|
+
response_messages.last.done = true
|
66
|
+
|
67
|
+
response_chunks = response_messages.map do |m|
|
68
|
+
encoded = m.encode
|
69
|
+
header = [encoded.length + 1, 26].pack 'NC'
|
70
|
+
[header, encoded]
|
71
|
+
end.flatten
|
72
|
+
|
73
|
+
@socket.should_receive(:read).and_return(*response_chunks)
|
74
|
+
|
75
|
+
block_body = mock 'block'
|
76
|
+
block_body.should_receive(:check).with(response_sets.first).once
|
77
|
+
block_body.should_receive(:check).with(response_sets.last).once
|
78
|
+
|
79
|
+
blk = proc {|m| block_body.check m }
|
80
|
+
|
81
|
+
backend.get_index 'bucket', 'words', 'asdf'..'hjkl', &blk
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should return a full batch of results when not streaming" do
|
86
|
+
backend.should_receive(:write_protobuff) do |msg, req|
|
87
|
+
msg.should == :IndexReq
|
88
|
+
req[:stream].should_not be
|
89
|
+
end
|
90
|
+
|
91
|
+
response_message = Riak::Client::BeefcakeProtobuffsBackend::
|
92
|
+
RpbIndexResp.new(
|
93
|
+
keys: %w{asdf asdg asdh}
|
94
|
+
).encode
|
95
|
+
header = [response_message.length + 1, 26].pack 'NC'
|
96
|
+
@socket.should_receive(:read).and_return(header, response_message)
|
97
|
+
|
98
|
+
results = backend.get_index 'bucket', 'words', 'asdf'..'hjkl'
|
99
|
+
results.should == %w{asdf asdg asdh}
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should not crash out when no keys or terms are released" do
|
103
|
+
backend.should_receive(:write_protobuff) do |msg, req|
|
104
|
+
msg.should == :IndexReq
|
105
|
+
req[:stream].should_not be
|
106
|
+
end
|
107
|
+
|
108
|
+
response_message = Riak::Client::BeefcakeProtobuffsBackend::
|
109
|
+
RpbIndexResp.new().encode
|
110
|
+
|
111
|
+
header = [response_message.length + 1, 26].pack 'NC'
|
112
|
+
@socket.should_receive(:read).and_return(header, response_message)
|
113
|
+
|
114
|
+
results = nil
|
115
|
+
fetch = proc do
|
116
|
+
results = backend.get_index 'bucket', 'words', 'asdf'
|
117
|
+
end
|
118
|
+
|
119
|
+
fetch.should_not raise_error
|
120
|
+
results.should == []
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
40
124
|
context "#mapred" do
|
41
125
|
let(:mapred) { Riak::MapReduce.new(client).add('test').map("function(){}").map("function(){}") }
|
42
126
|
|
data/spec/riak/bucket_spec.rb
CHANGED
@@ -16,6 +16,7 @@ describe Riak::Bucket do
|
|
16
16
|
lambda { Riak::Bucket.new("foo") }.should raise_error
|
17
17
|
lambda { Riak::Bucket.new("foo", @client) }.should raise_error
|
18
18
|
lambda { Riak::Bucket.new(@client, "foo") }.should_not raise_error
|
19
|
+
expect { Riak::Bucket.new(@client, '') }.to raise_error(ArgumentError)
|
19
20
|
end
|
20
21
|
|
21
22
|
it "should set the client and name attributes" do
|
@@ -27,12 +28,12 @@ describe Riak::Bucket do
|
|
27
28
|
|
28
29
|
describe "accessing keys" do
|
29
30
|
it "should list the keys" do
|
30
|
-
@backend.should_receive(:list_keys).with(@bucket).and_return(["bar"])
|
31
|
+
@backend.should_receive(:list_keys).with(@bucket, {}).and_return(["bar"])
|
31
32
|
@bucket.keys.should == ["bar"]
|
32
33
|
end
|
33
34
|
|
34
35
|
it "should allow streaming keys through block" do
|
35
|
-
@backend.should_receive(:list_keys).with(@bucket).and_yield([]).and_yield(["bar"]).and_yield(["baz"])
|
36
|
+
@backend.should_receive(:list_keys).with(@bucket, {}).and_yield([]).and_yield(["bar"]).and_yield(["baz"])
|
36
37
|
all_keys = []
|
37
38
|
@bucket.keys do |list|
|
38
39
|
all_keys.concat(list)
|
@@ -41,7 +42,7 @@ describe Riak::Bucket do
|
|
41
42
|
end
|
42
43
|
|
43
44
|
it "should not cache the list of keys" do
|
44
|
-
@backend.should_receive(:list_keys).with(@bucket).twice.and_return(["bar"])
|
45
|
+
@backend.should_receive(:list_keys).with(@bucket, {}).twice.and_return(["bar"])
|
45
46
|
2.times { @bucket.keys.should == ['bar'] }
|
46
47
|
end
|
47
48
|
|
@@ -52,6 +53,24 @@ describe Riak::Bucket do
|
|
52
53
|
@bucket.keys
|
53
54
|
Riak.disable_list_keys_warnings = true
|
54
55
|
end
|
56
|
+
|
57
|
+
it "should allow a specified timeout when listing keys" do
|
58
|
+
@backend.should_receive(:list_keys).with(@bucket, timeout: 1234).and_return(%w{bar})
|
59
|
+
|
60
|
+
keys = @bucket.keys timeout: 1234
|
61
|
+
|
62
|
+
keys.should == %w{bar}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "accessing a counter" do
|
67
|
+
it "should return a counter object" do
|
68
|
+
Riak::Counter.should_receive(:new).with(@bucket, 'asdf').and_return('example counter')
|
69
|
+
|
70
|
+
new_counter = @bucket.counter 'asdf'
|
71
|
+
|
72
|
+
new_counter.should == 'example counter'
|
73
|
+
end
|
55
74
|
end
|
56
75
|
|
57
76
|
describe "setting the bucket properties" do
|
@@ -104,6 +123,12 @@ describe Riak::Bucket do
|
|
104
123
|
@backend.should_receive(:fetch_object).with(@bucket, "db", {:r => 2}).and_return(nil)
|
105
124
|
@bucket.get("db", :r => 2)
|
106
125
|
end
|
126
|
+
|
127
|
+
it "should disallow fetching an object with a zero-length key" do
|
128
|
+
## TODO: This actually tests the Client object, but there is no suite
|
129
|
+
## of tests for its generic interface.
|
130
|
+
expect { @bucket.get('') }.to raise_error(ArgumentError)
|
131
|
+
end
|
107
132
|
end
|
108
133
|
|
109
134
|
describe "creating a new blank object" do
|
@@ -141,10 +166,35 @@ describe Riak::Bucket do
|
|
141
166
|
end
|
142
167
|
end
|
143
168
|
|
169
|
+
describe "fetching multiple objects" do
|
170
|
+
it 'should get each object individually' do
|
171
|
+
@object1 = mock('obj1')
|
172
|
+
@object2 = mock('obj2')
|
173
|
+
@bucket.should_receive(:[]).with('key1').and_return(@object1)
|
174
|
+
@bucket.should_receive(:[]).with('key2').and_return(@object2)
|
175
|
+
|
176
|
+
@results = @bucket.get_many %w{key1 key2}
|
177
|
+
|
178
|
+
@results['key1'].should == @object1
|
179
|
+
@results['key2'].should == @object2
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
144
183
|
describe "querying an index" do
|
145
184
|
it "should list the matching keys" do
|
146
|
-
@backend.
|
147
|
-
|
185
|
+
@backend.
|
186
|
+
should_receive(:get_index).
|
187
|
+
with(@bucket, "test_bin", "testing", {return_terms: true}).
|
188
|
+
and_return(Riak::IndexCollection.new_from_json({
|
189
|
+
'results' => [
|
190
|
+
{'testing' => 'asdf'},
|
191
|
+
{'testing' => 'hjkl'}]
|
192
|
+
}.to_json))
|
193
|
+
result = @bucket.get_index("test_bin", "testing", return_terms: true)
|
194
|
+
|
195
|
+
result.should be_a Riak::IndexCollection
|
196
|
+
result.to_a.should == %w{asdf hjkl}
|
197
|
+
result.with_terms.should == {'testing' => %w{asdf hjkl}}
|
148
198
|
end
|
149
199
|
end
|
150
200
|
|
data/spec/riak/client_spec.rb
CHANGED
@@ -196,6 +196,29 @@ describe Riak::Client do
|
|
196
196
|
end
|
197
197
|
end
|
198
198
|
|
199
|
+
describe "retrieving many values" do
|
200
|
+
before :each do
|
201
|
+
@client = Riak::Client.new
|
202
|
+
@bucket = @client.bucket('foo')
|
203
|
+
@bucket.should_receive(:[]).with('value1').and_return(mock('robject'))
|
204
|
+
@bucket.should_receive(:[]).with('value2').and_return(mock('robject'))
|
205
|
+
@pairs = [
|
206
|
+
[@bucket, 'value1'],
|
207
|
+
[@bucket, 'value2']
|
208
|
+
]
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'should accept an array of bucket and key pairs' do
|
212
|
+
lambda{ @client.get_many(@pairs) }.should_not raise_error
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'should return a hash of bucket/key pairs and robjects' do
|
216
|
+
@results = @client.get_many(@pairs)
|
217
|
+
@results.should be_a Hash
|
218
|
+
@results.length.should be(@pairs.length)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
199
222
|
describe "retrieving a bucket" do
|
200
223
|
before :each do
|
201
224
|
@client = Riak::Client.new
|
@@ -218,6 +241,10 @@ describe Riak::Client do
|
|
218
241
|
@client.bucket("baz").should == @bucket
|
219
242
|
@client.bucket("baz").should == @bucket
|
220
243
|
end
|
244
|
+
|
245
|
+
it "should reject buckets with zero-length names" do
|
246
|
+
expect { @client.bucket('') }.to raise_error(ArgumentError)
|
247
|
+
end
|
221
248
|
end
|
222
249
|
|
223
250
|
describe "listing buckets" do
|
@@ -244,6 +271,13 @@ describe Riak::Client do
|
|
244
271
|
@client.should_receive(:warn)
|
245
272
|
@client.buckets
|
246
273
|
end
|
274
|
+
|
275
|
+
it "should support a timeout option" do
|
276
|
+
@backend.should_receive(:list_buckets).with(timeout: 1234).and_return(%w{test test2})
|
277
|
+
|
278
|
+
buckets = @client.buckets timeout: 1234
|
279
|
+
buckets.should have(2).items
|
280
|
+
end
|
247
281
|
end
|
248
282
|
|
249
283
|
describe "Luwak (large-files) support" do
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Riak::Counter do
|
4
|
+
describe "initialization" do
|
5
|
+
before :each do
|
6
|
+
@bucket = Riak::Bucket.allocate
|
7
|
+
@key = 'key'
|
8
|
+
@bucket.stub allow_mult: true
|
9
|
+
@bucket.stub(client: mock('client'))
|
10
|
+
@bucket.stub('is_a?' => true)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should set the bucket and key" do
|
14
|
+
ctr = Riak::Counter.new @bucket, @key
|
15
|
+
ctr.bucket.should == @bucket
|
16
|
+
ctr.key.should == @key
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should require allow_mult" do
|
20
|
+
@bad_bucket = Riak::Bucket.allocate
|
21
|
+
@bad_bucket.stub allow_mult: false
|
22
|
+
@bad_bucket.stub(client: mock('client'))
|
23
|
+
|
24
|
+
expect{ctr = Riak::Counter.new @bad_bucket, @key}.to raise_error(ArgumentError)
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "incrementing and decrementing" do
|
30
|
+
before :each do
|
31
|
+
@backend = mock 'backend'
|
32
|
+
|
33
|
+
@client = mock 'client'
|
34
|
+
@client.stub(:backend).and_yield @backend
|
35
|
+
|
36
|
+
@bucket = Riak::Bucket.allocate
|
37
|
+
@bucket.stub allow_mult: true
|
38
|
+
@bucket.stub client: @client
|
39
|
+
|
40
|
+
@key = 'key'
|
41
|
+
|
42
|
+
@ctr = Riak::Counter.new @bucket, @key
|
43
|
+
|
44
|
+
@increment_expectation = proc{|n| @backend.should_receive(:post_counter).with(@bucket, @key, n, {})}
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should increment by 1 by default" do
|
48
|
+
@increment_expectation[1]
|
49
|
+
@ctr.increment
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should support incrementing by positive numbers" do
|
53
|
+
@increment_expectation[15]
|
54
|
+
@ctr.increment 15
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should support incrementing by negative numbers" do
|
58
|
+
@increment_expectation[-12]
|
59
|
+
@ctr.increment -12
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should decrement by 1 by default" do
|
63
|
+
@increment_expectation[-1]
|
64
|
+
@ctr.decrement
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should support decrementing by positive numbers" do
|
68
|
+
@increment_expectation[-30]
|
69
|
+
@ctr.decrement 30
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should support decrementing by negative numbers" do
|
73
|
+
@increment_expectation[41]
|
74
|
+
@ctr.decrement -41
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should forbid incrementing by non-integers" do
|
78
|
+
[1.1, nil, :'1', '1', 2.0/2, [1]].each do |candidate|
|
79
|
+
expect do
|
80
|
+
@ctr.increment candidate
|
81
|
+
raise candidate.to_s
|
82
|
+
end.to raise_error(ArgumentError)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "failure modes" do
|
88
|
+
before :each do
|
89
|
+
@nodes = 10.times.map do |n|
|
90
|
+
{pb_port: "100#{n}7"}
|
91
|
+
end
|
92
|
+
|
93
|
+
@fake_pool = mock 'pool'
|
94
|
+
@backend = mock 'backend'
|
95
|
+
|
96
|
+
@client = Riak::Client.new nodes: @nodes, protocol: 'pbc'
|
97
|
+
@client.instance_variable_set :@protobuffs_pool, @fake_pool
|
98
|
+
|
99
|
+
@fake_pool.stub(:take).and_yield(@backend)
|
100
|
+
|
101
|
+
@bucket = Riak::Bucket.allocate
|
102
|
+
@bucket.stub allow_mult: true
|
103
|
+
@bucket.stub client: @client
|
104
|
+
|
105
|
+
@key = 'key'
|
106
|
+
|
107
|
+
@expect_post = @backend.should_receive(:post_counter).with(@bucket, @key, 1, {})
|
108
|
+
|
109
|
+
@ctr = Riak::Counter.new @bucket, @key
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should not retry on timeout" do
|
113
|
+
@expect_post.once.and_raise('timeout')
|
114
|
+
expect(proc { @ctr.increment }).to raise_error
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should not retry on quorum failure" do
|
118
|
+
@expect_post.once.and_raise('quorum not satisfied')
|
119
|
+
expect(proc { @ctr.increment }).to raise_error
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Riak::IndexCollection do
|
4
|
+
describe "json initialization" do
|
5
|
+
it "should accept a list of keys" do
|
6
|
+
@input = {
|
7
|
+
'keys' => %w{first second third}
|
8
|
+
}.to_json
|
9
|
+
lambda { @coll = Riak::IndexCollection.new_from_json @input }.should_not raise_error
|
10
|
+
%w{first second third}.should == @coll
|
11
|
+
end
|
12
|
+
it "should accept a list of keys and a continuation" do
|
13
|
+
@input = {
|
14
|
+
'keys' => %w{first second third},
|
15
|
+
'continuation' => 'examplecontinuation'
|
16
|
+
}.to_json
|
17
|
+
lambda { @coll = Riak::IndexCollection.new_from_json @input }.should_not raise_error
|
18
|
+
%w{first second third}.should == @coll
|
19
|
+
@coll.continuation.should == 'examplecontinuation'
|
20
|
+
end
|
21
|
+
it "should accept a list of results hashes" do
|
22
|
+
@input = {
|
23
|
+
'results' => [
|
24
|
+
{'first' => 'first'},
|
25
|
+
{'second' => 'second'},
|
26
|
+
{'second' => 'other'}
|
27
|
+
]
|
28
|
+
}.to_json
|
29
|
+
|
30
|
+
lambda { @coll = Riak::IndexCollection.new_from_json @input }.should_not raise_error
|
31
|
+
%w{first second other}.should == @coll
|
32
|
+
{'first' => %w{first}, 'second' => %w{second other}}.should == @coll.with_terms
|
33
|
+
end
|
34
|
+
it "should accept a list of results hashes and a continuation" do
|
35
|
+
@input = {
|
36
|
+
'results' => [
|
37
|
+
{'first' => 'first'},
|
38
|
+
{'second' => 'second'},
|
39
|
+
{'second' => 'other'}
|
40
|
+
],
|
41
|
+
'continuation' => 'examplecontinuation'
|
42
|
+
}.to_json
|
43
|
+
|
44
|
+
lambda { @coll = Riak::IndexCollection.new_from_json @input }.should_not raise_error
|
45
|
+
%w{first second other}.should == @coll
|
46
|
+
@coll.continuation.should == 'examplecontinuation'
|
47
|
+
{'first' => %w{first}, 'second' => %w{second other}}.should == @coll.with_terms
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Riak::ListBuckets do
|
4
|
+
before :each do
|
5
|
+
@client = Riak::Client.new protocol: 'pbc'
|
6
|
+
@backend = mock 'backend'
|
7
|
+
@fake_pool = mock 'connection pool'
|
8
|
+
@fake_pool.stub(:take).and_yield(@backend)
|
9
|
+
|
10
|
+
@expect_list = @backend.should_receive(:list_buckets)
|
11
|
+
|
12
|
+
@client.instance_variable_set :@protobuffs_pool, @fake_pool
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "non-streaming" do
|
16
|
+
it 'should call the backend without a block' do
|
17
|
+
@expect_list.with({}).and_return(%w{a b c d})
|
18
|
+
|
19
|
+
@client.list_buckets
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "streaming" do
|
24
|
+
it 'should call the backend with a block' do
|
25
|
+
@expect_list.
|
26
|
+
and_yield(%w{abc abd abe}).
|
27
|
+
and_yield(%w{bbb ccc ddd})
|
28
|
+
|
29
|
+
@yielded = []
|
30
|
+
|
31
|
+
@client.list_buckets do |bucket|
|
32
|
+
@yielded << bucket
|
33
|
+
end
|
34
|
+
|
35
|
+
@yielded.each do |b|
|
36
|
+
b.should be_a Riak::Bucket
|
37
|
+
end
|
38
|
+
@yielded.map(&:name).should == %w{abc abd abe bbb ccc ddd}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|