--- /dev/null
+# Methods common to IO::Readable, IO::Writable, and IO::Seekable.
+module IO::Common
+
+ # Calls both +close_read+ and +close_write+, and raises an exception unless
+ # at least one succeeds. It may be desirable to reimplement this.
+ def close
+ cr = respond_to?(:close_read)
+ cw = respond_to?(:close_write)
+ if cr && cw
+ failed = false
+ begin
+ close_read
+ rescue IOError
+ failed = $!
+ end
+ begin
+ close_write
+ rescue IOError
+ return unless failed
+ raise
+ end
+ elsif cr
+ close_read
+ elsif cw
+ close_write
+ else
+ raise NotImplementedError, "close_read and close_write unimplemented", caller
+ end
+ end
+
+ # Checks both +closed_read+? and +closed_write+?, raising an exception if
+ # neither method exists. This method must be redefined for IO::Readable
+ # and IO::Writable classes unless one of the previously mentioned methods
+ # has been provided.
+ def closed?
+ cr = respond_to?(:closed_read?)
+ cw = respond_to?(:closed_write?)
+ if cr && cw
+ return closed_read? && closed_write?
+ elsif cr
+ return closed_read?
+ elsif cw
+ return closed_write?
+ else
+ raise NotImplementedError, "closed_read? and closed_write? unimplemented", caller
+ end
+ end
+
+ # Do not override this method. Instead, use +isatty+.
+ def tty?
+ isatty
+ end
+
+ # Always +false+. May be redefined.
+ def isatty
+ false
+ end
+
+ # Always 0. May be redefined.
+ def fsync
+ 0
+ end
+
+ # Always +true+. May be redefined.
+ def sync
+ true
+ end
+
+ # Raises a +NotImplementedError+. May be redefined.
+ def fcntl(*args)
+ raise NotImplementedError, "fcntl not implemented", caller
+ end
+
+ # Always returns the object. May be redefined.
+ def binmode
+ self
+ end
+
+ [ :fileno, :pid, :sync=, :path ].each do |method|
+ define_method(method) {nil}
+ end
+
+end
+
+# The IO::Readable mixin provides classes with several methods for reading
+# a data stream, similar to those found in the IO class. The +sysread+ and
+# +close_read+ methods must be defined. Additionally, <tt>closed_read?</tt>,
+# <tt>closed?</tt>, and <tt>close</tt> are recommended.
+#
+# IO::Readable provides buffering, but only the absolute minimum needed to
+# provide a complete implementation. For example, #eof checks for the end of
+# file by reading and buffering a single byte.
+#
+# The example below illustrates the use of IO::Readable in a class that
+# concatenates multiple streams. Note that seeking is not supported but one
+# can start over with #rewind.
+#
+# :include:../../test/read_joiner.rb
+module IO::Readable
+
+ REQUIRED_METHODS = [ :sysread, :close_read ]
+
+ include IO::Common
+ include Enumerable
+
+ # This adds three identically named class methods that function like
+ # IO::read, IO::readlines, and IO::foreach. These may be factored into
+ # IO::Openable or removed entirely in a future release.
+ def self.included(base) #:nodoc:
+ unless base.respond_to?(:read)
+ def base.read(name,length=nil,offset=nil)
+ name = [name] unless name.respond_to?(:to_ary)
+ object = new(*name)
+ object.seek(offset) if offset
+ object.read(length)
+ ensure
+ object.close_read if object.respond_to?(:close_read)
+ end
+ end
+ unless base.respond_to?(:readlines)
+ def base.readlines(name,sep_string=$/)
+ name = [name] unless name.respond_to?(:to_ary)
+ object = new(*name)
+ object.readlines(sep_string)
+ ensure
+ object.close_read if object.respond_to?(:close_read)
+ end
+ end
+ unless base.respond_to?(:foreach)
+ def base.foreach(*args,&block)
+ readlines(*args).each(&block)
+ end
+ end
+ super
+ end
+
+ # As with with IO#gets, read and return a line, with lines separated by
+ # +sep_string+. This method may be overridden, although this is not
+ # recommended unless one is intimately familiar with the required behaviors
+ # and side effects.
+ def gets(sep_string=$/)
+ return read(nil) unless sep_string
+ line = ""
+ paragraph = false
+ if sep_string == ""
+ sep_string = "\n\n"
+ paragraph = true
+ end
+ sep = sep_string.dup
+ position = @_iom_pos
+ while (char = getc)
+ if paragraph && line.empty?
+ if char == ?\n
+ next
+ # else
+ # paragraph = false
+ end
+ end
+ if char == sep[0]
+ sep[0] = ""
+ else
+ sep = sep_string.dup
+ end
+ if sep == ""
+ if paragraph
+ ungetc char
+ else
+ line << char
+ end
+ break
+ end
+ line << char
+ if position && @_iom_pos == position
+ raise IOError, "loop encountered", caller
+ end
+ end
+ line = nil if line == ""
+ if line && respond_to?(:lineno=) && respond_to?(:lineno)
+ self.lineno += 1
+ $. = lineno
+ end
+ $_ = line
+ end
+
+ # This method returns the number of times IO::Readable#gets was called.
+ # The count is reset on calls to IO::Seekable#rewind.
+ def lineno
+ @_iom_lineno ||= 0
+ end
+
+ # This method and +lineno+ must be redefined together, or not at all.
+ def lineno=(num)
+ @_iom_lineno = num
+ end
+
+ # Like IO#read, read up to +length+ bytes and return them, optionally
+ # assigning them to +buffer+ as well.
+ def read(length = nil,buffer = "")
+ raise ArgumentError, "negative length #{length} given", caller if (length||0) < 0
+ return "" if length == 0 && (@_iom_buffer.nil? || @_iom_buffer.length > 0)
+ return (length ? nil : "") if eof
+ return "" if length == 0
+ @_iom_buffer ||= ""
+ if length
+ begin
+ @_iom_buffer << sysread(length-@_iom_buffer.length) if @_iom_buffer.length<length
+ # rescue EOFError
+ end
+ else
+ begin
+ while str = sysread(1024)
+ @_iom_buffer << str
+ end
+ rescue EOFError
+ nil # For coverage
+ end
+ end
+ buffer[0..-1] = @_iom_buffer.slice!(0..(length || 0)-1)
+ @_iom_pos ||= 0
+ @_iom_pos += buffer.length
+ return buffer
+ end
+
+ # See IO#getc. Implemented via #read.
+ def getc
+ read(1).to_s[0]
+ end
+
+ # See IO#ungetc.
+ def ungetc(char)
+ raise IOError, "unread stream", caller unless @_iom_buffer
+ @_iom_buffer[0,0] = char.chr
+ @_iom_pos -= 1
+ nil
+ end
+
+ # See IO#eof.
+ def eof
+ # tell if respond_to?(:tell)
+ @_iom_buffer ||= ""
+ return false unless @_iom_buffer.empty?
+ str = sysread(1)
+ if str
+ @_iom_buffer << str
+ @_iom_buffer.empty?
+ else
+ true
+ end
+ rescue EOFError
+ return true
+ end
+
+ # Reads a character as with IO#getc, but raises an +EOFError+ on end of
+ # file. Implemented via #getc.
+ def readchar()
+ getc or raise EOFError, "end of file reached", caller
+ end
+
+ # Reads a character as with #gets, but raises an +EOFError+ on
+ # end of file. Implemented via #gets.
+ def readline(sep_string = $/)
+ gets(sep_string) or raise EOFError, "end of file reached", caller
+ end
+
+ # See IO#readlines. Implemented via #gets.
+ def readlines(sep_string = $/)
+ array = []
+ line = nil
+ array << line while line = gets(sep_string)
+ array
+ end
+
+ # See IO#each_byte. Implemented via #getc.
+ def each_byte
+ while byte = getc
+ yield byte
+ end
+ self
+ end
+
+ # See IO#each_line. Implemented via #gets.
+ def each_line(sep_string = $/)
+ while line = gets(sep_string)
+ yield line
+ end
+ self
+ end
+
+ alias each each_line
+
+ # See IO#eof?. Implemented via #eof.
+ def eof?
+ eof
+ end
+
+end
+
+# The IO::Writable mixin provides classes with several methods for writing
+# a data stream, similar to those found in the IO class. The methods
+# +syswrite+ and +close_write+ must be provided. Additionally,
+# <tt>closed_write?</tt>, <tt>closed?</tt>, and +close+ are recommended.
+#
+# The following example illustrates the use of IO::Readable, IO::Writable, and
+# IO::Seekable. It wraps around another stream and applies rot13 to the
+# contents.
+#
+# :include:../../test/rot13_filter.rb
+module IO::Writable
+
+ REQUIRED_METHODS = [ :syswrite, :close_write ]
+ include IO::Common
+
+ # See IO#write.
+ def write(string)
+ @_iom_pos ||= 0
+ @_iom_pos += string.to_s.length
+ @_iom_buffer.slice!(0,string.to_s.length) if @_iom_buffer
+ syswrite(string.to_s)
+ end
+
+ # See IO#<<. Implemented via #write.
+ def <<(string)
+ # write(string.respond_to?(:to_int) ? string.to_int.chr : string)
+ write(string)
+ self
+ end
+
+ # See IO#print. Implemented via #write.
+ def print(obj = $_,*args)
+ ([obj]+args).each do |line|
+ write(line + ($\||''))
+ end
+ nil
+ end
+
+ # See IO#puts. Implemented via #write.
+ def puts(obj = "\n",*args)
+ [obj,args].flatten.each do |line|
+ line = "nil" if line.nil?
+ write(line.to_s + (line.to_s[-1] == ?\n ? "" : "\n"))
+ end
+ nil
+ end
+
+ # See IO#printf. Implemented via #write.
+ def printf(first="",*rest)
+ write(sprintf(first,*rest))
+ nil
+ end
+
+ # See IO#putc. Implemented via #write
+ def putc(char)
+ if char.respond_to?(:to_int)
+ write((char.to_int & 0xFF).chr)
+ elsif char.respond_to?(:to_str) && char.to_str.length > 0
+ write(char.to_str[0,1])
+ else
+ raise TypeError, "can't convert #{char.class} to Integer", caller
+ end
+ char
+ end
+
+ # Returns +self+. May be redefined.
+ def flush
+ self
+ end
+
+end
+
+# The IO::Seekable mixin provides classes with several methods for seeking
+# within a data stream, similar to those found in the IO class. The method
+# +sysseek+ must be provided. This method should operate identically to
+# IO#sysseek, seeking to the desired location and returning the final
+# absolute offset. One should also consider optimizing for
+# <tt>sysseek(0,IO::SEEK_CUR)</tt>. This particular call should simply return
+# the position in the stream.
+#
+# For an example implementation, see IO::Writable.
+module IO::Seekable
+
+ REQUIRED_METHODS = [ :sysseek ]
+ include IO::Common
+
+ # See IO#pos. Implemented via #tell.
+ def pos
+ tell
+ end
+
+ # See IO#pos=. Implemented via #seek.
+ def pos=(pos)
+ seek(pos)
+ tell
+ end
+
+ # See IO#rewind. Calls both #seek and IO::Readable#lineno=, if included.
+ def rewind
+ seek(0)
+ self.lineno = 0 if respond_to?(:lineno=)
+ 0
+ end
+
+ # See IO#seek. This method calls #sysseek and only should be overridden in a
+ # class that is taking responsibility for tracking its own position. If this
+ # is the case, the class should also override #seek, as well as
+ # IO::Readable#read, IO::Readable#ungetc, IO::Readable#getc,
+ # IO::Readable#eof, IO::Writable#write, and IO::Writable#putc, if the
+ # respective modules are included.
+ def seek(amount, whence = IO::SEEK_SET)
+ if whence == IO::SEEK_CUR && @_iom_buffer
+ amount -= @_iom_buffer.length
+ end
+ @_iom_pos ||= 0
+ @_iom_pos += @_iom_buffer.length if @_iom_buffer
+ @_iom_buffer = nil if @_iom_buffer
+ @_iom_pos = sysseek(amount, whence)
+ 0
+ end
+
+ # See IO#tell. See #seek for information on overriding this method.
+ def tell
+ @_iom_pos ||= 0
+ end
+
+end
+
+# The IO::Closable mixin works with IO::Readable, IO::Writable, and
+# IO::Seekable. It provides #closed_read?, #closed_write?, #close_read, and
+# #close_write methods, and wraps the required methods for the aforementioned
+# modules such that they raise an exception if applied to a closed object.
+# You must include _at_ _least_ one of IO::Readable or IO::Writable for this
+# mixin to be useful.
+#
+# This mixin is highly experimental. It currently works by trapping
+# method_added and wrapping the newly defined method.
+module IO::Closable
+ WRAPPED_READ_METHODS = [ :sysread, :getc, :ungetc, :eof, :read, :gets ]
+ WRAPPED_WRITE_METHODS = [ :syswrite, :putc, :write ]
+ WRAPPED_BOTH_METHODS = [ :sysseek, :tell, :seek, :reopen, :pos, :pos= ]
+ WRAPPED_METHODS = WRAPPED_READ_METHODS + WRAPPED_WRITE_METHODS + WRAPPED_BOTH_METHODS
+
+ # Either #close or #close_read was called.
+ def closed_read?
+ !respond_to?(:sysread) || !!@_closed_read
+ end
+
+ # Either #close or #close_write was called
+ def closed_write?
+ !respond_to?(:syswrite) || !!@_closed_write
+ end
+
+ # Object is closed for both reading and writing
+ def closed?
+ closed_read? && closed_write?
+ end
+
+ # Wrap an existing method in with a prerequisite that the file must not be
+ # closed.
+ #
+ # wrap_method_for_close(:printf, :closed_write?)
+ def self.wrap_method_for_close(method,condition = :closed?) #:nodoc:
+ class_eval(<<-EOF,__FILE__,__LINE__)
+ def with_io_sneakiness_do_#{method}(*args,&block)
+ raise IOError, "stream closed", caller if #{condition}
+ send(:without_io_sneakiness_do_#{method},*args,&block)
+ end
+ EOF
+ private "with_io_sneakiness_do_#{method}"
+ end
+
+ WRAPPED_READ_METHODS.each do |method|
+ wrap_method_for_close(method,:closed_read?)
+ end
+
+ WRAPPED_WRITE_METHODS.each do |method|
+ wrap_method_for_close(method,:closed_write?)
+ end
+
+ WRAPPED_BOTH_METHODS.each do |method|
+ wrap_method_for_close(method,:closed?)
+ end
+
+ def with_io_sneakiness_do_reopen(*args, &block) #:nodoc:
+ @_closed_read = nil
+ @_closed_write = nil
+ without_io_sneakiness_do_reopen(*args,&block)
+ end
+
+ # After this method is called, future read operations will fail, and
+ # #closed_read? will read return true. If this method is redefined, it
+ # will be transparently wrapped to preserve this behavior.
+ def close_read(*args,&block)
+ closed_io_error if closed_read?
+ @_closed_read = true
+ without_io_sneakiness_do_close_read(*args,&block) if respond_to?(:without_io_sneakiness_do_close_read)
+ without_io_sneakiness_do_close(*args,&block) if closed_write? && respond_to?(:without_io_sneakiness_do_close)
+ end
+
+ # After this method is called, future write operations will fail, and
+ # #closed_write? will write return true. If this method is redefined, it
+ # will be transparently wrapped to preserve this behavior.
+ def close_write(*args,&block)
+ closed_io_error if closed_write?
+ @_closed_write = true
+ without_io_sneakiness_do_close_write(*args,&block) if respond_to?(:without_io_sneakiness_do_close_write)
+ without_io_sneakiness_do_close(*args,&block) if closed_read? && respond_to?(:without_io_sneakiness_do_close)
+ end
+
+ # Calls both close_read and close_write.
+ def close(*args,&block)
+ closed_io_error if closed?
+ close_read(*args,&block) unless closed_read?
+ close_write(*args,&block) unless closed_write?
+ end
+
+ [:close_read, :close_write, :close].each do |method|
+ alias_method("with_io_sneakiness_do_#{method}", method)
+ end
+
+
+ def self.included(base) #:nodoc:
+ # p base.instance_methods.sort
+ WRAPPED_METHODS.each do |method|
+ if method_defined?(method) && !private_method_defined?(without) #&& private_method_defined?(with)
+ base.send(:alias_method, without, method)
+ base.send(:alias_method, method, with)
+ base.send(:private, with, without)
+ base.send(:public, method)
+ end
+ end
+ ::IO::Sneakiness.extend_into(base)
+end
+
+ private
+ def closed_io_error
+ raise IOError, "stream closed", caller[1..-1]
+ end
+
+end
+
+# The IO::Openable mixin is a set of _class_ methods concerning opening a
+# stream. As such, it should be +extend+ed, not +include+d into a class.
+#
+# class MyIO
+# include IO::Readable
+# include IO::Writable
+# include IO::Seekable
+# extend IO::Openable
+# # ...
+# end
+module IO::Openable
+ # Currently the only method, this method creates a new object, and yields
+ # it if a block is given, in a manner similar to IO::open.
+ def open(*args)
+ io = new(*args)
+ if block_given?
+ begin
+ yield io
+ ensure
+ io.close
+ end
+ else
+ io
+ end
+ end
+end
+
+# The IO::Editable mixin allows for full emulation of the IO class by
+# including IO::Readable, IO::Writable, IO::Seekable, and IO::Closable. The
+# following is a partial implementation of a looped IO class which writes to
+# its own input.
+#
+# :include:../../test/io_loop.rb
+module IO::Editable
+ REQUIRED_METHODS = IO::Readable::REQUIRED_METHODS + IO::Writable::REQUIRED_METHODS + IO::Seekable::REQUIRED_METHODS
+ if false # for rdoc
+ include Readable
+ include Writable
+ include Seekable
+ include Closable
+ end
+
+ # Adds an +open+ class method which acts like IO::open.
+ def self.included(base)
+ unless base.respond_to?(:open)
+ base.extend(IO::Openable)
+ [ IO::Readable, IO::Writable, IO::Seekable, IO::Closable ].each do |mod|
+ base.send(:include,mod)
+ # mod.send(:included,base)
+ end
+ end
+ super
+ end
+end
+
+# We nest these in IO because otherwise, :nodoc: is ignored.
+class IO #:nodoc:
+
+ MIXINS_VERSION = '0.3'
+ module Sneakiness #:nodoc:
+ def method_added(id)
+ method = id.id2name
+ with = "with_io_sneakiness_do_#{method}"
+ without = "without_io_sneakiness_do_#{method}"
+ if !@io_sneakiness_disabled && (method_defined?(with) || private_method_defined?(with))
+ alias_method without, method
+ disable_sneakiness do
+ alias_method method, with
+ private with, without
+ public method
+ end
+ end
+ method_added_without_io_sneakiness(method)
+ end
+
+ def self.extend_into(base)
+ unless base.respond_to?(:method_added_without_io_sneakiness)
+ class << base
+ alias_method :method_added_without_io_sneakiness, :method_added
+ end
+ base.extend(self)
+ end
+ end
+
+ protected
+ def disable_sneakiness
+ old_crit = Thread.critical
+ Thread.critical = true
+ @io_sneakiness_disabled = true
+ yield
+ ensure
+ @io_sneakiness_disabled = false
+ Thread.critical = old_crit if Thread.critical != old_crit
+ end
+
+ end
+
+ # Deprecated.
+ module Simple #:nodoc:
+ include IO::Editable
+
+ def sysread(length)
+ return "" if length == 0
+ @_simple_buffer ||= ""
+ while @_simple_buffer.length < length && chunk = (readchunk rescue nil)
+ break if chunk == ""
+ @_simple_buffer << chunk
+ end
+ out = @_simple_buffer.slice!(0,length)
+ if out.length == 0
+ raise EOFError, "end of file reached", caller
+ else
+ out
+ end
+ end
+
+ end
+end
+
+# end
--- /dev/null
+#!/usr/bin/ruby
+# $Id$
+# -*- ruby -*- vim:set ft=ruby et sw=2 sts=2:
+
+require 'io/mixins'
+require 'stringio'
+require 'forwardable'
+require 'test/unit'
+
+class SimpleIO
+ include IO::Readable
+ include IO::Writable
+ include IO::Seekable
+ extend IO::Openable
+ extend Forwardable
+ def initialize(*args)
+ @string = StringIO.new(*args)
+ end
+ def sysread(length)
+ @string.read(length) or raise EOFError, "end of file reached", caller
+ end
+ def syswrite(chunk)
+ @string.write(chunk)
+ end
+ def sysseek(offset, whence = IO::SEEK_SET)
+ @string.seek(offset,whence)
+ @string.tell
+ end
+ DELEGATORS = [:close_read,:close_write,:closed_read?,:closed_write?,:reopen]
+ def_delegators :@string, *DELEGATORS
+ # def close_read() @string.close_read end
+ # def close_write() @string.close_write end
+ # def closed_read?() @string.closed_read? end
+ # def closed_write?()@string.closed_write? end
+ # def close() @string.close end
+end
+
+require File.join(File.dirname(__FILE__),'rot13_filter')
+require File.join(File.dirname(__FILE__),'io_loop')
+require File.join(File.dirname(__FILE__),'read_joiner')
+
+class PairTester
+ def initialize(test,initial,*args)
+ @test = test
+ @string1 = initial.dup
+ @string2 = initial.dup
+ @s1 = StringIO.new(@string1,*args)
+ @s2 = SimpleIO.new(@string2,*args)
+ end
+
+ def perform(*args)
+ @s1.__send__(*args)
+ @s2.__send__(*args)
+ end
+
+ def assert_equal
+ @test.assert_equal @string1, @string2
+ end
+
+ def method_missing(*args)
+ value1 = @s1.__send__(*args) rescue $!.class
+ value2 = @s2.__send__(*args) rescue $!.class
+ value2 = value1 if value1 == @s1 && value2 == @s2
+ @test.assert_equal value1, value2
+ value1
+ end
+
+end
+
+class IOMixinsTest < Test::Unit::TestCase
+
+ def test_minimal
+ SimpleIO.open(":)\n") do |io|
+ assert_nothing_raised { io.gets }
+ assert_raise(NotImplementedError) { io.fcntl }
+ end
+ end
+
+ def test_enough_methods
+ junk = Object.instance_methods | Enumerable.instance_methods
+ good = IO.instance_methods(false) & StringIO.instance_methods(false)
+ mine = SimpleIO.instance_methods
+ assert_equal [], good - junk - mine
+ end
+
+ def test_io_loop
+ string = "foo\nbar\n"
+ io = IOLoop.open(string)
+ assert_equal ?f, io.getc
+ io.ungetc ?g
+ assert_equal ?g, io.getc
+ io.putc "lqqqqq" # Should use first character
+ assert_equal "o\n", io.gets
+ io.puts "baz\n"
+ assert_equal "flo\n", io.readline
+ assert_equal "baz\n", io.readline
+ io.close
+ io.instance_variable_set("@closed_read",false)
+ assert_equal true, io.closed_read?
+ assert_raise(IOError) { io.read }
+ io = IOLoop.open("foo")
+ assert_raise(IOError) { io.read }
+ end
+
+ def empty_file
+ File.open(RUBY_PLATFORM =~ /win32/ ? "NUL:" : "/dev/null")
+ end
+
+ def empty_string
+ StringIO.open("")
+ end
+
+ def empty_simple
+ SimpleIO.new("")
+ end
+
+ def assert_identical_response(object1,object2,method,*args,&block)
+ value1 = object1.send(method,*args,&block) rescue $!.class
+ value2 = object2.send(method,*args,&block) rescue $!.class
+ assert_equal value1, value2, "#{object1.class} gave #{value1.inspect} while #{object2.class} gave #{value2.inspect}"
+ end
+
+ def initialize_both(*args)
+ @both = PairTester.new(self,*args)
+ end
+
+ def test_simple_io_empty_read
+ [lambda {empty_file},lambda {empty_string}].each do |l|
+ assert_identical_response l.call,empty_simple,:read
+ assert_identical_response l.call,empty_simple,:read,0
+ assert_identical_response l.call,empty_simple,:read,2
+ end
+ end
+
+ def test_simple_io_read
+ initialize_both("Hello\nworld!")
+ @both.eof
+ @both.tty?
+ @both.lineno
+ @both.read
+ @both.eof?
+ @both.seek(2)
+ @both.pos
+ @both.rewind
+ @both.tell
+ @both.gets
+ @both.lineno
+ @both.read(4, "buffer")
+ @both.tell
+ @both.read(0)
+ @both.read(6)
+ @both.read
+ @both.eof
+ @both.read(0)
+ @both.rewind
+ @both.lineno
+ @both.read(8)
+ @both.read(0)
+ @both.read(1)
+ @both.readchar
+ @both.close_read
+ @both.close
+ @both.close_write
+ @both.closed_read?
+ @both.closed_write?
+ @both.sync
+ @both.closed?
+ end
+
+ def test_simple_io_gets
+ # io = SimpleIO.open(StringIO.new(<<-EOF))
+ string = <<-EOF
+
+
+Hello world!
+
+
+Chunky
+bacon!
+
+
+
+Goodbye world!
+ EOF
+ string.chomp!
+ initialize_both(string)
+ @both.gets ""
+ @both.readline ""
+ @both.map {|x|x}
+ @both.close
+ end
+
+ def test_simple_io_write
+ initialize_both("x","w")
+ @both.eof
+ @both.putc(42)
+ @both.eof # => IOError
+ @both.print("Hello ")
+ @both.binmode
+ @both.puts("world")
+ @both << ":-"
+ @both << ?)
+ @both.fsync
+ @both.printf("%4d",17)
+ @both.putc(1..3) # => TypeError
+ @both.putc(:bad) # => TypeError
+ @both.pos
+ @both.flush
+ @both.pos = 6
+ @both.putc(?_)
+ @both.close
+ @both.assert_equal
+ end
+
+ def test_simple_io_seek
+ initialize_both("Look ma!","r+")
+ @both.pos
+ @both.getc
+ @both.tell
+ @both.seek(3,IO::SEEK_SET)
+ @both.read(2)
+ @both.ungetc(?z)
+ @both.tell
+ @both.seek(-2,IO::SEEK_CUR)
+ @both.getc
+ @both.pos
+ @both.seek(-3,IO::SEEK_END)
+ @both.tell
+ @both.getc
+ end
+
+ def test_missing_methods
+ readable = Class.new do
+ include IO::Readable
+ end
+ writable = Class.new do
+ include IO::Writable
+ def close_write
+ if @closed_write
+ raise IOError
+ else
+ @closed_write = true
+ end
+ end
+ def closed_write?
+ !!@closed_write
+ end
+ end
+ streamable = Class.new(writable) do
+ include IO::Readable
+ def close_read
+ if @closed_read
+ raise IOError
+ else
+ @closed_read = true
+ end
+ end
+ def closed_read?
+ !!@closed_read
+ end
+ end
+ assert_raise(NotImplementedError) {readable.new.close}
+ assert_raise(NotImplementedError) {readable.new.closed?}
+ w = writable.new
+ assert_equal false, w.closed_write?
+ assert_nothing_thrown {w.close}
+ assert_raise(IOError) {w.close}
+ assert_equal true, w.closed?
+ s = streamable.new
+ assert_equal false, s.closed?
+ assert_nothing_thrown {s.close_read}
+ assert_nothing_thrown {s.close}
+ assert_raise(IOError) {s.close_write}
+ assert_raise(IOError) {s.close}
+ assert_equal true, s.closed?
+ end
+
+ def test_open_readers
+ assert_raise(EOFError) { SimpleIO.open("") {|io|io.sysread(1)}}
+ assert_equal "READ", SimpleIO.read("README",4)
+ assert_equal "x" * 1025, SimpleIO.read("x"*1025)
+ assert_equal ["foo\n","bar"], SimpleIO.readlines("foo\nbar")
+ assert_raise(LocalJumpError) { SimpleIO.foreach("foo\nbar") }
+ end
+
+ def test_rot13
+ string = "Puhaxl onpba!"
+ stringio = StringIO.open(string,"r")
+ filtered = Rot13Filter.open(stringio)
+ assert_equal "Chunky bacon!", filtered.read
+ stringio = StringIO.open(string,"r+")
+ filtered = Rot13Filter.open(stringio)
+ filtered.write("Rotten")
+ assert_equal "Ebggra onpba!", string
+ end
+
+ def test_joiner
+ joiner = ReadJoiner.new(StringIO.new("fo"),StringIO.new("o\nbar\n"))
+ assert_equal "foo\n", joiner.readline
+ assert_nil joiner.close
+ assert_equal true, joiner.closed?
+ end
+
+end