io_unblock 0.1.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/.autotest +18 -0
- data/.gitignore +7 -0
- data/Gemfile +15 -0
- data/README.md +10 -0
- data/Rakefile +16 -0
- data/io_unblock.gemspec +23 -0
- data/lib/io_unblock.rb +10 -0
- data/lib/io_unblock/buffer.rb +50 -0
- data/lib/io_unblock/delegation.rb +91 -0
- data/lib/io_unblock/stream.rb +158 -0
- data/lib/io_unblock/version.rb +3 -0
- data/spec/io_unblock/buffer_spec.rb +46 -0
- data/spec/io_unblock/delegation_spec.rb +88 -0
- data/spec/io_unblock/stream_spec.rb +199 -0
- data/spec/spec_helper.rb +81 -0
- metadata +87 -0
data/.autotest
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# vim: set filetype=ruby :
|
2
|
+
|
3
|
+
require 'autotest/restart'
|
4
|
+
require 'autotest/growl'
|
5
|
+
|
6
|
+
Autotest.add_hook :initialize do |at|
|
7
|
+
at.testlib = 'minitest/spec'
|
8
|
+
|
9
|
+
at.clear_mappings
|
10
|
+
at.add_mapping(/^lib\/(.*)\.rb$/) { |f, m| "spec/#{m[1]}_spec.rb" }
|
11
|
+
at.add_mapping(/^spec\/.*_spec\.rb$/) { |f,_| f }
|
12
|
+
at.add_mapping(/^spec\/spec_helper\.rb$/) { |f,_|
|
13
|
+
at.files_matching(/^spec\/.*_spec\.rb$/)
|
14
|
+
}
|
15
|
+
|
16
|
+
at.add_exception '.git'
|
17
|
+
at.add_exception 'coverage'
|
18
|
+
end
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in io_unblock.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem 'autotest'
|
8
|
+
gem 'minitest'
|
9
|
+
gem 'minitest-emoji'
|
10
|
+
end
|
11
|
+
|
12
|
+
group :human_testing do
|
13
|
+
gem 'autotest-growl'
|
14
|
+
gem 'simplecov'
|
15
|
+
end
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Rake::TestTask.new do |t|
|
5
|
+
t.libs.push "lib"
|
6
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
7
|
+
t.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Generate a coverage report from tests with simplecov"
|
11
|
+
task :coverage do
|
12
|
+
ENV['SCOV'] = '1'
|
13
|
+
Rake::Task['test'].invoke
|
14
|
+
end
|
15
|
+
|
16
|
+
task :default => [:test]
|
data/io_unblock.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "io_unblock/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "io_unblock"
|
7
|
+
s.version = IoUnblock::VERSION
|
8
|
+
s.authors = ["Ian D. Eccles"]
|
9
|
+
s.email = ["ian.eccles@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/iande/io_unblock"
|
11
|
+
s.summary = %q{Non-blocking IO reads/writes wrapped in a thread}
|
12
|
+
s.description = %q{Non-blocking IO reads/writes wrapped in a thread}
|
13
|
+
|
14
|
+
s.rubyforge_project = "io_unblock"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency 'minitest'
|
22
|
+
s.add_development_dependency 'rake'
|
23
|
+
end
|
data/lib/io_unblock.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module IoUnblock
|
2
|
+
# A very simple synchronized buffer.
|
3
|
+
#
|
4
|
+
# @api private
|
5
|
+
class Buffer
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@buffer = []
|
9
|
+
@mutex = Mutex.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def push bytes, cb
|
13
|
+
synched { @buffer.push [bytes, cb] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def pop
|
17
|
+
synched { @buffer.pop }
|
18
|
+
end
|
19
|
+
|
20
|
+
def unshift bytes, cb
|
21
|
+
synched { @buffer.unshift [bytes, cb] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def shift
|
25
|
+
synched { @buffer.shift }
|
26
|
+
end
|
27
|
+
|
28
|
+
def first
|
29
|
+
synched { @buffer.first }
|
30
|
+
end
|
31
|
+
|
32
|
+
def last
|
33
|
+
synched { @buffer.last }
|
34
|
+
end
|
35
|
+
|
36
|
+
def empty?
|
37
|
+
# Should we lock?
|
38
|
+
@buffer.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def buffered?
|
42
|
+
!empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def synched &block
|
47
|
+
@mutex.synchronize(&block)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module IoUnblock
|
2
|
+
# Handles delegating read and write methods to their non-blocking
|
3
|
+
# counterparts on the IO object. If the IO object does not support
|
4
|
+
# non-blocking methods, falls back to blocking ones.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
module Delegation
|
8
|
+
module NonBlockingWrites
|
9
|
+
def io_write bytes; io.write_nonblock bytes; end
|
10
|
+
private :io_write
|
11
|
+
end
|
12
|
+
|
13
|
+
module BlockingWrites
|
14
|
+
def io_write bytes; io.write bytes; end
|
15
|
+
private :io_write
|
16
|
+
end
|
17
|
+
|
18
|
+
module ForwardWriteable
|
19
|
+
def writeable?; io.writeable?; end
|
20
|
+
private :writeable?
|
21
|
+
end
|
22
|
+
|
23
|
+
module SelectWriteable
|
24
|
+
def writeable?; !!IO.select(nil, io_selector, nil, select_delay); end
|
25
|
+
private :writeable?
|
26
|
+
end
|
27
|
+
|
28
|
+
module NonBlockingReads
|
29
|
+
def io_read len; io.read_nonblock len; end
|
30
|
+
private :io_read
|
31
|
+
end
|
32
|
+
|
33
|
+
module PartialReads
|
34
|
+
def io_read len; io.readpartial len; end
|
35
|
+
private :io_read
|
36
|
+
end
|
37
|
+
|
38
|
+
module BlockingReads
|
39
|
+
def io_read len; io.read len; end
|
40
|
+
private :io_read
|
41
|
+
end
|
42
|
+
|
43
|
+
module ForwardReadable
|
44
|
+
def readable?; io.readable?; end
|
45
|
+
private :readable?
|
46
|
+
end
|
47
|
+
|
48
|
+
module SelectReadable
|
49
|
+
def readable?; !!IO.select(io_selector, nil, nil, select_delay); end
|
50
|
+
private :readable?
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def define_io_methods stream
|
55
|
+
define_io_write stream
|
56
|
+
define_io_read stream
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def define_io_write stream
|
61
|
+
if stream.io.respond_to? :write_nonblock
|
62
|
+
stream.extend NonBlockingWrites
|
63
|
+
else
|
64
|
+
stream.extend BlockingWrites
|
65
|
+
end
|
66
|
+
|
67
|
+
if stream.io.respond_to? :writeable?
|
68
|
+
stream.extend ForwardWriteable
|
69
|
+
else
|
70
|
+
stream.extend SelectWriteable
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def define_io_read stream
|
75
|
+
if stream.io.respond_to? :read_nonblock
|
76
|
+
stream.extend NonBlockingReads
|
77
|
+
elsif stream.io.respond_to? :readpartial
|
78
|
+
stream.extend PartialReads
|
79
|
+
else
|
80
|
+
stream.extend BlockingReads
|
81
|
+
end
|
82
|
+
|
83
|
+
if stream.io.respond_to? :readable?
|
84
|
+
stream.extend ForwardReadable
|
85
|
+
else
|
86
|
+
stream.extend SelectReadable
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module IoUnblock
|
2
|
+
class StreamError < IoUnblockError; end
|
3
|
+
|
4
|
+
class Stream
|
5
|
+
MAX_BYTES_PER_WRITE = 1024 * 8
|
6
|
+
MAX_BYTES_PER_READ = 1024 * 4
|
7
|
+
|
8
|
+
attr_reader :running, :connected, :io, :io_selector, :callbacks
|
9
|
+
attr_accessor :select_delay
|
10
|
+
alias :running? :running
|
11
|
+
alias :connected? :connected
|
12
|
+
|
13
|
+
# The given IO object, `io`, is assumed to be opened/connected.
|
14
|
+
# Global callbacks:
|
15
|
+
# - failed: called when IO access throws an exception that cannot
|
16
|
+
# be recovered from (opening the IO fails,
|
17
|
+
# TCP connection reset, unexpected EOF, etc.)
|
18
|
+
# - read: called when any data is read from the underlying IO
|
19
|
+
# object
|
20
|
+
# - wrote: called when any data is written to the underlying IO
|
21
|
+
# object
|
22
|
+
# - closed: called when the underlying IO object is closed (even
|
23
|
+
# if it is closed as a result of a failure)
|
24
|
+
# - started: called when the IO processing has started
|
25
|
+
# - stopped: called when the IO processing has stopped
|
26
|
+
# - looped: called when each time the IO processing loops
|
27
|
+
def initialize io, callbacks=nil
|
28
|
+
@io = io
|
29
|
+
@io_selector = [@io]
|
30
|
+
@processor = nil
|
31
|
+
@s_mutex = Mutex.new
|
32
|
+
@w_buff = IoUnblock::Buffer.new
|
33
|
+
@running = false
|
34
|
+
@connected = true
|
35
|
+
@callbacks = callbacks || {}
|
36
|
+
@select_delay = 0.1
|
37
|
+
Delegation.define_io_methods self
|
38
|
+
yield self if block_given?
|
39
|
+
end
|
40
|
+
|
41
|
+
def start &cb
|
42
|
+
@s_mutex.synchronize do
|
43
|
+
raise StreamError, "already started" if @running
|
44
|
+
@running = true
|
45
|
+
@processor = Thread.new do
|
46
|
+
trigger_callbacks :started, :start, &cb
|
47
|
+
read_and_write while running? && connected?
|
48
|
+
flush_and_close
|
49
|
+
trigger_callbacks :stopped, :stop, &cb
|
50
|
+
end
|
51
|
+
end
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def stop
|
56
|
+
@s_mutex.synchronize do
|
57
|
+
if @running
|
58
|
+
@running = false
|
59
|
+
@processor.join
|
60
|
+
end
|
61
|
+
end
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# The callback triggered here will be invoked only when all bytes
|
66
|
+
# have been written.
|
67
|
+
def write bytes, &cb
|
68
|
+
@w_buff.push bytes, cb
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def trigger_callbacks named, *args, &other
|
74
|
+
other && other.call(*args)
|
75
|
+
@callbacks.key?(named) && @callbacks[named].call(*args)
|
76
|
+
end
|
77
|
+
|
78
|
+
def flush_and_close
|
79
|
+
_write while connected? && @w_buff.buffered?
|
80
|
+
io_close
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_and_write
|
85
|
+
_read if read?
|
86
|
+
_write if write?
|
87
|
+
trigger_callbacks :looped, self
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
def _read
|
92
|
+
begin
|
93
|
+
bytes = io_read MAX_BYTES_PER_READ
|
94
|
+
trigger_callbacks(:read, bytes) if bytes
|
95
|
+
rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
|
96
|
+
rescue Exception
|
97
|
+
force_close $!
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def _write
|
102
|
+
written = 0
|
103
|
+
while written < MAX_BYTES_PER_WRITE
|
104
|
+
bytes, cb = @w_buff.shift
|
105
|
+
break unless bytes
|
106
|
+
begin
|
107
|
+
w = io_write bytes
|
108
|
+
rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
|
109
|
+
# writing will either block, or cannot otherwise be completed,
|
110
|
+
# put data back and try again some other day
|
111
|
+
@w_buff.unshift bytes, cb
|
112
|
+
break
|
113
|
+
rescue Exception
|
114
|
+
force_close $!
|
115
|
+
break
|
116
|
+
end
|
117
|
+
written += w
|
118
|
+
if w < bytes.size
|
119
|
+
@w_buff.unshift bytes[w..-1], cb
|
120
|
+
trigger_callbacks :wrote, bytes, w
|
121
|
+
else
|
122
|
+
trigger_callbacks :wrote, bytes, w, &cb
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def io_close
|
128
|
+
if connected?
|
129
|
+
@io.close rescue nil
|
130
|
+
@connected = false
|
131
|
+
trigger_callbacks :closed
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def force_close ex
|
136
|
+
io_close
|
137
|
+
trigger_callbacks :failed, ex
|
138
|
+
end
|
139
|
+
|
140
|
+
def write?
|
141
|
+
begin
|
142
|
+
connected? && @w_buff.buffered? && writeable?
|
143
|
+
rescue Exception
|
144
|
+
force_close $!
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def read?
|
150
|
+
begin
|
151
|
+
connected? && readable?
|
152
|
+
rescue Exception
|
153
|
+
force_close $!
|
154
|
+
false
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.expand_path("../../spec_helper.rb", __FILE__)
|
2
|
+
|
3
|
+
describe IoUnblock::Buffer do
|
4
|
+
before do
|
5
|
+
@buffer = IoUnblock::Buffer.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should be empty when nothing is buffered" do
|
9
|
+
@buffer.push :a, 1
|
10
|
+
@buffer.pop
|
11
|
+
@buffer.empty?.must_equal true
|
12
|
+
@buffer.buffered?.must_equal false
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should not be empty when stuff is buffered" do
|
16
|
+
@buffer.push :a, 1
|
17
|
+
@buffer.empty?.must_equal false
|
18
|
+
@buffer.buffered?.must_equal true
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should push to the end of the buffer" do
|
22
|
+
@buffer.push :a, 1
|
23
|
+
@buffer.push :b, 2
|
24
|
+
@buffer.first.must_equal [:a, 1]
|
25
|
+
@buffer.last.must_equal [:b, 2]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should unshift to the beginning of the buffer" do
|
29
|
+
@buffer.unshift :a, 1
|
30
|
+
@buffer.unshift :b, 2
|
31
|
+
@buffer.last.must_equal [:a, 1]
|
32
|
+
@buffer.first.must_equal [:b, 2]
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should shift from the beginning" do
|
36
|
+
@buffer.push :a, 1
|
37
|
+
@buffer.push :b, 2
|
38
|
+
@buffer.shift.must_equal [:a, 1]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should pop from the end" do
|
42
|
+
@buffer.push :a, 1
|
43
|
+
@buffer.push :b, 2
|
44
|
+
@buffer.pop.must_equal [:b, 2]
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require File.expand_path("../../spec_helper.rb", __FILE__)
|
2
|
+
|
3
|
+
describe IoUnblock::Delegation do
|
4
|
+
class FakeStream
|
5
|
+
attr_reader :io
|
6
|
+
def initialize io
|
7
|
+
@io = io
|
8
|
+
end
|
9
|
+
def io_selector; [@io]; end
|
10
|
+
def select_delay; 18; end
|
11
|
+
end
|
12
|
+
|
13
|
+
before do
|
14
|
+
@mock_io = MiniTest::Mock.new
|
15
|
+
@stream = FakeStream.new @mock_io
|
16
|
+
end
|
17
|
+
|
18
|
+
it "delegates to read_nonblock when available" do
|
19
|
+
@mock_io.expect(:read_nonblock, :reading, [42])
|
20
|
+
IoUnblock::Delegation.define_io_methods @stream
|
21
|
+
@stream.__send__ :io_read, 42
|
22
|
+
@mock_io.verify
|
23
|
+
end
|
24
|
+
|
25
|
+
it "delegates to readpartial when available" do
|
26
|
+
@mock_io.expect(:readpartial, :reading, [42])
|
27
|
+
IoUnblock::Delegation.define_io_methods @stream
|
28
|
+
@stream.__send__ :io_read, 42
|
29
|
+
@mock_io.verify
|
30
|
+
end
|
31
|
+
|
32
|
+
it "delegates to read if all else fails" do
|
33
|
+
@mock_io.expect(:read, :reading, [42])
|
34
|
+
IoUnblock::Delegation.define_io_methods @stream
|
35
|
+
@stream.__send__ :io_read, 42
|
36
|
+
@mock_io.verify
|
37
|
+
end
|
38
|
+
|
39
|
+
it "delegates to write_nonblock when available" do
|
40
|
+
@mock_io.expect(:write_nonblock, :writing, ['hello'])
|
41
|
+
IoUnblock::Delegation.define_io_methods @stream
|
42
|
+
@stream.__send__ :io_write, 'hello'
|
43
|
+
@mock_io.verify
|
44
|
+
end
|
45
|
+
|
46
|
+
it "delegates to write if all else fails" do
|
47
|
+
@mock_io.expect(:write, :writing, ['hello'])
|
48
|
+
IoUnblock::Delegation.define_io_methods @stream
|
49
|
+
@stream.__send__ :io_write, 'hello'
|
50
|
+
@mock_io.verify
|
51
|
+
end
|
52
|
+
|
53
|
+
it "delegates to IO.select when there is no readable? method" do
|
54
|
+
mocked = MiniTest::Mock.new
|
55
|
+
mocked.expect(:select, nil, [[@mock_io], nil, nil, 18])
|
56
|
+
real_io = IO
|
57
|
+
::IO = mocked
|
58
|
+
IoUnblock::Delegation.define_io_methods @stream
|
59
|
+
@stream.__send__ :readable?
|
60
|
+
::IO = real_io
|
61
|
+
mocked.verify
|
62
|
+
end
|
63
|
+
|
64
|
+
it "delegates to IO.select when there is no writeable? method" do
|
65
|
+
mocked = MiniTest::Mock.new
|
66
|
+
mocked.expect(:select, nil, [nil, [@mock_io], nil, 18])
|
67
|
+
real_io = IO
|
68
|
+
::IO = mocked
|
69
|
+
IoUnblock::Delegation.define_io_methods @stream
|
70
|
+
@stream.__send__ :writeable?
|
71
|
+
::IO = real_io
|
72
|
+
mocked.verify
|
73
|
+
end
|
74
|
+
|
75
|
+
it "delegates to readable? when the method is available" do
|
76
|
+
@mock_io.expect(:readable?, false, [])
|
77
|
+
IoUnblock::Delegation.define_io_methods @stream
|
78
|
+
@stream.__send__ :readable?
|
79
|
+
@mock_io.verify
|
80
|
+
end
|
81
|
+
|
82
|
+
it "delegates to writeable? when the method is available" do
|
83
|
+
@mock_io.expect(:writeable?, false, [])
|
84
|
+
IoUnblock::Delegation.define_io_methods @stream
|
85
|
+
@stream.__send__ :writeable?
|
86
|
+
@mock_io.verify
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require File.expand_path("../../spec_helper.rb", __FILE__)
|
2
|
+
require 'stringio'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
$log = Logger.new $stdout
|
6
|
+
|
7
|
+
describe IoUnblock::Stream do
|
8
|
+
def dummy_io; @dummy_io ||= DummyIO.new; end
|
9
|
+
def stream; @stream ||= IoUnblock::Stream.new dummy_io; end
|
10
|
+
|
11
|
+
it "raises an exception if started twice" do
|
12
|
+
stream.start
|
13
|
+
lambda {
|
14
|
+
stream.start
|
15
|
+
}.must_raise IoUnblock::StreamError
|
16
|
+
stream.stop
|
17
|
+
end
|
18
|
+
|
19
|
+
it "reads only if the IO is ready for it" do
|
20
|
+
dummy_io.r_stream.string = 'test string'
|
21
|
+
dummy_io.readable = false
|
22
|
+
stream.start
|
23
|
+
Thread.pass until stream.running?
|
24
|
+
dummy_io.r_stream.pos.must_equal 0
|
25
|
+
dummy_io.readable = true
|
26
|
+
Thread.pass until dummy_io.r_stream.eof?
|
27
|
+
stream.stop
|
28
|
+
dummy_io.r_stream.pos.must_equal 11
|
29
|
+
end
|
30
|
+
|
31
|
+
it "closes the io when stopping" do
|
32
|
+
dummy_io.closed?.must_equal false
|
33
|
+
stream.start
|
34
|
+
Thread.pass until stream.running?
|
35
|
+
stream.stop
|
36
|
+
dummy_io.closed?.must_equal true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "flushes all writes before stopping (even if io claims to be unwriteable)" do
|
40
|
+
dummy_io.writeable = false
|
41
|
+
stream.start
|
42
|
+
stream.write 'hello '
|
43
|
+
stream.write 'world.'
|
44
|
+
Thread.pass until stream.running?
|
45
|
+
dummy_io.w_stream.string.must_equal ''
|
46
|
+
stream.stop
|
47
|
+
dummy_io.w_stream.string.must_equal 'hello world.'
|
48
|
+
end
|
49
|
+
|
50
|
+
it "does not die on EINTER" do
|
51
|
+
dummy_io.raise_write = Errno::EINTR.new
|
52
|
+
dummy_io.raise_read = Errno::EINTR.new
|
53
|
+
stream.start
|
54
|
+
stream.write 'hello'
|
55
|
+
Thread.pass until dummy_io.raised_read?
|
56
|
+
stream.connected?.must_equal true
|
57
|
+
dummy_io.raise_read = nil
|
58
|
+
Thread.pass until dummy_io.raised_write?
|
59
|
+
stream.connected?.must_equal true
|
60
|
+
dummy_io.raise_write = nil
|
61
|
+
stream.stop
|
62
|
+
dummy_io.w_stream.string.must_equal 'hello'
|
63
|
+
end
|
64
|
+
|
65
|
+
it "does not die on EAGAIN" do
|
66
|
+
dummy_io.raise_write = Errno::EAGAIN.new
|
67
|
+
dummy_io.raise_read = Errno::EINTR.new
|
68
|
+
stream.start
|
69
|
+
stream.write 'hello'
|
70
|
+
Thread.pass until dummy_io.raised_read?
|
71
|
+
stream.connected?.must_equal true
|
72
|
+
dummy_io.raise_read = nil
|
73
|
+
Thread.pass until dummy_io.raised_write?
|
74
|
+
stream.connected?.must_equal true
|
75
|
+
dummy_io.raise_write = nil
|
76
|
+
stream.stop
|
77
|
+
dummy_io.w_stream.string.must_equal 'hello'
|
78
|
+
end
|
79
|
+
|
80
|
+
it "does not die on EWOULDBLOCK" do
|
81
|
+
dummy_io.raise_write = Errno::EWOULDBLOCK.new
|
82
|
+
dummy_io.raise_read = Errno::EINTR.new
|
83
|
+
stream.start
|
84
|
+
stream.write 'hello'
|
85
|
+
Thread.pass until dummy_io.raised_read?
|
86
|
+
stream.connected?.must_equal true
|
87
|
+
dummy_io.raise_read = nil
|
88
|
+
Thread.pass until dummy_io.raised_write?
|
89
|
+
stream.connected?.must_equal true
|
90
|
+
dummy_io.raise_write = nil
|
91
|
+
stream.stop
|
92
|
+
dummy_io.w_stream.string.must_equal 'hello'
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "callbacks" do
|
96
|
+
def called_with; @calls_received ||= []; end
|
97
|
+
def callback; @callback ||= lambda { |*a| called_with << a }; end
|
98
|
+
|
99
|
+
def callback_stream cbs=nil
|
100
|
+
IoUnblock::Stream.new(dummy_io, cbs)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "triggers started when starting" do
|
104
|
+
cb_stream = callback_stream(started: callback)
|
105
|
+
cb_stream.start
|
106
|
+
cb_stream.stop
|
107
|
+
called_with.must_equal [ [:start] ]
|
108
|
+
end
|
109
|
+
|
110
|
+
it "triggers stopped when stopping" do
|
111
|
+
cb_stream = callback_stream(stopped: callback)
|
112
|
+
cb_stream.start
|
113
|
+
cb_stream.stop
|
114
|
+
called_with.must_equal [ [:stop] ]
|
115
|
+
end
|
116
|
+
|
117
|
+
it "triggers looped after each read/write cycle" do
|
118
|
+
cb_stream = callback_stream(looped: callback)
|
119
|
+
cb_stream.start
|
120
|
+
Thread.pass while called_with.empty?
|
121
|
+
cb_stream.stop
|
122
|
+
called_with.first.must_equal [cb_stream]
|
123
|
+
end
|
124
|
+
|
125
|
+
it "triggers wrote when writing" do
|
126
|
+
dummy_io.max_write = 3
|
127
|
+
cb_stream = callback_stream(wrote: callback)
|
128
|
+
cb_stream.start
|
129
|
+
cb_stream.write "hello"
|
130
|
+
cb_stream.stop
|
131
|
+
called_with.must_equal [ ['hello', 3], ['lo', 2] ]
|
132
|
+
end
|
133
|
+
|
134
|
+
it "triggers read when reading" do
|
135
|
+
dummy_io.max_read = 3
|
136
|
+
dummy_io.r_stream.string = 'hello'
|
137
|
+
cb_stream = callback_stream(read: callback)
|
138
|
+
cb_stream.start
|
139
|
+
Thread.pass until dummy_io.r_stream.eof?
|
140
|
+
cb_stream.stop
|
141
|
+
called_with.must_equal [ ['hel'], ['lo'] ]
|
142
|
+
end
|
143
|
+
|
144
|
+
it "triggers closed when closing" do
|
145
|
+
cb_stream = callback_stream(closed: callback)
|
146
|
+
cb_stream.start
|
147
|
+
cb_stream.stop
|
148
|
+
called_with.must_equal [ [] ]
|
149
|
+
end
|
150
|
+
|
151
|
+
it "triggers failed when reading raises an error" do
|
152
|
+
err = RuntimeError.new "fail"
|
153
|
+
cb_stream = callback_stream(failed: callback)
|
154
|
+
dummy_io.raise_read = err
|
155
|
+
cb_stream.start
|
156
|
+
Thread.pass until cb_stream.running?
|
157
|
+
Thread.pass while cb_stream.connected?
|
158
|
+
cb_stream.stop
|
159
|
+
called_with.must_equal [ [err] ]
|
160
|
+
end
|
161
|
+
|
162
|
+
it "triggers failed when writing raises an error" do
|
163
|
+
err = RuntimeError.new "fail"
|
164
|
+
cb_stream = callback_stream(failed: callback)
|
165
|
+
dummy_io.raise_write = err
|
166
|
+
cb_stream.start
|
167
|
+
cb_stream.write "hello"
|
168
|
+
cb_stream.stop
|
169
|
+
called_with.must_equal [ [err] ]
|
170
|
+
end
|
171
|
+
|
172
|
+
it "triggers the given callback when starting and stopping" do
|
173
|
+
stream.start(&callback)
|
174
|
+
stream.stop
|
175
|
+
called_with.must_equal [ [:start], [:stop]]
|
176
|
+
end
|
177
|
+
|
178
|
+
it "triggers the given callback after writing the full string" do
|
179
|
+
dummy_io.max_write = 3
|
180
|
+
stream.start
|
181
|
+
stream.write('hello', &callback)
|
182
|
+
stream.stop
|
183
|
+
called_with.must_equal [ ['lo', 2] ]
|
184
|
+
end
|
185
|
+
|
186
|
+
it "is not connected when failed is triggered" do
|
187
|
+
is_connected = true
|
188
|
+
cb_stream = callback_stream(
|
189
|
+
failed: lambda { |ex| is_connected = cb_stream.connected? }
|
190
|
+
)
|
191
|
+
err = RuntimeError.new "fail"
|
192
|
+
dummy_io.raise_write = err
|
193
|
+
cb_stream.start
|
194
|
+
cb_stream.write "hello"
|
195
|
+
cb_stream.stop
|
196
|
+
is_connected.must_equal false
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
if ENV['SCOV']
|
2
|
+
require 'simplecov'
|
3
|
+
SimpleCov.start do
|
4
|
+
puts "I am started?! #{Process.pid}"
|
5
|
+
add_filter "/spec/"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'minitest/autorun'
|
10
|
+
require 'minitest/emoji'
|
11
|
+
require 'io_unblock'
|
12
|
+
require 'stringio'
|
13
|
+
|
14
|
+
# Used to test IO stuff, using stringio objects for the write and
|
15
|
+
# read stream.
|
16
|
+
class DummyIO
|
17
|
+
attr_reader :closed, :w_stream, :r_stream, :raised_write, :raised_read
|
18
|
+
alias :closed? :closed
|
19
|
+
alias :raised_write? :raised_write
|
20
|
+
alias :raised_read? :raised_read
|
21
|
+
attr_accessor :readable, :writeable
|
22
|
+
attr_accessor :write_delay, :read_delay
|
23
|
+
attr_accessor :max_write, :max_read
|
24
|
+
attr_accessor :raise_read, :raise_write
|
25
|
+
alias :readable? :readable
|
26
|
+
alias :writeable? :writeable
|
27
|
+
|
28
|
+
def initialize *args, &block
|
29
|
+
@r_stream = StringIO.new
|
30
|
+
@w_stream = StringIO.new
|
31
|
+
@readable = @writeable = true
|
32
|
+
@read_delay = @write_delay = 0
|
33
|
+
@max_write = 0
|
34
|
+
@max_read = 0
|
35
|
+
@closed = false
|
36
|
+
@raise_read = nil
|
37
|
+
@raise_write = nil
|
38
|
+
@raised_write = false
|
39
|
+
@raised_read = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def close
|
43
|
+
@closed = true
|
44
|
+
@w_stream.close
|
45
|
+
@r_stream.close
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_nonblock bytes
|
49
|
+
sleep(@write_delay) if @write_delay > 0
|
50
|
+
do_raise_write
|
51
|
+
if @max_write > 0 && bytes.size > @max_write
|
52
|
+
@w_stream.write bytes[0...@max_write]
|
53
|
+
else
|
54
|
+
@w_stream.write bytes
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_nonblock len
|
59
|
+
sleep(@read_delay) if @read_delay > 0
|
60
|
+
do_raise_read
|
61
|
+
if @max_read > 0 && len > @max_read
|
62
|
+
@r_stream.read @max_read
|
63
|
+
else
|
64
|
+
@r_stream.read len
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def do_raise_write
|
69
|
+
if @raise_write
|
70
|
+
@raised_write = true
|
71
|
+
raise @raise_write
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def do_raise_read
|
76
|
+
if @raise_read
|
77
|
+
@raised_read = true
|
78
|
+
raise @raise_read
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: io_unblock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ian D. Eccles
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-12 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: minitest
|
16
|
+
requirement: &2161332520 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2161332520
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &2161331860 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2161331860
|
36
|
+
description: Non-blocking IO reads/writes wrapped in a thread
|
37
|
+
email:
|
38
|
+
- ian.eccles@gmail.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- .autotest
|
44
|
+
- .gitignore
|
45
|
+
- Gemfile
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- io_unblock.gemspec
|
49
|
+
- lib/io_unblock.rb
|
50
|
+
- lib/io_unblock/buffer.rb
|
51
|
+
- lib/io_unblock/delegation.rb
|
52
|
+
- lib/io_unblock/stream.rb
|
53
|
+
- lib/io_unblock/version.rb
|
54
|
+
- spec/io_unblock/buffer_spec.rb
|
55
|
+
- spec/io_unblock/delegation_spec.rb
|
56
|
+
- spec/io_unblock/stream_spec.rb
|
57
|
+
- spec/spec_helper.rb
|
58
|
+
homepage: https://github.com/iande/io_unblock
|
59
|
+
licenses: []
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubyforge_project: io_unblock
|
78
|
+
rubygems_version: 1.8.12
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: Non-blocking IO reads/writes wrapped in a thread
|
82
|
+
test_files:
|
83
|
+
- spec/io_unblock/buffer_spec.rb
|
84
|
+
- spec/io_unblock/delegation_spec.rb
|
85
|
+
- spec/io_unblock/stream_spec.rb
|
86
|
+
- spec/spec_helper.rb
|
87
|
+
has_rdoc:
|