commute 0.2.0.rc.2 → 0.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.todo +28 -12
  2. data/README.md +0 -1
  3. data/commute.gemspec +4 -5
  4. data/lib/commute/common/basic_auth.rb +10 -9
  5. data/lib/commute/common/caching.rb +208 -0
  6. data/lib/commute/common/chemicals.rb +47 -24
  7. data/lib/commute/common/eventmachine.rb +68 -0
  8. data/lib/commute/common/synchrony.rb +42 -0
  9. data/lib/commute/common/typhoeus.rb +64 -0
  10. data/lib/commute/core/api.rb +42 -29
  11. data/lib/commute/core/builder.rb +4 -15
  12. data/lib/commute/core/context.rb +156 -15
  13. data/lib/commute/core/http.rb +124 -0
  14. data/lib/commute/core/layer.rb +187 -0
  15. data/lib/commute/core/sequence.rb +83 -132
  16. data/lib/commute/core/stack.rb +63 -72
  17. data/lib/commute/core/status.rb +45 -0
  18. data/lib/commute/core/util/event_emitter.rb +58 -0
  19. data/lib/commute/core/util/path.rb +37 -0
  20. data/lib/commute/core/util/stream.rb +141 -0
  21. data/lib/commute/extensions/crud.rb +88 -0
  22. data/lib/commute/extensions/param.rb +20 -0
  23. data/lib/commute/extensions/url.rb +53 -0
  24. data/lib/commute/version.rb +1 -1
  25. data/spec/commute/common/caching_spec.rb +158 -0
  26. data/spec/commute/common/eventmachine_spec.rb +74 -0
  27. data/spec/commute/common/typhoeus_spec.rb +67 -0
  28. data/spec/commute/core/api_spec.rb +3 -1
  29. data/spec/commute/core/builder_spec.rb +8 -8
  30. data/spec/commute/core/http_spec.rb +39 -0
  31. data/spec/commute/core/layer_spec.rb +81 -0
  32. data/spec/commute/core/sequence_spec.rb +36 -150
  33. data/spec/commute/core/stack_spec.rb +33 -83
  34. data/spec/commute/core/util/event_emitter_spec.rb +35 -0
  35. data/spec/commute/core/util/path_spec.rb +29 -0
  36. data/spec/commute/core/util/stream_spec.rb +90 -0
  37. data/spec/commute/extensions/url_spec.rb +76 -0
  38. data/spec/spec_helper.rb +3 -1
  39. metadata +61 -48
  40. data/examples/gist_api.rb +0 -71
  41. data/examples/highrise_task_api.rb +0 -59
  42. data/examples/pastie_api.rb +0 -18
  43. data/lib/commute/aspects/caching.rb +0 -37
  44. data/lib/commute/aspects/crud.rb +0 -41
  45. data/lib/commute/aspects/pagination.rb +0 -16
  46. data/lib/commute/aspects/url.rb +0 -57
  47. data/lib/commute/common/cache.rb +0 -43
  48. data/lib/commute/common/conditional.rb +0 -27
  49. data/lib/commute/common/em-synchrony_adapter.rb +0 -29
  50. data/lib/commute/common/em_http_request_adapter.rb +0 -57
  51. data/lib/commute/common/typhoeus_adapter.rb +0 -40
  52. data/lib/commute/common/xml.rb +0 -7
  53. data/lib/commute/core/commuter.rb +0 -116
  54. data/lib/commute/core/processors/code_status_processor.rb +0 -40
  55. data/lib/commute/core/processors/hook.rb +0 -14
  56. data/lib/commute/core/processors/request_builder.rb +0 -26
  57. data/lib/commute/core/processors/sequencer.rb +0 -46
  58. data/lib/commute/core/request.rb +0 -58
  59. data/lib/commute/core/response.rb +0 -18
  60. data/spec/commute/aspects/caching_spec.rb +0 -12
  61. data/spec/commute/aspects/url_spec.rb +0 -61
  62. data/spec/commute/core/commuter_spec.rb +0 -64
  63. data/spec/commute/core/processors/code_status_processor_spec.rb +0 -5
  64. data/spec/commute/core/processors/hook_spec.rb +0 -25
  65. data/spec/commute/core/processors/request_builder_spec.rb +0 -25
  66. data/spec/commute/core/processors/sequencer_spec.rb +0 -33
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ require 'commute/core/http'
4
+
5
+ describe Commute::Http::Status do
6
+
7
+ Status = Commute::Http::Status
8
+
9
+ it 'should be a failure for 4xx and 5xx status codes' do
10
+ Status.new(420).fail?.must_equal true
11
+ Status.new(501).fail?.must_equal true
12
+ Status.new(501).success?.must_equal false
13
+ end
14
+
15
+ it 'should be a success otherwise' do
16
+ Status.new(200).success?.must_equal true
17
+ Status.new(200).fail?.must_equal false
18
+ Status.new(301).success?.must_equal true
19
+ end
20
+ end
21
+
22
+ describe Commute::Http::Request do
23
+
24
+ it 'should have some default values' do
25
+ request = Commute::Http::Request.new nil
26
+ request.uri.path.must_equal '/'
27
+ request.headers.must_be_empty
28
+ request.query.must_be_empty
29
+ request.method.must_equal :get
30
+ end
31
+
32
+ it 'should keep uri#query consistent' do
33
+ request = Commute::Http::Request.new nil
34
+ request.query[:limit] = 10
35
+ request.uri.query.must_equal 'limit%3D10'
36
+ request.query[:index] = 20
37
+ request.uri.query.must_equal 'limit%3D10%26index%3D20'
38
+ end
39
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ require 'commute/core/builder'
4
+ require 'commute/core/http'
5
+ require 'commute/core/util/path'
6
+ require 'commute/core/layer'
7
+
8
+ describe Commute::Layer do
9
+
10
+ it 'should have a name' do
11
+ layer = Commute::Layer.new proc {}, :test
12
+ layer.name.must_equal :test
13
+ end
14
+
15
+ it 'should figure out the name is none was given' do
16
+ # With an instance var.
17
+ klass = Class.new do
18
+ @name = :test
19
+ end
20
+ layer = Commute::Layer.new klass.new
21
+ layer.name.must_equal :test
22
+ # With a method.
23
+ klass = Class.new do
24
+ def name; :test; end
25
+ end
26
+ layer = Commute::Layer.new klass.new
27
+ layer.name.must_equal :test
28
+ end
29
+
30
+ describe '#call' do
31
+
32
+ it 'should auto-add context options to the options hash' do
33
+ context = Commute::Context.new(nil).with auth: { key: 'secret' }
34
+ request = Commute::Layer::Request.new Commute::Http::Request.new(context)
35
+ callable = mock
36
+ router = mock
37
+ callable.expects(:call).with(router, request, key: 'secret')
38
+ Commute::Layer.new(callable, :auth).call router, request
39
+ end
40
+ end
41
+
42
+ describe '#callable' do
43
+ let(:context) { Commute::Context.new(nil) }
44
+
45
+ it 'should return false if the layer is disabled' do
46
+ Commute::Layer.new(proc {}, :test).callable?(context.disable(:test).context).must_equal false
47
+ end
48
+
49
+ it 'should return false if the layer has no callable' do
50
+ Commute::Layer.new(nil, :test).callable?(context).must_equal false
51
+ end
52
+
53
+ it 'should return true otherwise' do
54
+ Commute::Layer.new(proc {}, :test).callable?(context).must_equal true
55
+ end
56
+ end
57
+ end
58
+
59
+ describe Commute::Layer::Communication do
60
+
61
+ it 'should hold an underlaying http request/response' do
62
+ http = Commute::Http::Request.new(nil)
63
+ Commute::Layer::Request.new(http).http.must_equal http
64
+ end
65
+ end
66
+
67
+ describe Commute::Layer::Request do
68
+
69
+ it 'should be respondable' do
70
+ receiver = proc {}
71
+ http_request = Commute::Http::Request.new(nil)
72
+ request = Commute::Layer::Request.new(http_request)
73
+ http_response = Commute::Http::Response.new(request)
74
+ status = Commute::Status.ok
75
+ request.on(:response, &receiver)
76
+ receiver.expects(:call).with do |response, _status|
77
+ response.http == http_response && _status == status
78
+ end
79
+ request.respond http_response, status
80
+ end
81
+ end
@@ -3,188 +3,74 @@ require 'commute/core/sequence'
3
3
 
