distribustream 0.1.0 → 0.2.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.
data/lib/pdtp/server.rb CHANGED
@@ -8,361 +8,60 @@
8
8
  # See http://distribustream.rubyforge.org/
9
9
  #++
10
10
 
11
- require File.dirname(__FILE__) + '/common/protocol'
12
- require File.dirname(__FILE__) + '/common/common_init'
13
- require File.dirname(__FILE__) + '/server/file_service'
14
- require File.dirname(__FILE__) + '/server/client_info'
15
- require File.dirname(__FILE__) + '/server/transfer'
11
+ require 'rubygems'
12
+ require 'eventmachine'
13
+ require 'mongrel'
16
14
 
17
- require 'thread'
18
- require 'erb'
15
+ require File.dirname(__FILE__) + '/server/dispatcher'
16
+ require File.dirname(__FILE__) + '/server/file_service'
17
+ require File.dirname(__FILE__) + '/server/connection'
18
+ require File.dirname(__FILE__) + '/server/stats_handler'
19
19
 
20
20
  module PDTP
21
- # PDTP server implementation
21
+ # PDTP::Server provides an interface for creating a PDTP server
22
22
  class Server
23
- attr_reader :connections
24
- attr_accessor :file_service
25
- def initialize
26
- @connections = Array.new
27
- @stats_mutex=Mutex.new
28
- @used_client_ids=Hash.new #keeps a list of client ids in use, they must be unique
29
- @updated_clients=Hash.new #a set of clients that have been modified and need transfers spawned
30
- end
31
-
32
- #called by pdtp_protocol when a connection is created
33
- def connection_created(connection)
34
- @stats_mutex.synchronize do
35
- @@log.info "Client connected: #{connection.get_peer_info.inspect}"
36
- connection.user_data = ClientInfo.new
37
- @connections << connection
38
- end
39
- end
40
-
41
- #called by pdtp_protocol when a connection is destroyed
42
- def connection_destroyed(connection)
43
- @stats_mutex.synchronize do
44
- @@log.info "Client connection closed: #{connection.get_peer_info.inspect}"
45
- @connections.delete connection
46
- end
47
- end
48
-
49
- # returns the ClientInfo object associated with this connection
50
- def client_info(connection)
51
- connection.user_data ||= ClientInfo.new
52
- end
53
-
54
- # called when a transfer either finishes, successfully or not
55
- def transfer_completed(transfer,connection,chunk_hash,send_response=true)
56
- # did the transfer complete successfully?
57
- local_hash=@file_service.get_chunk_hash(transfer.url,transfer.chunkid)
58
-
59
- c1=client_info(transfer.taker)
60
- c2=client_info(transfer.giver)
61
-
62
- if connection==transfer.taker
63
- success= (chunk_hash==local_hash)
64
-
65
- if success
66
- #the taker now has the file, so he can provide it
67
- client_info(transfer.taker).chunk_info.provide(transfer.url,transfer.chunkid..transfer.chunkid)
68
- c1.trust.success(c2.trust)
69
- else
70
- #transfer failed, the client still wants the chunk
71
- client_info(transfer.taker).chunk_info.request(transfer.url,transfer.chunkid..transfer.chunkid)
72
- c1.trust.failure(c2.trust)
73
- end
74
-
75
- transfer.taker.send_message(:hash_verify,
76
- :url => transfer.url,
77
- :range => transfer.byte_range,
78
- :hash_ok => success
79
- ) if send_response
80
- end
81
-
82
- #outstr="#{@ids[transfer.giver]}->#{@ids[transfer.taker]} transfer completed: #{transfer}"
83
- #outstr=outstr+" t->g=#{c1.trust.weight(c2.trust)} g->t=#{c2.trust.weight(c1.trust)}"
84
- #outstr=outstr+"sent_by: "+ ( connection==transfer.taker ? "taker" : "giver" )
85
- #outstr=outstr+" success=#{success} "
86
- #@@log.debug(outstr)
87
-
88
- #remove this transfer from whoever sent it
89
- client_info(connection).transfers.delete(transfer.transfer_id)
90
- @updated_clients[connection]=true #flag this client for transfer creation
91
- end
92
-
93
- #Creates a new transfer between two peers
94
- #returns true on success, or false if the specified transfer is already in progress
95
- def begin_transfer(taker, giver, url, chunkid)
96
- byte_range = @file_service.get_info(url).chunk_range(chunkid)
97
- t = Transfer.new(taker, giver, url, chunkid, byte_range)
98
-
99
- #make sure this transfer doesnt already exist
100
- t1 = client_info(taker).transfers[t.transfer_id]
101
- t2 = client_info(giver).transfers[t.transfer_id]
102
- return false unless t1.nil? and t2.nil?
103
-
104
- client_info(taker).chunk_info.transfer(url, chunkid..chunkid)
105
- client_info(taker).transfers[t.transfer_id] = t
106
- client_info(giver).transfers[t.transfer_id] = t
107
-
108
- #send transfer message to the connector
109
- addr, port = t.acceptor.get_peer_info
23
+ # Create a new PDTP::Server which will listen on the given address and port
24
+ def initialize(addr, port = 6086)
25
+ @addr, @port = addr, port
110
26
 
