From c3e39e668936f22f348138a9b857631e6ac99ef8 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Fri, 21 Mar 2008 23:15:58 -0600 Subject: [PATCH] document Net::SSH::Test::Channel. Refactor Test::Extensions to use modules rather than direct monkeypatching, and add documentation. --- lib/net/ssh/test/channel.rb | 70 +++++++++++- lib/net/ssh/test/extensions.rb | 193 ++++++++++++++++++++++----------- 2 files changed, 197 insertions(+), 66 deletions(-) diff --git a/lib/net/ssh/test/channel.rb b/lib/net/ssh/test/channel.rb index 0f3ec2940..261d8f380 100644 --- a/lib/net/ssh/test/channel.rb +++ b/lib/net/ssh/test/channel.rb @@ -1,58 +1,126 @@ module Net; module SSH; module Test + # A mock channel, used for scripting actions in tests. It wraps a + # Net::SSH::Test::Script instance, and delegates to it for the most part. + # This class has little real functionality on its own, but rather acts as + # a convenience for scripting channel-related activity for later comparison + # in a unit test. + # + # story do |session| + # channel = session.opens_channel + # channel.sends_exec "ls" + # channel.gets_data "result of ls" + # channel.gets_close + # channel.sends_close + # end class Channel + # The Net::SSH::Test::Script instance employed by this mock channel. attr_reader :script - attr_writer :local_id, :remote_id + # Sets the local-id of this channel object (the id assigned by the client). + attr_writer :local_id + + # Sets the remote-id of this channel object (the id assigned by the mock-server). + attr_writer :remote_id + + # Creates a new Test::Channel instance on top of the given +script+ (which + # must be a Net::SSH::Test::Script instance). def initialize(script) @script = script @local_id = @remote_id = nil end + # Returns the local (client-assigned) id for this channel, or a Proc object + # that will return the local-id later if the local id has not yet been set. + # (See Net::SSH::Test::Packet#instantiate!.) def local_id @local_id || Proc.new { @local_id or raise "local-id has not been set yet!" } end + # Returns the remote (server-assigned) id for this channel, or a Proc object + # that will return the remote-id later if the remote id has not yet been set. + # (See Net::SSH::Test::Packet#instantiate!.) def remote_id @remote_id || Proc.new { @remote_id or raise "remote-id has not been set yet!" } end + # Because adjacent calls to #gets_data will sometimes cause the data packets + # to be concatenated (causing expectations in tests to fail), you may + # need to separate those calls with calls to #inject_remote_delay! (which + # essentially just mimics receiving an empty data packet): + # + # channel.gets_data "abcdefg" + # channel.inject_remote_delay! + # channel.gets_data "hijklmn" def inject_remote_delay! gets_data("") end + # Scripts the sending of an "exec" channel request packet to the mock + # server. If +reply+ is true, then the server is expected to reply to the + # request, otherwise no response to this request will be sent. If +success+ + # is +true+, then the request will be successful, otherwise a failure will + # be scripted. + # + # channel.sends_exec "ls -l" def sends_exec(command, reply=true, success=true) script.sends_channel_request(self, "exec", reply, command, success) end + # Scripts the sending of a "subsystem" channel request packet to the mock + # server. See #sends_exec for a discussion of the meaning of the +reply+ + # and +success+ arguments. + # + # channel.sends_subsystem "sftp" def sends_subsystem(subsystem, reply=true, success=true) script.sends_channel_request(self, "subsystem", reply, subsystem, success) end + # Scripts the sending of a data packet across the channel. + # + # channel.sends_data "foo" def sends_data(data) script.sends_channel_data(self, data) end + # Scripts the sending of an EOF packet across the channel. + # + # channel.sends_eof def sends_eof script.sends_channel_eof(self) end + # Scripts the sending of a "channel close" packet across the channel. + # + # channel.sends_close def sends_close script.sends_channel_close(self) end + # Scripts the reception of a channel data packet from the remote end. + # + # channel.gets_data "bar" def gets_data(data) script.gets_channel_data(self, data) end + # Scripts the reception of an "exit-status" channel request packet. + # + # channel.gets_exit_status(127) def gets_exit_status(status=0) script.gets_channel_request(self, "exit-status", false, status) end + # Scripts the reception of an EOF packet from the remote end. + # + # channel.gets_eof def gets_eof script.gets_channel_eof(self) end + # Scripts the reception of a "channel close" packet from the remote end. + # + # channel.gets_close def gets_close script.gets_channel_close(self) end diff --git a/lib/net/ssh/test/extensions.rb b/lib/net/ssh/test/extensions.rb index 352574de9..e19a98636 100644 --- a/lib/net/ssh/test/extensions.rb +++ b/lib/net/ssh/test/extensions.rb @@ -6,84 +6,147 @@ require 'net/ssh/transport/constants' require 'net/ssh/transport/packet_stream' -module Net::SSH::BufferedIo - def select_for_read? - pos < size - end +module Net; module SSH; module Test + + # A collection of modules used to extend/override the default behavior of + # Net::SSH internals for ease of testing. As a consumer of Net::SSH, you'll + # never need to use this directly--they're all used under the covers by + # the Net::SSH::Test system. + module Extensions + + # An extension to Net::SSH::BufferedIo (assumes that the underlying IO + # is actually a StringIO). Facilitates unit testing. + module BufferedIo + # Returns +true+ if the position in the stream is less than the total + # length of the stream. + def select_for_read? + pos < size + end - attr_accessor :select_for_write, :select_for_error - alias select_for_write? select_for_write - alias select_for_error? select_for_error -end + # Set this to +true+ if you want the IO to pretend to be available for writing + attr_accessor :select_for_write + # Set this to +true+ if you want the IO to pretend to be in an error state + attr_accessor :select_for_error -module Net::SSH::Transport::PacketStream - include Net::SSH::Connection::Constants - include Net::SSH::Transport::Constants + alias select_for_write? select_for_write + alias select_for_error? select_for_error + end - MAP = Hash.new { |h,k| const_get(k.to_s.upcase) } + # An extension to Net::SSH::Transport::PacketStream (assumes that the + # underlying IO is actually a StringIO). Facilitates unit testing. + module PacketStream + include BufferedIo # make sure we get the extensions here, too - def idle! - return false unless script.next(:first) + def self.included(base) #:nodoc: + base.send :alias_method, :real_available_for_read?, :available_for_read? + base.send :alias_method, :available_for_read?, :test_available_for_read? - if script.next(:first).remote? - self.string << script.next.to_s - self.pos = pos - end + base.send :alias_method, :real_enqueue_packet, :enqueue_packet + base.send :alias_method, :enqueue_packet, :test_enqueue_packet - return true - end + base.send :alias_method, :real_poll_next_packet, :poll_next_packet + base.send :alias_method, :poll_next_packet, :test_poll_next_packet + end - alias real_available_for_read? available_for_read? - def available_for_read? - return true if select_for_read? - idle! - false - end + # Called when another packet should be inspected from the current + # script. If the next packet is a remote packet, it pops it off the + # script and shoves it onto this IO object, making it available to + # be read. + def idle! + return false unless script.next(:first) - alias real_enqueue_packet enqueue_packet - def enqueue_packet(payload) - packet = Net::SSH::Buffer.new(payload.to_s) - script.process(packet) - end + if script.next(:first).remote? + self.string << script.next.to_s + self.pos = pos + end - alias real_poll_next_packet poll_next_packet - def poll_next_packet - return nil if available <= 0 - packet = Net::SSH::Buffer.new(read_available(4)) - length = packet.read_long - Net::SSH::Packet.new(read_available(length)) - end -end - -class Net::SSH::Connection::Channel - alias original_send_data send_data - def send_data(data) - original_send_data(data) - # force each packet of sent data to be enqueued separately, so that - # scripted sends are properly interpreted. - enqueue_pending_output - end -end - -class IO - class <