summaryrefslogtreecommitdiff
path: root/lib/net/ssh/test/extensions.rb
blob: b52323cb56f5e6dbd5055fd2386cba91d5673ab5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
require 'net/ssh/buffer'
require 'net/ssh/packet'
require 'net/ssh/buffered_io'
require 'net/ssh/connection/channel'
require 'net/ssh/connection/constants'
require 'net/ssh/transport/constants'
require 'net/ssh/transport/packet_stream'

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
    
          # 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
    
          alias select_for_write? select_for_write
          alias select_for_error? select_for_error
        end
    
        # 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 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?
    
            base.send :alias_method, :real_enqueue_packet, :enqueue_packet
            base.send :alias_method, :enqueue_packet, :test_enqueue_packet
    
            base.send :alias_method, :real_poll_next_packet, :poll_next_packet
            base.send :alias_method, :poll_next_packet, :test_poll_next_packet
          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)
    
            if script.next(:first).remote?
              self.string << script.next.to_s
              self.pos = pos
            end
    
            return true
          end
    
          # The testing version of Net::SSH::Transport::PacketStream#available_for_read?.
          # Returns true if there is data pending to be read. Otherwise calls #idle!.
          def test_available_for_read?
            return true if select_for_read?
            idle!
            false
          end
    
          # The testing version of Net::SSH::Transport::PacketStream#enqueued_packet.
          # Simply calls Net::SSH::Test::Script#process on the packet.
          def test_enqueue_packet(payload)
            packet = Net::SSH::Buffer.new(payload.to_s)
            script.process(packet)
          end
    
          # The testing version of Net::SSH::Transport::PacketStream#poll_next_packet.
          # Reads the next available packet from the IO object and returns it.
          def test_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
    
        # An extension to Net::SSH::Connection::Channel. Facilitates unit testing.
        module Channel
          def self.included(base) #:nodoc:
            base.send :alias_method, :send_data_for_real, :send_data
            base.send :alias_method, :send_data, :send_data_for_test
          end
    
          # The testing version of Net::SSH::Connection::Channel#send_data. Calls
          # the original implementation, and then immediately enqueues the data for
          # output so that scripted sends are properly interpreted as discrete
          # (rather than concatenated) data packets.
          def send_data_for_test(data)
            send_data_for_real(data)
            enqueue_pending_output
          end
        end
    
        # An extension to the built-in ::IO class. Simply redefines IO.select
        # so that it can be scripted in Net::SSH unit tests.
        module IO
          def self.included(base) #:nodoc:
            base.extend(ClassMethods)
          end
    
          @extension_enabled = false
    
          def self.with_test_extension(&block)
            orig_value = @extension_enabled
            @extension_enabled = true
            begin
              yield
            ensure
              @extension_enabled = orig_value
            end
          end
    
          def self.extension_enabled?
            @extension_enabled
          end
    
          module ClassMethods
            def self.extended(obj) #:nodoc:
              class <<obj
                alias_method :select_for_real, :select
                alias_method :select, :select_for_test
              end
            end
    
            # The testing version of ::IO.select. Assumes that all readers,
            # writers, and errors arrays are either nil, or contain only objects
            # that mix in Net::SSH::Test::Extensions::BufferedIo.
            def select_for_test(readers=nil, writers=nil, errors=nil, wait=nil)
              return select_for_real(readers, writers, errors, wait) unless Net::SSH::Test::Extensions::IO.extension_enabled?
              ready_readers = Array(readers).select { |r| r.select_for_read? }
              ready_writers = Array(writers).select { |r| r.select_for_write? }
              ready_errors  = Array(errors).select  { |r| r.select_for_error? }
    
              return [ready_readers, ready_writers, ready_errors] if ready_readers.any? || ready_writers.any? || ready_errors.any?
    
              processed = 0
              Array(readers).each do |reader|
                processed += 1 if reader.idle!
              end
    
              raise "no readers were ready for reading, and none had any incoming packets" if processed == 0 && wait != 0
            end
          end
        end
      end

end; end; end

Net::SSH::BufferedIo.send(:include, Net::SSH::Test::Extensions::BufferedIo)
Net::SSH::Transport::PacketStream.send(:include, Net::SSH::Test::Extensions::PacketStream)
Net::SSH::Connection::Channel.send(:include, Net::SSH::Test::Extensions::Channel)
IO.send(:include, Net::SSH::Test::Extensions::IO)