111
- t.connector.send_message(:transfer,
112
- :host => addr,
113
- :port => t.acceptor.user_data.listen_port,
114
- :method => t.connector == t.taker ? "get" : "put",
115
- :url => url,
116
- :range => byte_range,
117
- :peer_id => client_info(t.acceptor).client_id
118
- )
119
- true
120
- end
121
-
122
- #this function removes all stalled transfers from the list
123
- #and spawns new transfers as appropriate
124
- #it must be called periodically by EventMachine
125
- def clear_all_stalled_transfers
126
- @connections.each { |connection| clear_stalled_transfers_for_client connection }
127
- spawn_all_transfers
128
- end
129
-
130
- #removes all stalled transfers that this client is a part of
131
- def clear_stalled_transfers_for_client(client_connection)
132
- client_info(client_connection).get_stalled_transfers.each do |transfer|
133
- transfer_completed transfer, client_connection, nil, false
134
- end
135
- end
136
-
137
- #spawns uploads and downloads for this client.
138
- #should be called every time there is a change that would affect
139
- #what this client has or wants
140
- def spawn_transfers_for_client(client_connection)
141
- info = client_info client_connection
142
-
143
- while info.wants_download? do
144
- break if spawn_download_for_client(client_connection) == false
145
- end
146
-
147
- while info.wants_upload? do
148
- break if spawn_upload_for_client(client_connection) == false
149
- end
150
- end
151
-
152
- #creates a single download for the specified client
153
- #returns true on success, false on failure
154
- def spawn_download_for_client(client_connection)
155
- feasible_peers=[]
156
-
157
- c1info=client_info(client_connection)
158
- begin
159
- url,chunkid=c1info.chunk_info.high_priority_chunk
160
- rescue
161
- return false
162
- end
163
-
164
- @connections.each do |c2|
165
- next if client_connection==c2
166
- next if client_info(c2).wants_upload? == false
167
- if client_info(c2).chunk_info.provided?(url,chunkid)
168
- feasible_peers << c2
169
- break if feasible_peers.size > 5
170
- end
171
- end
172
-
173
- # we now have a list of clients that have the requested chunk.
174
- # pick one and start the transfer
175
- if feasible_peers.size > 0
176
- #FIXME base this on the trust model
177
- giver=feasible_peers[rand(feasible_peers.size)]
178
- return begin_transfer(client_connection,giver,url,chunkid)
179
- #FIXME should we try again if begin_transfer fails?
180
- end
181
-
182
- false
183
- end
184
-
185
- #creates a single upload for the specified client
186
- #returns true on success, false on failure
187
- def spawn_upload_for_client(client_connection)
188
- c1info=client_info(client_connection)
189
-
190
- @connections.each do |c2|
191
- next if client_connection==c2
192
- next if client_info(c2).wants_download? == false
193
-
194
- begin
195
- url,chunkid=client_info(c2).chunk_info.high_priority_chunk
196
- rescue
197
- next
198
- end
199
-
200
- if c1info.chunk_info.provided?(url,chunkid)
201
- return begin_transfer(c2,client_connection,url,chunkid)
202
- end
203
- end
204
-
205
- false
206
- end
207
-
208
- #called by pdtp_protocol for each message that comes in from the wire
209
- def dispatch_message(command, message, connection)
210
- @stats_mutex.synchronize do
211
- dispatch_message_needslock command, message, connection
212
- end
213
- end
214
-
215
- #creates new transfers for all clients that have been updated
216
- def spawn_all_transfers
217
- while @updated_clients.size > 0 do
218
- tmp=@updated_clients
219
- @updated_clients=Hash.new
220
- tmp.each do |client,true_key|
221
- spawn_transfers_for_client(client)
222
- end
223
- end
224
- end
225
-
226
- #handles the request, provide, unrequest, unprovide messages
227
- def handle_requestprovide(connection,message)
228
- type=message["type"]
229
- url=message["url"]
230
- info=@file_service.get_info(url) rescue nil
231
- raise ProtocolWarn.new("Requested URL: '#{url}' not found") if info.nil?
232
-
233
- exclude_partial= (type=="provide") #only exclude partial chunks from provides
234
- range=info.chunk_range_from_byte_range(message["range"],exclude_partial)
235
-
236
- #call request, provide, unrequest, or unprovide
237
- client_info(connection).chunk_info.send( type.to_sym, url, range)
238
- @updated_clients[connection]=true #add to the list of client that need new transfers
239
- end
240
-
241
- #handles all incoming messages from clients
242
- def dispatch_message_needslock(command, message, connection)
243
- # store the command in the message hash
244
- message["type"] = command
27
+ @dispatcher = PDTP::Server::Dispatcher.new
28
+ @dispatcher.file_service = PDTP::Server::FileService.new
29
+ end
30
+
31
+ # Run a web server to display statistics on the given address and port
32
+ def enable_stats_service(addr = nil, port = 6087)
33
+ # Use the same address as the main server unless a different one was specified
34
+ addr ||= @addr
245
35
 
