=begin rdoc = Expect Given an IO object, read data from that object and expect certain data. All expected data is expected in 'chains'. When a message is received, it is compared to the pattern at the top of each chain. If it matches one of the patterns, then all is well, but an error message is printed otherwise. A message can match patterns in more than one chain. == Synopsis require 'carat/expect' # to do == Legal Adopted from Ruby Treasures 0.4 Copyright (C) 2002 Paul Brannan You may distribute this software under the same terms as Ruby (see the file COPYING that was distributed with this library). == History $Id: expect.rb, v0.4 2004/11/30 transami Exp $ =end class Expect public VERSION = "0.4" class UnmatchedMessage < Exception attr_reader :msg def initialize(msg) @msg = msg super("Message not matched: `#{msg}'") end end class Timeout < Exception attr_reader :chain attr_reader :pattern def initialize(chain, pattern) @chain = chain @pattern = pattern super("Timed out waiting for #{pattern.inspect} in chain #{chain.inspect}") end end ## # A special type of pattern that matches a list of patterns, separated # by a given separator, in any order. # # Example: #
  #   # matches 'foo,bar' and 'bar,foo'
  #   p = Expect::SeparatedPattern.new(['foo', 'bar'])
  #   
  #   e = Expect.new(nil, nil)
  #   e.expect(p)
  # 
# class SeparatedPattern attr_accessor :separator def initialize(message, separator = ',') @message = message @separator = separator end def ===(line) message = @message.dup # Iterate through each field to make sure it matches. When # we find a match, delete that field so we never check it # again. for field in fields do message.each_index do |index| if message[index] === field then message.delete_at(index) end end end # If there are no more fields, then we deleted all of them, # and we got a match return message.length == 0 end end class DefaultChain end ## # It is highly recommended that this be an immutable object. # attr_accessor :timeout ## # Create a new Expect_Test object. # # @param io_object an IO object to read from. # @param reactor a reactor to use # def initialize(io_object, reactor) @io_object = io_object @reactor = reactor @reactor.register_read(@io_object, method(:handle_event)) reset() end ## # Reset the the ignore chain and all expect chains. # def reset() @expect_chain = Hash.new @ignore_chain = Array.new @expects_left = 0 end ## # Expect a message matching pattern in a certain chain. # # @param pattern the pattern to expect. # @param chain the chain in which to expect the message. # def expect(pattern, chain=DefaultChain) @expect_chain[chain] ||= Array.new @expect_chain[chain].push(pattern) @expects_left += 1 end ## # Ignore a pattern in all chains. # # @param pattern the pattern to ignore. # def ignore(pattern) @ignore_chain.push(pattern) end def run(timeout = nil) return if @expects_left == 0 catch :expect_loop_done do @reactor.run_event_loop(timeout) for key, chain in @expect_chain do next if chain.length == 0 raise Timeout.new(key, chain.first) end # TODO: What does it mean if we got here? end end def handle_event(io_object) handle_line(io_object.gets()) end def handle_line(line) line.chomp!() # Check each element in the ignore chain for pattern in @ignore_chain do return if pattern === line end # Check the first element of each expect chain to see if # the message matches match = false for key, chain in @expect_chain do next if chain.length == 0 if chain.first() === line then chain.shift() @expects_left -= 1 match = true end end throw :expect_loop_done if @expects_left == 0 raise UnmatchedMessage.new(line) if match == false end end