4
4
  describe Commute::Sequence do
5
5
 
6
- let(:sequence) { Commute::Sequence.new }
6
+ let(:sequence) { Commute::Sequence.new(:test) }
7
7
 
8
- let(:processor1) {
9
- lambda { |result, subject, options = {}| result + 1 }
10
- }
11
-
12
- let(:processor2) {
13
- lambda { |result, subject, options = {}| result * 2 }
14
- }
15
-
16
- let(:processor3) {
17
- Class.new {
18
- @id = :test
19
-
20
- def call result, subject, options = {}
21
- result ** options[:power]
22
- end
23
- }.new
24
- }
8
+ it 'should have a name' do
9
+ sequence.name.must_equal :test
10
+ end
25
11
 
26
12
  describe '#new' do
13
+ it 'should be empty' do
14
+ sequence.size.must_equal 0
15
+ end
16
+
27
17
  it 'should alter the sequence when a block is provided' do
28
- sequence = Commute::Sequence.new do |s|
29
- s.append processor1
30
- end
31
- sequence.processors.size.must_equal 1
18
+ Commute::Sequence.new(:test) { |s|
19
+ s.append proc {}
20
+ }.size.must_equal 1
21
+ Commute::Sequence.new(:test) {
22
+ append proc {}
23
+ append proc {}
24
+ }.size.must_equal 2
32
25
  end