246
- #require the client to be logged in with a client id
247
- if command != "client_info" and client_info(connection).client_id.nil?
248
- raise ProtocolError.new("You need to send a 'client_info' message first")
249
- end
250
-
251
- case command
252
- when "client_info"
253
- cid = message["client_id"]
254
- #make sure this id isnt in use
255
- if @used_client_ids[cid]
256
- raise ProtocolError.new("Your client id: #{cid} is already in use.")
257
- end
258
-
259
- @used_client_ids[cid] = true
260
- client_info(connection).listen_port = message["listen_port"]
261
- client_info(connection).client_id = cid
262
- when "ask_info"
263
- info = file_service.get_info(message["url"])
264
- response = { :url => message["url"] }
265
- unless info.nil?
266
- response[:size] = info.file_size
267
- response[:chunk_size] = info.base_chunk_size
268
- response[:streaming] = info.streaming
269
- end
270
- connection.send_message :tell_info, response
271
- when "request", "provide", "unrequest", "unprovide"
272
- handle_requestprovide connection, message
273
- when "ask_verify"
274
- #check if the specified transfer is a real one
275
- my_id = client_info(connection).client_id
276
- transfer_id=Transfer.gen_transfer_id(my_id,message["peer_id"],message["url"],message["range"])
277
- ok = !!client_info(connection).transfers[transfer_id]
278
- client_info(connection).transfers[transfer_id].verification_asked=true if ok
279
- @@log.debug "AskVerify not ok: id=#{transfer_id}" unless ok
280
- connection.send_message(:tell_verify,
281
- :url => message["url"],
282
- :peer_id => message["peer_id"],
283
- :range => message["range"],
284
- :peer => message["peer"],
285
- :is_authorized=>ok
286
- )
287
- when "completed"
288
- my_id = client_info(connection).client_id
289
- transfer_id= Transfer::gen_transfer_id(
290
- my_id,message["peer_id"],
291
- message["url"],
292
- message["range"]
293
- )
294
- transfer=client_info(connection).transfers[transfer_id]
295
- @@log.debug("Completed: id=#{transfer_id} ok=#{transfer != nil}" )
296
- if transfer
297
- transfer_completed(transfer,connection,message["hash"])
298
- else
299
- raise ProtocolWarn.new("You sent me a transfer completed message for unknown transfer: #{transfer_id}")
36
+ @stats_server = Mongrel::HttpServer.new addr, port
37
+ @@log.info "Mongrel server listening on port: #{port}"
38
+ @stats_server.register '/', PDTP::Server::StatsHandler.new(@dispatcher)
39
+ @stats_server.run
40
+ end
41
+
42
+ # Serve files from the given directory
43
+ def enable_file_service(path, options = {})
44
+ opts = {
45
+ :chunk_size => 100000
46
+ }.merge(options)
47
+
48
+ @dispatcher.file_service.root = path
49
+ @dispatcher.file_service.default_chunk_size = opts[:chunk_size]
50
+ end
51
+
52
+ # Run the PDTP server event loop
53
+ def run
54
+ EventMachine::run do
55
+ EventMachine::start_server(@addr, @port, PDTP::Server::Connection) do |connection|
56
+ connection.server = @dispatcher
57
+ connection.connection_completed
300
58
  end
301
- when 'protocol_error', 'protocol_warn' #ignore
302
- else raise ProtocolError.new("Unhandled message type: #{command}")
303
- end
304
59
 
305
- spawn_all_transfers
306
- end
60
+ @@log.info "accepting connections with ev=#{EventMachine::VERSION}"
61
+ @@log.info "host=#{@host} port=#{@port}"
307
62
 
308
- #returns a string representing the specified connection
309
- def connection_name(c)
310
- #host,port=c.get_peer_info
311
- #return "#{get_id(c)}: #{host}:#{port}"
312
- client_info(c).client_id
313
- end
314
-
315
- def generate_html_stats
316
- @stats_mutex.synchronize { generate_html_stats_needslock }
317
- end
318
-
319
- #builds an html page with information about the server's internal workings
320
- def generate_html_stats_needslock
321
- s = ERB.new <<EOF
322
- <html><head><title>DistribuStream Statistics</title></head>
323
- <body>
324
- <h1>DistribuStream Statistics</h1>
325
- Time=<%= Time.new %><br> Connected Clients=<%= @connections.size %>
326
- <center><table border=1>
327
- <tr><th>Client</th><th>Transfers</th><th>Files</th></tr>
328
- <% @connections.each do |c| %>
329
- <tr><td>
330
- <% host, port = c.get_peer_info %>
331
- <%= connection_name(c) %><br><%= host %>:<%= port %>
332
- </td>
333
- <td>
334
- <%
335
- client_info(c).transfers.each do |key,t|
336
- if c==t.giver
337
- type="UP: "
338
- peer=t.taker
339
- else
340
- type="DOWN: "
341
- peer=t.giver
342
- end
343
- %>
344
- <%= type %> id=<%= t.transfer_id %><br>
345
- <%
346
- end
347
- %>
348
- </td>
349
- <td>
350
- <%
351
- client_info(c).chunk_info.get_file_stats.each do |fs|
352
- %>
353
- <%= fs.url %> size=<%= fs.file_chunks %> req=<%= fs.chunks_requested %>
354
- prov=<%= fs.chunks_provided %> transf=<%= fs.chunks_transferring %><br>
355
- <%
356
- end
357
- %>
358
- </td></tr>
359
- <%
360
- end
361
- %>
362
- </table>
363
- </body></html>
364
- EOF
365
- s.result binding
63
+ EventMachine::add_periodic_timer(2) { @dispatcher.clear_all_stalled_transfers }
64
+ end
366
65
  end
367
66
  end
368
67
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: distribustream
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.1.0
7
- date: 2008-10-11 00:00:00 -06:00
6
+ version: 0.2.0
7
+ date: 2008-10-22 00:00:00 -06:00
8
8
  summary: DistribuStream is a fully open peercasting system allowing on-demand or live streaming media to be delivered at a fraction of the normal cost
9
9
  require_paths:
10
10
  - lib
@@ -33,30 +33,33 @@ authors:
33
33
  - James Sanders
34
34
  - Tom Stapleton
35
35
  files:
36
- - bin/distribustream
37
36
  - bin/dsclient
38
37
  - bin/dsseed
38
+ - bin/dstream
39
39
  - lib/pdtp
40
40
  - lib/pdtp/client
41
41
  - lib/pdtp/client.rb
42
42
  - lib/pdtp/common
43
43
  - lib/pdtp/server
44
44
  - lib/pdtp/server.rb
45
+ - lib/pdtp/client/callbacks.rb
46
+ - lib/pdtp/client/connection.rb
45
47
  - lib/pdtp/client/file_buffer.rb
46
- - lib/pdtp/client/file_buffer_spec.rb
47
48
  - lib/pdtp/client/file_service.rb
48
- - lib/pdtp/client/protocol.rb
49
+ - lib/pdtp/client/http_handler.rb
49
50
  - lib/pdtp/client/transfer.rb
50
51
  - lib/pdtp/common/common_init.rb
51
52
  - lib/pdtp/common/file_service.rb
52
- - lib/pdtp/common/file_service_spec.rb
53
+ - lib/pdtp/common/packet.rb
53
54
  - lib/pdtp/common/protocol.rb
54
- - lib/pdtp/common/protocol_spec.rb
55
55
  - lib/pdtp/server/client_info.rb
56
+ - lib/pdtp/server/connection.rb
57
+ - lib/pdtp/server/dispatcher.rb
56
58
  - lib/pdtp/server/file_service.rb
59
+ - lib/pdtp/server/file_service_protocol.rb
60
+ - lib/pdtp/server/stats_handler.rb
57
61
  - lib/pdtp/server/transfer.rb
58
62
  - lib/pdtp/server/trust.rb
59
- - lib/pdtp/server/trust_spec.rb
60
63
  - conf/bigchunk.yml
61
64
  - conf/debug.yml
62
65
  - conf/example.yml
@@ -77,7 +80,7 @@ extra_rdoc_files:
77
80
  - README
78
81
  - CHANGES
79
82
  executables:
80
- - distribustream
83
+ - dstream
81
84
  - dsseed
82
85
  - dsclient
83
86
  extensions: []
