require 'tempfile' require 'rbconfig' ## # 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 class UnitDiff WINDOZE = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ DIFF = if WINDOZE 'diff.exe' else if system("gdiff", __FILE__, __FILE__) 'gdiff' # solaris and kin suck else 'diff' end end unless defined? DIFF ## # Handy wrapper for UnitDiff#unit_diff. def self.unit_diff trap 'INT' do exit 1 end puts UnitDiff.new.unit_diff end 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 # Parses a single diff recording the header and what # was expected, and what was actually obtained. 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 ## # Scans Test::Unit output +input+ looking for comparison failures and makes # them easily readable by passing them through diff. 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 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 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 end