class UnitDiff

UnitDiff makes reading Test::Unit output easy and fun. Instead of a confusing jumble of text with nearly unnoticable changes like this:

1) Failure:
test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
<"new GPolyline([\n  new GPoint(  47.00000, -122.00000),\n  new GPoint(  46.5000
0, -122.50000),\n  new GPoint(  46.75000, -122.75000),\n  new GPoint(  46.00000,
 -123.00000)])"> expected but was
<"new Gpolyline([\n  new GPoint(  47.00000, -122.00000),\n  new GPoint(  46.5000
0, -122.50000),\n  new GPoint(  46.75000, -122.75000),\n  new GPoint(  46.00000,
 -123.00000)])">.

You get an easy-to-read diff output like this:

1) Failure:
test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
1c1
< new GPolyline([
---
> new Gpolyline([

Usage

test.rb | unit_diff [options]
  options:
  -b ignore whitespace differences
  -c contextual diff
  -h show usage
  -k keep temp diff files around
  -l prefix line numbers on the diffs
  -u unified diff [default]
  -p plain diff
  -v display version

Constants

DIFF
WINDOZE

Public Class Methods

unit_diff() click to toggle source

Handy wrapper for UnitDiff#unit_diff.

# File lib/unit_diff.rb, line 57
def self.unit_diff
  trap 'INT' do exit 1 end
  puts UnitDiff.new.unit_diff
end

Public Instance Methods

diff(expect, butwas) click to toggle source
# File lib/unit_diff.rb, line 232
def diff expect, butwas
  output = nil

  Tempfile.open("expect") do |a|
    a.write(massage(expect))
    a.rewind
    Tempfile.open("butwas") do |b|
      b.write(massage(butwas))
      b.rewind

      diff_flags = $p ? "" : $c ? "-c" : "-u"
      diff_flags += " -b" if $b

      result = `#{DIFF} #{diff_flags} #{a.path} #{b.path}`
      result.sub!(/^\-\-\- .+/, "--- expected")
      result.sub!(/^\+\+\+ .+/, "+++ actual")

      output = if result.empty? then
                 "[no difference--suspect ==]"
               else
                 result.split(/\n/)
               end

      if $k then
        warn "moving #{a.path} to #{a.path}.keep"
        File.rename a.path, a.path + ".keep"
        warn "moving #{b.path} to #{b.path}.keep"
        File.rename b.path, b.path + ".keep"
      end
    end
  end

  output
end
massage(data) click to toggle source
# File lib/unit_diff.rb, line 267
def massage(data)
  # unescape newlines, strip <> from entire string
  data = data.join
  data = data.gsub(/\\n/, "\n").gsub(/0x[a-f0-9]+/m, '0xXXXXXX') + "\n"
  data += "\n" unless data[-1] == ?\n
  data
end
parse_diff(result) click to toggle source

Parses a single diff recording the header and what was expected, and what was actually obtained.

# File lib/unit_diff.rb, line 121
def parse_diff(result)
  header = []
  expect = []
  butwas = []
  footer = []
  state = :header

  until result.empty? do
    case state
    when :header then
      header << result.shift
      state = :expect if result.first =~ /^<|^Expected/
    when :expect then
      case result.first
      when /^Expected (.*?) to equal (.*?):$/ then
        expect << $1
        butwas << $2
        state = :footer
        result.shift
      when /^Expected (.*?), not (.*)$/m then
        expect << $1
        butwas << $2
        state = :footer
        result.shift
      when /^Expected (.*?)$/ then
        expect << "#{$1}\n"
        result.shift
      when /^to equal / then
        state = :spec_butwas
        bw = result.shift.sub(/^to equal (.*):?$/, '\1')
        butwas << bw
      else
        state = :butwas if result.first.sub!(/ expected( but was|, not)/, '')
        expect << result.shift
      end
    when :butwas then
      butwas = result[0..-1]
      result.clear
    when :spec_butwas then
      if result.first =~ /^\s+\S+ at |^:\s*$/
        state = :footer
      else
        butwas << result.shift
      end
    when :footer then
      butwas.last.sub!(/:$/, '')
      footer = result.map {|l| l.chomp }
      result.clear
    else
      raise "unknown state #{state}"
    end
  end

  return header, expect, nil, footer if butwas.empty?

  expect.last.chomp!
  expect.first.sub!(/^<\"/, '')
  expect.last.sub!(/\">$/, '')

  butwas.last.chomp!
  butwas.last.chop! if butwas.last =~ /\.$/
  butwas.first.sub!( /^<\"/, '')
  butwas.last.sub!(/\">$/, '')

  return header, expect, butwas, footer
end
parse_input(input, output) click to toggle source
# File lib/unit_diff.rb, line 62
def parse_input(input, output)
  current = []
  data = []
  data << current
  print_lines = true

  term = "\nFinished".split(//).map { |c| c[0] }
  term_length = term.size

  old_sync = output.sync
  output.sync = true
  while line = input.gets
    case line
    when /^(Loaded suite|Started|# Running tests:)/ then
      print_lines = true
      output.puts line
      chars = []
      while c = input.getc do
        output.putc c
        chars << c
        tail = chars[-term_length..-1]
        break if chars.size >= term_length and tail == term
      end
      output.puts input.gets # the rest of "Finished in..."
      output.puts
      next
    when /^\s*$/, /^\(?\s*\d+\) (Failure|Error):/, /^\d+\)/ then
      print_lines = false
      current = []
      data << current
    when /^Finished in \d/ then
      print_lines = false
    end
    output.puts line if print_lines
    current << line
  end
  output.sync = old_sync
  data = data.reject { |o| o == ["\n"] or o.empty? }
  footer = data.pop

  data.map do |result|
    break if result.any? { |l| l =~ / expected( but was|, not)/ }

    header = result.find do |l|
      l =~ /^\(?\s*\d+\) (Failure|Error):/
    end

    break unless header

    message_index = result.index(header) + 2

    result[message_index..-1] = result[message_index..-1].join
  end

  return data, footer
end
unit_diff(input=ARGF, output=$stdout) click to toggle source

Scans Test::Unit output input looking for comparison failures and makes them easily readable by passing them through diff.

# File lib/unit_diff.rb, line 192
def unit_diff(input=ARGF, output=$stdout)
  $b = false unless defined? $b
  $c = false unless defined? $c
  $k = false unless defined? $k
  $u = true  unless defined? $u
  $p = false unless defined? $p

  data, footer = self.parse_input(input, output)

  output = []

  # Output
  data.each do |result|
    if result.first =~ /Error/ then
      output.push result.join('')
      next
    end

    prefix, expect, butwas, result_footer = parse_diff(result)

    output.push prefix.compact.map {|line| line.strip}.join("\n")

    if butwas then
      output.push self.diff(expect, butwas)

      output.push result_footer
      output.push ''
    else
      output.push expect.join('')
    end
  end

  if footer then
    footer.shift if footer.first.strip.empty?
    output.push footer.compact.map {|line| line.strip}.join("\n")
  end

  return output.flatten.join("\n")
end