33
26
  end
34
27
 
35
28
  describe '#alter' do
36
29
  it 'should alter the sequence in a block when the arity is 1' do
37
- sequence.alter do |s|
38
- s.append processor1
39
- end
40
- sequence.processors.size.must_equal 1
30
+ sequence.alter { |s|
31
+ s.append proc {}
32
+ }.size.must_equal 1
41
33
  end
42
34
 
43
35
  it 'should evaluate the block when the arity is 0' do
44
- sequence.alter do
45
- append Proc.new { |n| n.data += 1 }
46
- end
47
- sequence.processors.size.must_equal 1
48
- end
49
- end
50
-
51
- describe '#processors' do
52
- it 'should return an empty array when there are no processors' do
53
- sequence.processors.must_equal []
54
- end
55
-
56
- it 'should not return a modifiable array' do
57
- sequence.processors << 'test'
58
- sequence.processors.must_be_empty
59
- end
60
- end
61
-
62
- describe '#processors=' do
63
-
64
- it 'should not exist' do
65
- Proc.new { sequence.processors = nil }.must_raise NoMethodError
66
- end
67
- end
68
-
69
- describe '#call' do
70
- let(:processor1) do
71
- Proc.new do |commuter|
72
- commuter.change { |n| n+1 }
73
- end
74
- end
75
-
76
- let(:processor2) do
77
- Proc.new do |commuter, operation|
78
- commuter.change { |n| n.send operation, n }
79
- end
80
- end
81
-
82
- let(:commuter) { Commute::Commuter.new mock, 1 }
83
-
84
- it 'should call all processors' do
85
- commuter.expects(:parameters).with(nil).never
86
- commuter.expects(:parameters).with(:operator).returns(:**)
87
- sequence.append(processor1)
88
- sequence.append(processor2, as: :operator)
89
- sequence.call commuter
90
- commuter.get.must_equal 4
91
- end
92
-
93
- # This is not tested implicitely due to the Proc usage.
94
- it 'should call processors with the correct number of arguments' do
95
- commuter.expects(:parameters).with(:operator).returns(:+)
96
- processor1 = mock
97
- processor1.expects(:call).with(commuter)
98
- processor2 = mock
99
- processor2.expects(:call).with(commuter, :+)
100
- sequence.append processor1
101
- sequence.append processor2, as: :operator
102
- sequence.call commuter
103
- end
104
-
105
- it 'should be a processor itself' do
106
- commuter.expects(:parameters).with(:operator).returns(:**)
107
-
108
- subsequence = Commute::Sequence.new do |s|
109
- s.append processor1
110
- end
111
-
112
- sequence = Commute::Sequence.new do |s|
113
- s.append subsequence
114
- s.append processor2, as: :operator
115
- end
116
-
117
- sequence.call commuter
118
- commuter.get.must_equal 4
119
- end
120
-
121
- it 'should return nothing' do
122
- Commute::Sequence.new { |s|
123
- s.append processor1
124
- }.call(commuter).must_equal nil
36
+ sequence.alter {
37
+ append proc {}
38
+ }.size.must_equal 1
125
39
  end
126
40
  end
127
41
 
128
42
  describe '#append' do
129
43
  it 'should append the processor to the tail' do
130
- sequence.append processor1
131
- sequence.processors.must_equal [processor1]
132
- assert_equal sequence.append(processor2), processor2
133
- sequence.processors.must_equal [processor1, processor2]
134
- end
135
-
136
- it 'should be able to override the default processor id' do
137
- sequence.append processor1, as: :increment
138
- sequence.at(:increment).call(1, nil).must_equal 2
139
-
140
- sequence.append processor3, as: :power
141
- sequence.at(:power).must_equal processor3
44
+ layer1 = sequence.append proc {}
45
+ sequence.must_equal [layer1]
46
+ layer2 = sequence.append proc {}
47
+ sequence.must_equal [layer1, layer2]
142
48
  end
143
49
 
144
50
  it 'should be able to append after a specified processor' do
145
- sequence.append processor1, as: :increment
146
- sequence.append processor2
51
+ layer1 = sequence.append proc {}, as: :action1
52
+ layer3 = sequence.append proc {}
147
53
 
148
- sequence.append processor3, as: :power, after: :increment
149
- sequence.processors.must_equal [processor1, processor3, processor2]
54
+ layer2 = sequence.append proc {}, as: :action2, after: :action1
55
+ sequence.must_equal [layer1, layer2, layer3]
150
56
  end
151
57
  end
152
58
 
153
59
  describe '#remove' do
154
60
  before do
155
- sequence.append processor1
156
- sequence.append processor2, as: :multiplier
157
- end
158
-
159
- it 'should remove the processor if the processor itself is given' do
160
- processor = sequence.remove processor1
161
- (processor == processor1).must_equal true
162
- sequence.processors.must_equal [processor2]
163
- assert_equal sequence.at(:multiplier), processor2
61
+ @layer1 = sequence.append proc {}
62
+ @layer2 = sequence.append proc {}, as: :action
164
63
  end
165
64
 
166
65
  it 'should remove the processor if its id is given' do
167
- processor = sequence.remove :multiplier
168
- (processor == processor2).must_equal true
169
- sequence.processors.must_equal [processor1]
170
- sequence.at(:multiplier).must_be_nil
66
+ sequence.remove(:action).must_equal @layer2
67
+ sequence.must_equal [@layer1]
171
68
  end
172
69
  end
173
70
 
174
- describe '#at' do
175
- it 'should index processors with a default id' do
176
- sequence.append processor3
177
- sequence.at(:test).must_equal processor3
178
- end
179
- end
180
-
181
- describe '#dup' do
182
- it 'should duplicate the sequence' do
183
- sequence.append processor1
184
- cloned = sequence.dup
185
- cloned.append processor2
186
- sequence.processors.size.must_equal 1
187
- cloned.processors.size.must_equal 2
71
+ describe '#call' do
72
+ it 'should lazily push the sequence on the path' do
73
+ skip
188
74
  end
189
75
  end
190
76
  end
@@ -4,93 +4,43 @@ require 'commute/core/stack'
4
4
 
5
5
  describe Commute::Stack do
6
6
 
7
- let(:sequence) do
8
- Commute::Sequence.new do
9
- append Proc.new { |n| n.data += 1 }
10
- end
11
- end
12
-
13
- let(:stack) do
14
- Commute::Stack.new do |stack, main|
15
- end
16
- end
17
-
18
- describe '#initialize' do
19
- describe 'when no sequence is given' do
20
- it 'should yield the main sequence for the stack when the arity is 1' do
21
- stack = Commute::Stack.new do |sequence|
22
- sequence.append Proc.new {}
7
+ let(:stack) { Commute::Stack.new }
8
+
9
+ # A Layer that forwards to the next.
10
+ let(:forward_layer) { proc { |router, request, options = {}|
11
+ request.pipe router.call(request.http) { |response, status|
12
+ response.pipe request.respond(response.http, status)
13
+ }
14
+ }}
15
+
16
+ # A layer that echoes all data.
17
+ let(:echo_layer) { proc { |router, request, options = {}|
18
+ request.pipe request.respond(Commute::Http::Response.new(request), Commute::Status.ok)
19
+ }}
20
+
21
+ describe '#call' do
22
+ describe 'with a forward and echo layer' do
23
+ it 'should echo all data' do
24
+ # Append a forward and echo layer to the main sequence.
25
+ stack.sequence do |s|
26
+ s.append forward_layer
27
+ s.append forward_layer
28
+ s.append stack.sequence(:echo)
23
29
  end
