ONLamp.com
oreilly.comSafari Books Online.Conferences.

advertisement


Unit Testing Your Documentation
Pages: 1, 2, 3, 4

Scripting an irb Session

Now the program has a list of Assertion objects: chunks of code and the expected values when that code is run. We wrote the Ruby Cookbook code to run in an irb session, so that's how I tested it. I decided it would be easier to script an irb session than to figure out how to get the equivalent behavior out of eval.



The hardest part of scripting an irb session is knowing what to override. The class to modify is IRB::Irb. To get an Irb instance to accept an alternate source of input, give it an input class that supports the methods gets and prompt=.

Comparing the actual result of an expression to the expected result is also easy. The value of the most recent expression in an irb session is available through the Irb object's instance variable @context.

Here's an Irb subclass that takes a harness variable as its input source and sets up the interpreter to use it. The irb code passes HarnessedIrb#output_value the value of every expression it runs. To keep this class simple, its output_value implementation simply delegates to the harness class. This class and an appropriate harness are all you need to instrument an irb session and inspect the output of the expressions.

require 'irb'
class HarnessedIrb < IRB::Irb

  def initialize(harness)
    IRB.setup(__FILE__)
    # Prevent Ruby code from being echoed to standard output.
    IRB.conf[:VERBOSE] = false
    @harness = harness
    super(nil, harness)
  end

  def output_value
    @harness.output_value(@context.last_value)
  end

  def run
    IRB.conf[:MAIN_CONTEXT] = self.context
    eval_input
  end
end

Here's an AssertionTestingHarness class that takes a list of Assertion objects and feeds the code into irb, one line at a time.

@require 'stringio'

class AssertionTestingHarness
  def initialize(assertions)
    @assertions = assertions
    @assertion_counter, @line_counter = 0
    @keep_feeding = false
    $stdout = StringIO.new
  end

  # Called when irb wants a line of input.
  def gets
    line = nil
    assertion = @assertions[@assertion_counter]
    @line_counter += 1 if @keep_feeding
    line = assertion[:code][@line_counter] + "\n" if assertion
    @keep_feeding = true
    return line
  end

  # Called by irb to display a prompt to the end-user. We have no
  # end-user, and so no prompt. Strangely, irb knows that we have no
  # prompt, but it calls this method anyway.
  def prompt=(x)
  end

The irb interpreter calls output_value every time it evaluates a line of code, but nothing happens except on the final line of a code chunk, when it's time to test the assertion.

  # Compare a value received by irb to the expected value of the
  # current assertion.
  def output_value(value)
    begin
      assertion = @assertions[@assertion_counter]
      if @line_counter < assertion[:code].size - 1
        # We have more lines of code to run before we can test an assertion.
        @line_counter += 1

The interpreter passes the result of the Ruby expression as an argument to output_value. If the assertion is a :value-type assertion, the harness simply compares the expected value to that argument.

If the assertion is a :stdout-type, then it is ignorable. Instead, the harness captures the standard output gathered during the code chunk, and compares that to the expected value. This is why the initialize method replaces $stdout with a StringIO object.

      else
        # We're done with this code chunk; it's time to check its assertion.
        value = value.inspect
        if assertion[:how] == :value
          # Compare expected to actual expression value
          actual = value.strip
        else
          # Compare expected to actual standard output.
          actual = $stdout.string.strip
        end
        report_assertion(assertion, actual)
        # Reset standard output and move on to the next code chunk
        @assertion_counter += 1
        @line_counter = 0
        $stdout.string = ""
      end
    rescue Exception => e
      # Restore standard output and re-throw the exception.
      $stdout = STDOUT
      raise e
    end
    @keep_feeding = false
  end

The report_assertion method compares an assertion to reality. When testing the book, my harness printed out an HTML report for each recipe, flagging failed assertions in red (here's the report for "Word-Wrapping Lines of Text"). The implementation presented here is much simpler; it inspects the assertion in light of the code chunk's actual value. A third implementation might make a Test::Unit assertion here.

  # Compare the expected value of an assertion to the actual value.
  def report_assertion(assertion, actual)
    STDOUT.puts assertion.inspect(actual)
  end
end

Finally, here is code to test standard input when running this code as a script.

if $0 == __FILE__
  assertions = AssertionParser.new($stdin.read).assertions
  HarnessedIrb.new(AssertionTestingHarness.new(assertions)).run
end

Run a recipe into this script to extract, evaluate, and test its code listings. The following is the result of this script run against the sample code from "Word-Wrapping Lines of Text" given above. There are five lines of Ruby code here and two assertions:

*** When I run this code: ***
def wrap(s, width=78)
  s.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
end
wrap("This text is too short to be wrapped.")
*** I expect this value: ***
"This text is too short to be wrapped.\n"
*** What I get is this: ***
"This text is too short to be wrapped.\n"

*** When I run this code: ***
puts wrap("This text is not too short to be wrapped.", 20)
*** I expect this stdout: ***
This text is not too
short to be wrapped.
*** What I get is this: ***
This text is not too
short to be wrapped.

Pages: 1, 2, 3, 4

Next Pagearrow





Sponsored by: