From: Tim Pope Date: Tue, 17 Oct 2006 20:39:08 +0000 (+0000) Subject: Initial import X-Git-Url: http://git.tpope.net/?p=ruby-io-mixins.git;a=commitdiff_plain;h=HEAD Initial import --- 265855d776bec64274fa64c1bd03dcc43d245f0a diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bbf483 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/doc +/pkg +/coverage diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..bf70634 --- /dev/null +++ b/Rakefile @@ -0,0 +1,117 @@ +begin + require 'rubygems' +rescue LoadError +end +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/contrib/sshpublisher' +# require 'rake/contrib/rubyforgepublisher' +require File.join(File.dirname(__FILE__), 'lib', 'io', 'mixins') + +PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' +PKG_NAME = 'io-mixins' +PKG_VERSION = IO::MIXINS_VERSION +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" +# PKG_DESTINATION = ENV["PKG_DESTINATION"] || "../#{PKG_NAME}" + +# RELEASE_NAME = "REL #{PKG_VERSION}" + +RUBY_FORGE_PROJECT = PKG_NAME +RUBY_FORGE_USER = "tpope" + +desc "Default task: test" +task :default => [ :test ] + + +# Run the unit tests +Rake::TestTask.new { |t| + t.libs << "test" + t.test_files = Dir['test/*_test.rb'] + Dir['test/test_*.rb'] + t.verbose = true +} + + +# Generate the RDoc documentation +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.rdoc_files.add('lib') + rdoc.main = "IO" + rdoc.title = "IO Mixins" + rdoc.options << '--inline-source' + rdoc.options << '-d' if `which dot` =~ /\/dot/ +} + +desc "Generate the RDoc documentation for RI" +task :ri do + system("rdoc","--ri","lib") +end + + + +# Create compressed packages +spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = PKG_NAME + s.summary = 'Pure Ruby IO Mixins' + s.description = 'IO::Readable, IO::Writable, and IO::Seekable' + s.version = PKG_VERSION + + s.author = 'Tim Pope' + s.email = 'r*by@tpope.in#o'.tr('*#','uf') + s.rubyforge_project = RUBY_FORGE_PROJECT + + s.has_rdoc = true + # s.requirements << 'none' + s.require_path = 'lib' + # s.autorequire = 'action_web_service' + + s.files = [ "Rakefile", "setup.rb" ] + s.files = s.files + Dir.glob( "lib/**/*.rb" ) + s.files = s.files + Dir.glob( "test/**/*" ).reject { |item| item.include?( "\.svn" ) } +end + +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + p.need_zip = true +end + + +# Publish beta gem +desc "Publish the gem" +task :pgem => [:package] do + Rake::SshFilePublisher.new("tpope#{'@'}tpope.us", "public_html/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload + # `ssh tpope@tpope.us './gemupdate.sh'` +end + +# Publish documentation +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::SshDirPublisher.new("tpope#{'@'}tpope.us", "public_html/#{PKG_NAME}", "doc").upload +end + +# desc "Publish the release files to RubyForge." +# task :release => [ :package ] do + # `rubyforge login` + + # for ext in %w( gem tgz zip ) + # release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" + # puts release_command + # system(release_command) + # end +# end + +begin + require 'rcov/rcovtask' + Rcov::RcovTask.new do |t| + t.test_files = Dir['test/*_test.rb'] + Dir['test/test_*.rb'] + t.verbose = true + # t.rcov_opts << "--text-report" + # t.rcov_opts << "--exclude \\\\A/var/lib/gems" + t.rcov_opts << "--exclude '/(active_record|active_support)\\b'" + end +rescue LoadError +end diff --git a/lib/io/mixins.rb b/lib/io/mixins.rb new file mode 100644 index 0000000..6dad9a9 --- /dev/null +++ b/lib/io/mixins.rb @@ -0,0 +1,659 @@ +# 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, closed_read?, +# closed?, and close 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.lengthclosed_write?, closed?, 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 +# sysseek(0,IO::SEEK_CUR). 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 diff --git a/test/io_loop.rb b/test/io_loop.rb new file mode 100644 index 0000000..5957ece --- /dev/null +++ b/test/io_loop.rb @@ -0,0 +1,61 @@ +require 'io/mixins' +class IOLoop + + include IO::Editable + + attr_reader :string + + def initialize(string = "") + @string = string + @pos = 0 + end + + def sysseek(offset, whence = IO::SEEK_SET) + @pos = case whence + when IO::SEEK_SET then offset + when IO::SEEK_CUR then @pos + offset + when IO::SEEK_END then @string.length + offset + end % @string.size + end + + # def eof + # @string.nil? || @string.empty? + # end + + def putchar(char) + @string[@pos,1] = char.respond_to?(:chr) ? char.chr : char[0,0] + @pos += 1 + @pos %= @string.length + char + end + private :putchar + + def syswrite(string) + string.each_byte do |byte| + putchar(byte) + end + end + + def sysread(length) + raise IOError if @string.length < length + if length <= @string.length - @pos + out = @string[@pos,length] + @pos += out.length + @pos %= @string.length + return out + else + out = @string[@pos..-1] + @pos = 0 + out << @string[0,length-out.length] + end + end + + private + # def ensure_open + # raise IOError, "closed stream" if closed? + # end + + alias close_read close + alias close_write close + +end diff --git a/test/io_mixins_test.rb b/test/io_mixins_test.rb new file mode 100755 index 0000000..f43268b --- /dev/null +++ b/test/io_mixins_test.rb @@ -0,0 +1,304 @@ +#!/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 diff --git a/test/read_joiner.rb b/test/read_joiner.rb new file mode 100644 index 0000000..1d13543 --- /dev/null +++ b/test/read_joiner.rb @@ -0,0 +1,40 @@ +class ReadJoiner + include IO::Readable + extend IO::Openable + + def initialize(*streams) + @streams = streams + rewind + end + + def sysread(length) + buffer = "" + return buffer if length == 0 + while buffer.length < length && @streams[@index] + begin + buffer << @streams[@index].sysread(length-buffer.length) + rescue EOFError + @index += 1 + end + end + return buffer if buffer.length == length || buffer.length > 0 + raise EOFError, "end of file reached", caller + end + + def close_read + @streams.each {|s| s.close} + @index = @streams.size - 1 if @index >= @streams.size + nil + end + + def closed_read? + @streams.all? {|s|s.closed?} + end + + def rewind + @streams.each {|s| s.rewind} + self.lineno = @index = 0 + end + +end + diff --git a/test/rot13_filter.rb b/test/rot13_filter.rb new file mode 100644 index 0000000..3aa05c0 --- /dev/null +++ b/test/rot13_filter.rb @@ -0,0 +1,37 @@ +require 'io/mixins' + +class Rot13Filter + include IO::Readable + include IO::Writable + include IO::Seekable + extend IO::Openable + + def initialize(ios) + @ios = ios + end + + def sysread(length) + rot13(@ios.sysread(length)) + end + def syswrite(string) + @ios.syswrite(rot13(string)) + end + def sysseek(*args) + if @ios.respond_to?(:sysseek) + @ios.sysseek(*args) + else + # StringIO is missing sysseek + @ios.seek(*args) + @ios.tell + end + end + def close_read() @ios.close_read end + def close_write() @ios.close_write end + def close() @ios.close end + + private + def rot13(string) + string.tr("A-Za-z","N-ZA-Mn-za-m") + end +end +