data/bin/distribustream DELETED
@@ -1,60 +0,0 @@
1
- #!/usr/bin/env ruby
2
- #--
3
- # Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)
4
- # All rights reserved. See COPYING for permissions.
5
- #
6
- # This source file is distributed as part of the
7
- # DistribuStream file transfer system.
8
- #
9
- # See http://distribustream.rubyforge.org/
10
- #++
11
-
12
- require 'rubygems'
13
- require 'eventmachine'
14
- require 'optparse'
15
- require 'logger'
16
- require 'mongrel'
17
-
18
- require File.dirname(__FILE__) + '/../lib/pdtp/server'
19
-
20
- common_init $0
21
-
22
- server = PDTP::Server.new
23
- server.file_service = PDTP::Server::FileService.new
24
- PDTP::Protocol.listener = server
25
-
26
- #set up the mongrel server for serving the stats page
27
- class MongrelServerHandler< Mongrel::HttpHandler
28
- def initialize(server)
29
- @server = server
30
- end
31
-
32
- def process(request,response)
33
- response.start(200) do |head, out|
34
- out.write begin
35
- outstr = @server.generate_html_stats
36
- rescue Exception=>e
37
- outstr = "Exception: #{e}\n#{e.backtrace.join("\n")}"
38
- end
39
- end
40
- end
41
- end
42
-
43
- #run the mongrel server
44
- mongrel_server = Mongrel::HttpServer.new '0.0.0.0', @@config[:port] + 1
45
- @@log.info "Mongrel server listening on port: #{@@config[:port] + 1}"
46
- mongrel_server.register '/', MongrelServerHandler.new(server)
47
- mongrel_server.run
48
-
49
- #set root directory
50
- server.file_service.root = @@config[:file_root]
51
- server.file_service.default_chunk_size = @@config[:chunk_size]
52
-
53
- EventMachine::run do
54
- host, port = '0.0.0.0', @@config[:port]
55
- EventMachine::start_server host, port, PDTP::Protocol
56
- @@log.info "accepting connections with ev=#{EventMachine::VERSION}"
57
- @@log.info "host=#{host} port=#{port}"
58
-
59
- EventMachine::add_periodic_timer(2) { server.clear_all_stalled_transfers }
60
- end
@@ -1,154 +0,0 @@
1
- #--
2
- # Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)
3
- # All rights reserved. See COPYING for permissions.
4
- #
5
- # This source file is distributed as part of the
6
- # DistribuStream file transfer system.
7
- #
8
- # See http://distribustream.rubyforge.org/
9
- #++
10
-
11
- require File.dirname(__FILE__) + '/file_buffer'
12
-
13
- describe PDTP::FileBuffer do
14
- before(:each) do
15
- @b = PDTP::FileBuffer.new
16
- end
17
-
18
- it "returns nil if read when empty" do
19
- @b.bytes_stored.should == 0
20
- @b.read(0..1).should == nil
21
- end
22
- end
23
-
24
- describe PDTP::FileBuffer, "with one entry" do
25
- before(:each) do
26
- @b = PDTP::FileBuffer.new
27
- @b.write 0, 'hello'
28
- end
29
-
30
- it "calculates bytes stored correctly" do
31
- @b.bytes_stored.should == 5
32
- end
33
-
34
- it "reads stored data correctly" do
35
- @b.read(0..4).should == "hello"
36
- @b.read(1..1).should == "e"
37
- @b.read(-1..2).should == nil
38
- @b.read(0..5).should == nil
39
- end
40
- end
41
-
42
- describe PDTP::FileBuffer, "with two overlapping entries" do
43
- before(:each) do
44
- @b = PDTP::FileBuffer.new
45
- @b.write(3,"hello")
46
- @b.write(7,"World")
47
- end
48
-
49
- it "calculates bytes stored correctly" do
50
- @b.bytes_stored.should == 9
51
- end
52
-
53
- it "reads stored data correctly" do
54
- @b.read(3..12).should == nil
55
- @b.read(3..11).should == "hellWorld"
56
- @b.read(3..1).should == nil
57
- @b.read(2..4).should == nil
58
- end
59
-
60
- end
61
-
62
- describe PDTP::FileBuffer, "with three overlapping entries" do
63
- before(:each) do
64
- @b = PDTP::FileBuffer.new
65
- @b.write(3,"hello")
66
- @b.write(7,"World")
67
- @b.write(2,"123456789ABCDEF")
68
- end
69
-
70
- it "calculates bytes stored correctly" do
71
- @b.bytes_stored.should == 15
72
- end
73
-
74
- it "reads stored data correctly" do
75
- @b.read(2..16).should == "123456789ABCDEF"
76
- @b.read(2..17).should == nil
77
- end
78
- end
79
-
80
- describe PDTP::FileBuffer, "with two tangential entries" do
81
- before(:each) do
82
- @b = PDTP::FileBuffer.new
83
- @b.write(3,"hello")
84
- @b.write(8,"World")
85
- end
86
-
87
- it "calculates bytes stored correctly" do
88
- @b.bytes_stored.should == 10
89
- end
90
-
91
- it "reads stored data correctly" do
92
- @b.read(3..12).should == "helloWorld"
93
- end
94
- end
95
-
96
- describe PDTP::FileBuffer, "with a chain of overlapping entries" do
97
- before(:each) do
98
- @b = PDTP::FileBuffer.new
99
- @b.write(3,"a123")
100
- @b.write(4,"b4")
101
- @b.write(0,"012c")
102
-
103
- #___a123
104
- #___ab43
105
- #012cb43
106
-
107
- end
108
-
109
- it "calculates bytes stored correctly" do
110
- @b.bytes_stored.should == 7
111
- end
112
-
113
- it "reads stored data correctly" do
114
- @b.read(0..6).should == "012cb43"
115
- @b.read(3..6).should == "cb43"
116
- end
117
- end
118
-
119
- describe PDTP::FileBuffer, "with an associated IO object" do
120
- before(:each) do
121
- @io = mock(:io)
122
- @b = PDTP::FileBuffer.new @io
123
- end
124
-
125
- it "writes received data to the IO object" do
126
- @io.should_receive(:write).once.with('foo').and_return(3)
127
- @b.write(0, "foo")
128
- end
129
-
130
- it "writes successively received data to the IO object" do
131
- @io.should_receive(:write).once.with('foo').and_return(3)
132
- @b.write(0, "foo")
133
-
134
- @io.should_receive(:write).once.with('bar').and_return(3)
135
- @b.write(3, "bar")
136
-
137
- @io.should_receive(:write).once.with('baz').and_return(3)
138
- @b.write(6, "baz")
139
- end
140
-
141
- it "reassembles single-byte out-of-order data and writes it to the IO object" do
142
- @io.should_receive(:write).once.with('bar').and_return(3)
143
- @b.write(1, 'a')
144
- @b.write(2, 'r')
145
- @b.write(0, 'b')
146
- end
147
-
148
- it "reassembles multibyte out-of-order data and writes it to the IO object" do
149
- @io.should_receive(:write).once.with('foobar').and_return(6)
150
- @b.write(2, 'ob')
151
- @b.write(4, 'ar')
152
- @b.write(0, 'fo')
153
- end
154
- end