lib/tryouts/tryout.rb



class Tryouts
  
  # = Tryout
  #
  # A Tryout is a set of drills (each drill is a test). 
  #
  class Tryout
  
    # The name of this tryout
  attr_reader :name
    # A default value for Drill.dtype
  attr_reader :dtype
    # A block to executed one time before starting the drills
  attr_reader :setup
    # A block to executed one time before starting the drills
  attr_reader :clean
    # An Array of Drill objects
  attr_reader :drills
    # The number of dreams that came true (successful drills)
  attr_reader :passed
    # The number of dreams that did not come true (failed drills)
  attr_reader :failed
    # For drill type :cli, this is the name of the command to test. It
    # should be a valid method available to a Rye::Box object.
    # For drill type :api, this attribute is ignored. 
  attr_reader :command
    # A Hash of Dream objects for this Tryout. The keys are drill names. 
  attr_accessor :dreams
  
  @@valid_dtypes = [:cli, :api]
  
  # All :api Drills are run within this context (not used for :cli). 
  # Each Drill is executed in a new instance of this class. That means
  # instance variables are not carried through, but class variables are. 
  # The before and after blocks are also run in this context.
  class DrillContext
      # An ordered Hash of stashed objects. 
    attr_reader :stash
    def initialize; @stash = Tryouts::HASH_TYPE.new; end
    # If called with no arguments, returns +@stash+. 
    # If called with arguments, it will add a new value to the +@stash+
    # and return the new value.  e.g.
    #
    #     stash :name, 'some value'   # => 'some value'
    #
    def stash(*args)
      if args.empty?
        @stash
      else
        @stash[args[0]] = args[1] 
        args[1] 
      end
    end
  end
     
  def initialize(name, dtype, command=nil, *args)
    raise "Must supply command for dtype :cli" if dtype == :cli && command.nil?
    raise "#{dtype} is not a valid drill type" if !@@valid_dtypes.member?(dtype)
    @name, @dtype, @command = name, dtype, command
    @drills, @dreams = [], {}
    @passed, @failed = 0, 0
  end
  
  ## ---------------------------------------  EXTERNAL API  -----
  
  # Populate this Tryout from a block. The block should contain calls to 
  # the external DSL methods: dream, drill, xdrill
  def from_block(b=nil, &inline)
    runtime = b.nil? ? inline : b
    instance_eval &runtime
  end
  
  # Execute all Drill objects
  def run
    update_drills!   # Ensure all drills have all known dreams
    DrillContext.module_eval &setup if setup.is_a?(Proc)
    puts Tryouts::TRYOUT_MSG.bright % @name
    @drills.each do |drill|
      drill.run(DrillContext.new)      # Returns true or false
      drill.reality.stash.each_pair do |n,v|
        puts '%14s: %s' % [n,v.inspect]
      end
      drill.success? ? @passed += 1 : @failed += 1
    end
    DrillContext.module_eval &clean if clean.is_a?(Proc)
  end
  
  # Prints error output. If there are no errors, it prints nothing. 
  def report
    return true if success?
    failed = @drills.select { |d| !d.success? }
    failed.each_with_index do |drill,index|
      title = ' %-59s' % %Q{ERROR #{index+1}/#{failed.size} "#{drill.name}"}
      puts $/, ' ' << title.color(:red).att(:reverse)
      
      if drill.dream
        puts '%24s: %s (expected %s)' % ["response code", drill.reality.rcode, drill.dream.rcode]
        puts '%24s: %s' % ["expected output", drill.dream.output.inspect]
        puts '%24s: %s' % ["actual output", drill.reality.output.inspect]
        if drill.reality.emsg || (drill.reality.emsg != drill.dream.emsg)
          puts '%24s: %s' % ["expected error msg", drill.dream.emsg.inspect]
            puts '%24s: %s' % ["actual error msg", drill.reality.emsg.inspect]
        end
        
        if drill.reality.rcode > 0
          puts '%24s: ' % ["backtrace"]
          puts drill.reality.backtrace, $/
        end
      else
        puts '%24s' % ["[nodream]"]
      end
      
    end
    false
  end
  
  # Did every Tryout finish successfully?
  def success?
    return @success unless @success.nil?
    # Returns true only when every Tryout result returns true
    @success = !(@drills.collect { |r| r.success? }.member?(false))
  end
  
  # Add a Drill object to the list for this Tryout. If there is a dream
  # defined with the same name as the Drill, that dream will be given to
  # the Drill before its added to the list. 
  def add_drill(d)
    d.add_dream @dreams[d.name] if !@dreams.nil? && @dreams.has_key?(d.name)
    drills << d if d.is_a?(Tryouts::Drill)
    d
  end
  
  # Goes through the list of Drill objects (@drills) and gives each 
  # one its associated Dream object (if available). 
  # 
  # This method is called before Tryout#run, but is only necessary in  
  # the case where dreams where loaded after the drills were defined. 
  def update_drills!
    return if @dreams.nil?
    @drills.each do |drill|
      next unless @dreams.has_key?(drill.name)
      drill.add_dream @dreams[drill.name]
    end
  end
  
  ## ---------------------------------------  EXTERNAL DSL  -----
  
  # A block to executed one time _before_ starting the drills
  def setup(&block)
    return @setup unless block
    @setup = block
  end
  
  # A block to executed one time _after_ the drills
  def clean(&block)
    return @clean unless block
    @clean = block
  end
  
  # Create and add a Drill object to the list for this Tryout
  # +name+ is the name of the drill. 
  # +args+ is sent directly to the Drill class. The values are specific on the Sergeant.
  def drill(dname, *args, &definition)
    raise "Empty drill name (#{@name})" if dname.nil? || dname.empty?
    args.unshift(@command) if @dtype == :cli
    drill = Tryouts::Drill.new(dname, @dtype, *args, &definition)
    add_drill drill
  end
  # A quick way to comment out a drill
  def xdrill(*args, &b); end # ignore calls to xdrill
  
  # +name+ of the Drill associated to this Dream
  # +output+ A String or Array of expected output. A Dream object will be created using this value (optional)
  # +definition+ is a block which will be run on an instance of Dream
  #
  # NOTE: This method is DSL-only. It's not intended to be used in OO syntax. 
  def dream(dname, output=nil, format=nil, rcode=0, emsg=nil, &definition) 
    raise "Empty dream name (#{@name})" if dname.nil? || dname.empty?
    if output.nil?
      raise "No output or block for '#{dname}' (#{@name})" if definition.nil?
      dobj = Tryouts::Drill::Dream.from_block definition
    else
      dobj = Tryouts::Drill::Dream.new(output)
      dobj.format, dobj.rcode, dobj.emsg = format, rcode, emsg
    end
    @dreams[dname] = dobj
    dobj
  end
  # A quick way to comment out a dream
  def xdream(*args, &b); end
  
end; end