# rcov Copyright (c) 2004-2006 Mauricio Fernandez # See LEGAL and LICENSE for additional licensing information. require 'pathname' require 'rcov/xx' # extend XX module XX module XMLish include Markup def xmlish_ *a, &b xx_which(XMLish){ xx_with_doc_in_effect(*a, &b)} end end end module Rcov # Try to fix bugs in the REXML shipped with Ruby 1.8.6 # They affect Mac OSX 10.5.1 users and motivates endless bug reports. begin require 'rexml/formatters/transitive' require 'rexml/formatter/pretty' rescue LoadError end if (RUBY_VERSION == "1.8.6" || RUBY_VERSION == "1.8.7") && defined? REXML::Formatters::Transitive class REXML::Document remove_method :write rescue nil def write( output=$stdout, indent=-1, trans=false, ie_hack=false ) if xml_decl.encoding != "UTF-8" && !output.kind_of?(Output) output = Output.new( output, xml_decl.encoding ) end formatter = if indent > -1 #if trans REXML::Formatters::Transitive.new( indent ) #else # REXML::Formatters::Pretty.new( indent, ie_hack ) #end else REXML::Formatters::Default.new( ie_hack ) end formatter.write( self, output ) end end class REXML::Formatters::Transitive remove_method :write_element rescue nil def write_element( node, output ) output << "<#{node.expanded_name}" node.attributes.each_attribute do |attr| output << " " attr.write( output ) end unless node.attributes.empty? if node.children.empty? output << "/>" else output << ">" # If compact and all children are text, and if the formatted output # is less than the specified width, then try to print everything on # one line skip = false @level += @indentation node.children.each { |child| write( child, output ) } @level -= @indentation output << "" end output << "\n" output << ' '*@level end end end class Formatter # :nodoc: require 'pathname' ignore_files = [ /\A#{Regexp.escape(Pathname.new(Config::CONFIG["libdir"]).cleanpath.to_s)}/, /\btc_[^.]*.rb/, /_test\.rb\z/, /\btest\//, /\bvendor\//, /\A#{Regexp.escape(__FILE__)}\z/] DEFAULT_OPTS = {:ignore => ignore_files, :sort => :name, :sort_reverse => false, :output_threshold => 101, :dont_ignore => [], :callsite_analyzer => nil, :comments_run_by_default => false} def initialize(opts = {}) options = DEFAULT_OPTS.clone.update(opts) @files = {} @ignore_files = options[:ignore] @dont_ignore_files = options[:dont_ignore] @sort_criterium = case options[:sort] when :loc : lambda{|fname, finfo| finfo.num_code_lines} when :coverage : lambda{|fname, finfo| finfo.code_coverage} else lambda{|fname, finfo| fname} end @sort_reverse = options[:sort_reverse] @output_threshold = options[:output_threshold] @callsite_analyzer = options[:callsite_analyzer] @comments_run_by_default = options[:comments_run_by_default] @callsite_index = nil @mangle_filename = Hash.new{|h,base| h[base] = Pathname.new(base).cleanpath.to_s.gsub(%r{^\w:[/\\]}, "").gsub(/\./, "_").gsub(/[\\\/]/, "-") + ".html" } end def add_file(filename, lines, coverage, counts) old_filename = filename filename = normalize_filename(filename) SCRIPT_LINES__[filename] = SCRIPT_LINES__[old_filename] if @ignore_files.any?{|x| x === filename} && !@dont_ignore_files.any?{|x| x === filename} return nil end if @files[filename] @files[filename].merge(lines, coverage, counts) else @files[filename] = FileStatistics.new(filename, lines, counts, @comments_run_by_default) end end def normalize_filename(filename) File.expand_path(filename).gsub(/^#{Regexp.escape(Dir.getwd)}\//, '') end def mangle_filename(base) @mangle_filename[base] end def each_file_pair_sorted(&b) return sorted_file_pairs unless block_given? sorted_file_pairs.each(&b) end def sorted_file_pairs pairs = @files.sort_by do |fname, finfo| @sort_criterium.call(fname, finfo) end.select{|_, finfo| 100 * finfo.code_coverage < @output_threshold} @sort_reverse ? pairs.reverse : pairs end def total_coverage lines = 0 total = 0.0 @files.each do |k,f| total += f.num_lines * f.total_coverage lines += f.num_lines end return 0 if lines == 0 total / lines end def code_coverage lines = 0 total = 0.0 @files.each do |k,f| total += f.num_code_lines * f.code_coverage lines += f.num_code_lines end return 0 if lines == 0 total / lines end def num_code_lines lines = 0 @files.each{|k, f| lines += f.num_code_lines } lines end def num_lines lines = 0 @files.each{|k, f| lines += f.num_lines } lines end private def cross_references_for(filename, lineno) return nil unless @callsite_analyzer @callsite_index ||= build_callsite_index @callsite_index[normalize_filename(filename)][lineno] end def reverse_cross_references_for(filename, lineno) return nil unless @callsite_analyzer @callsite_reverse_index ||= build_reverse_callsite_index @callsite_reverse_index[normalize_filename(filename)][lineno] end def build_callsite_index index = Hash.new{|h,k| h[k] = {}} @callsite_analyzer.analyzed_classes.each do |classname| @callsite_analyzer.analyzed_methods(classname).each do |methname| defsite = @callsite_analyzer.defsite(classname, methname) index[normalize_filename(defsite.file)][defsite.line] = @callsite_analyzer.callsites(classname, methname) end end index end def build_reverse_callsite_index index = Hash.new{|h,k| h[k] = {}} @callsite_analyzer.analyzed_classes.each do |classname| @callsite_analyzer.analyzed_methods(classname).each do |methname| callsites = @callsite_analyzer.callsites(classname, methname) defsite = @callsite_analyzer.defsite(classname, methname) callsites.each_pair do |callsite, count| next unless callsite.file fname = normalize_filename(callsite.file) (index[fname][callsite.line] ||= []) << [classname, methname, defsite, count] end end end index end class XRefHelper < Struct.new(:file, :line, :klass, :mid, :count) # :nodoc: end def _get_defsites(ref_blocks, filename, lineno, linetext, label, &format_call_ref) if @do_cross_references and (rev_xref = reverse_cross_references_for(filename, lineno)) refs = rev_xref.map do |classname, methodname, defsite, count| XRefHelper.new(defsite.file, defsite.line, classname, methodname, count) end.sort_by{|r| r.count}.reverse ref_blocks << [refs, label, format_call_ref] end end def _get_callsites(ref_blocks, filename, lineno, linetext, label, &format_called_ref) if @do_callsites and (refs = cross_references_for(filename, lineno)) refs = refs.sort_by{|k,count| count}.map do |ref, count| XRefHelper.new(ref.file, ref.line, ref.calling_class, ref.calling_method, count) end.reverse ref_blocks << [refs, label, format_called_ref] end end end class TextSummary < Formatter # :nodoc: def execute puts summary end def summary "%.1f%% %d file(s) %d Lines %d LOC" % [code_coverage * 100, @files.size, num_lines, num_code_lines] end end class TextReport < TextSummary # :nodoc: def execute print_lines print_header print_lines each_file_pair_sorted do |fname, finfo| name = fname.size < 52 ? fname : "..." + fname[-48..-1] print_info(name, finfo.num_lines, finfo.num_code_lines, finfo.code_coverage) end print_lines print_info("Total", num_lines, num_code_lines, code_coverage) print_lines puts summary end def print_info(name, lines, loc, coverage) puts "|%-51s | %5d | %5d | %5.1f%% |" % [name, lines, loc, 100 * coverage] end def print_lines puts "+----------------------------------------------------+-------+-------+--------+" end def print_header puts "| File | Lines | LOC | COV |" end end class FullTextReport < Formatter # :nodoc: DEFAULT_OPTS = {:textmode => :coverage} def initialize(opts = {}) options = DEFAULT_OPTS.clone.update(opts) @textmode = options[:textmode] @color = options[:color] super(options) end def execute each_file_pair_sorted do |filename, fileinfo| puts "=" * 80 puts filename puts "=" * 80 lines = SCRIPT_LINES__[filename] unless lines # try to get the source code from the global code coverage # analyzer re = /#{Regexp.escape(filename)}\z/ if $rcov_code_coverage_analyzer and (data = $rcov_code_coverage_analyzer.data_matching(re)) lines = data[0] end end (lines || []).each_with_index do |line, i| case @textmode when :counts puts "%-70s| %6d" % [line.chomp[0,70], fileinfo.counts[i]] when :gcc puts "%s:%d:%s" % [filename, i+1, line.chomp] unless fileinfo.coverage[i] when :coverage if @color prefix = fileinfo.coverage[i] ? "\e[32;40m" : "\e[31;40m" puts "#{prefix}%s\e[37;40m" % line.chomp else prefix = fileinfo.coverage[i] ? " " : "!! " puts "#{prefix}#{line}" end end end end end end class TextCoverageDiff < Formatter # :nodoc: FORMAT_VERSION = [0, 1, 0] DEFAULT_OPTS = {:textmode => :coverage_diff, :coverage_diff_mode => :record, :coverage_diff_file => "coverage.info", :diff_cmd => "diff", :comments_run_by_default => true} def SERIALIZER # mfp> this was going to be YAML but I caught it failing at basic # round-tripping, turning "\n" into "" and corrupting the data, so # it must be Marshal for now Marshal end def initialize(opts = {}) options = DEFAULT_OPTS.clone.update(opts) @textmode = options[:textmode] @color = options[:color] @mode = options[:coverage_diff_mode] @state_file = options[:coverage_diff_file] @diff_cmd = options[:diff_cmd] @gcc_output = options[:gcc_output] super(options) end def execute case @mode when :record record_state when :compare compare_state else raise "Unknown TextCoverageDiff mode: #{mode.inspect}." end end def record_state state = {} each_file_pair_sorted do |filename, fileinfo| state[filename] = {:lines => SCRIPT_LINES__[filename], :coverage => fileinfo.coverage.to_a, :counts => fileinfo.counts} end File.open(@state_file, "w") do |f| self.SERIALIZER.dump([FORMAT_VERSION, state], f) end rescue $stderr.puts <<-EOF Couldn't save coverage data to #{@state_file}. EOF end # ' require 'tempfile' def compare_state return unless verify_diff_available begin format, prev_state = File.open(@state_file){|f| self.SERIALIZER.load(f) } rescue $stderr.puts <<-EOF Couldn't load coverage data from #{@state_file}. EOF return # ' end if !(Array === format) or FORMAT_VERSION[0] != format[0] || FORMAT_VERSION[1] < format[1] $stderr.puts <<-EOF Couldn't load coverage data from #{@state_file}. The file is saved in the format #{format.inspect[0..20]}. This rcov executable understands #{FORMAT_VERSION.inspect}. EOF return # ' end each_file_pair_sorted do |filename, fileinfo| old_data = Tempfile.new("#{mangle_filename(filename)}-old") new_data = Tempfile.new("#{mangle_filename(filename)}-new") if prev_state.has_key? filename old_code, old_cov = prev_state[filename].values_at(:lines, :coverage) old_code.each_with_index do |line, i| prefix = old_cov[i] ? " " : "!! " old_data.write "#{prefix}#{line}" end else old_data.write "" end old_data.close SCRIPT_LINES__[filename].each_with_index do |line, i| prefix = fileinfo.coverage[i] ? " " : "!! " new_data.write "#{prefix}#{line}" end new_data.close diff = `#{@diff_cmd} -u "#{old_data.path}" "#{new_data.path}"` new_uncovered_hunks = process_unified_diff(filename, diff) old_data.close! new_data.close! display_hunks(filename, new_uncovered_hunks) end end def display_hunks(filename, hunks) return if hunks.empty? puts puts "=" * 80 puts <