24
- stack.sequence.processors.size.must_equal 1
25
- end
26
-
27
- it 'should yield the stack and the main sequence when the arity is 2' do
28
- stack = Commute::Stack.new do |stack, sequence|
29
- sequence.append Proc.new {}
30
+ stack.sequence(:echo) do |s|
31
+ s.append echo_layer
30
32
  end
31
- stack.sequence.processors.size.must_equal 1
32
- end
33
- end
34
-
35
- describe 'when a sequence is given' do
36
- it 'should use that sequence' do
37
- sequence = Commute::Sequence.new do
38
- append Proc.new {}
39
- end
40
- stack = Commute::Stack.new sequence
41
- stack.sequence.processors.size.must_equal 1
42
- end
43
- end
44
- end
45
-
46
- describe '#add' do
47
- it 'should add a sequence to the stack by name' do
48
- stack.add sequence, :calculation
49
- stack.sequence(:calculation).must_equal sequence
50
- end
51
- end
52
-
53
- describe '#sequence' do
54
- it 'should add the sequence when it was not found' do
55
- stack.sequence(:calculation) do
56
- append Proc.new { |c| c.set(c.get + 1) }
57
- end
58
-
59
- stack.sequence(:calculation).processors.size.must_equal 1
60
- end
61
- end
62
-
63
- describe '#sequencer' do
64
- it 'should allow us to embed a sequence in a sequence' do
65
- stack = Commute::Stack.new do |stack, sequence|
66
- sequence.append Commute::Sequencer.new(:calculation)
67
- sequence.append Proc.new { |c|
68
- c.change { |n| "The result is #{n}" }
69
- }
70
-
71
- stack.sequence(:calculation) do
72
- append Proc.new { |c| c.set(c.get + 1) }
73
- append Proc.new { |c| c.set(c.get + 2) }
33
+ # Call the stack.
34
+ context = Commute::Context.new stack
35
+ request = stack.call Commute::Http::Request.new(context) do |response, status|
36
+ puts 'response!'
37
+ response.on(:data) { |chunk| puts chunk }
38
+ response.on(:end) { puts 'end!'}
74
39
  end
40
+ request.write 1
41
+ request.write 2
42
+ request.end
75
43
  end
76
- commuter = Commute::Commuter.new(Commute::Context.new(stack), 1)
77
- stack.call(commuter)
78
- commuter.get.must_equal 'The result is 4'
79
- end
80
- end
81
-
82
- describe '#dup' do
83
- it 'should duplicate the stack and all its sequences' do
84
- stack.add sequence, :calculation
85
- cloned = stack.dup.alter do
86
- sequence.append Proc.new {}
87
-
88
- sequence(:calculation).append Proc.new {}
89
- end
90
- stack.sequence.processors.size.must_equal 0
91
- stack.sequence(:calculation).processors.size.must_equal 1
92
- cloned.sequence.processors.size.must_equal 1
93
- cloned.sequence(:calculation).processors.size.must_equal 2
94
44
  end
95
45
  end
96
46
  end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ require 'commute/core/util/event_emitter'
4
+
5
+ describe Commute::EventEmitter do
6
+
7
+ let(:emitter) {
8
+ Class.new { include Commute::EventEmitter }.new
9
+ }
10
+
11
+ let(:handler1) { proc {} }
12
+ let(:handler2) { proc {} }
13
+ let(:handler3) { proc {} }
14
+
15
+ it 'should notify event handlers for one event' do
16
+ [handler1, handler2].each do |handler|
17
+ handler.expects(:call).with :test, 1
18
+ end
19
+ emitter.on :data, &handler1
20
+ emitter.on :data, &handler2
21
+ emitter.send :emit, :data, :test, 1
22
+ end
23
+
24
+ it 'should notify event handlers for multiple events' do
25
+ [handler1, handler2].each do |handler|
26
+ handler.expects(:call).with(:test, 1).once
27
+ end
28
+ handler3.expects(:call).once
29
+ emitter.on :data, &handler1
30
+ emitter.on :data, &handler2
31
+ emitter.on :end, &handler3
32
+ emitter.send :emit, :data, :test, 1
33
+ emitter.send :emit, :end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'commute/core/util/path'
3
+
4
+ describe Commute::Path do
5
+
6
+ Path = Commute::Path
7
+
8
+ describe 'with a single route' do
9
+
10
+ it 'should walk the entire route' do
11
+ path = Path.new([1,2,3].each)
12
+ [path.succ, path.succ, path.succ].must_equal [1,2,3]
13
+ end
14
+ end
15
+
16
+ describe 'with one detour' do
17
+
18
+ it 'should walk the main route, the detour and the rest of the main route' do
19
+ router = Path.new([1,2,3].each)
20
+ router.succ.must_equal 1
21
+ router.succ.must_equal 2
22
+ router << ['a', 'b'].each
23
+ router.succ.must_equal 'a'
24
+ router.succ.must_equal 'b'
25
+ router.succ.must_equal 3
26
+ router.succ.must_equal nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ require 'commute/core/util/stream'
4
+
5
+ describe Commute::Stream do
6
+
7
+ let(:stream) {
8
+ Class.new { include Commute::Stream }.new
9
+ }
10
+
11
+ let(:handler) { proc {} }
12
+
13
+ describe '#new' do
14
+ it 'should be writeable' do
15
+ stream.writeable?.must_equal true
16
+ end
17
+ end
18
+
19
+ describe '#write' do
20
+ it 'should write data to the stream' do
21
+ handler.expects(:call).twice
22
+ stream.on :data, &handler
23
+ stream.write 1
24
+ stream.write 2
25
+ stream.writeable?.must_equal true
26
+ end
27
+ end
28
+
29
+ describe '#end' do
30
+ it 'should end the stream' do
31
+ data_handler = proc {}
32
+ end_handler = proc {}
33
+ stream.on :data, &data_handler
34
+ stream.on :end, &end_handler
35
+ data_handler.expects(:call).twice
36
+ end_handler.expects(:call).once
37
+ stream.write 1
38
+ stream.write 2
39
+ stream.end
40
+ stream.write(3).must_equal nil
41
+ stream.writeable?.must_equal false
42
+ end
43
+ end
44
+
45
+ describe '#buffer' do
46
+ it 'should buffer all stream data when a buffer is given' do
47
+ buffer = []
48
+ stream >> buffer
49
+ 0.upto(3) { |i| stream.write i }
50
+ stream.end
51
+ buffer.must_equal [0,1,2,3]
52
+ end
53
+
54
+ it 'should buffer all stream data when no buffer is given' do
55
+ stream.buffer do |buffer|
56
+ buffer.must_equal [0,1,2,3]
57
+ end
58
+ 0.upto(3) { |i| stream.write i }
59
+ end
60
+ end
61
+
62
+ describe '#pipe' do
63
+ it 'should pipe the stream to an other stream' do
64
+ destination = mock
65
+ streaming = sequence 'streaming'
66
+ destination.expects(:write).with(1).in_sequence streaming
67
+ destination.expects(:write).with(2).in_sequence streaming
68
+ destination.expects(:end).in_sequence streaming
69
+ stream.pipe destination
70
+ stream.write 1
71
+ stream.write 2
72
+ stream.end
73
+ end
74
+ end
75
+
76
+ describe '#map' do
77
+ it 'should map all data in a stream' do
78
+ buffer = []
79
+ output = stream.
80
+ map { |i| i*2 }.
81
+ reject { |i| i % 3 == 0 }.
82
+ inject(0, &:+) >> buffer
83
+ 1.upto(5) do |i|
84
+ stream.write i
85
+ end
86
+ stream.end
87
+ buffer.must_equal [24]
88
+ end
89
+ end
90
+ end