lib/rcov/formatters/html_coverage.rb



module Rcov
  class HTMLCoverage < BaseFormatter # :nodoc:
    require 'fileutils'

    DEFAULT_OPTS = {:color => false, :fsr => 30, :destdir => "coverage",
                    :callsites => false, :cross_references => false,
                    :charset => nil }

    def initialize(opts = {})
      options = DEFAULT_OPTS.clone.update(opts)
      super(options)
      @dest = options[:destdir]
      @css = options[:css]
      @color = options[:color]
      @fsr = options[:fsr]
      @do_callsites = options[:callsites]
      @do_cross_references = options[:cross_references]
      @span_class_index = 0
      @charset = options[:charset]
    end

    def execute
      return if @files.empty?
      FileUtils.mkdir_p @dest
      
      # Copy collaterals
      ['screen.css','print.css','rcov.js','jquery-1.3.2.min.js','jquery.tablesorter.min.js'].each do |_file|
        _src = File.expand_path("#{File.dirname(__FILE__)}/../templates/#{_file}")
        FileUtils.cp(_src, File.join(@dest, "#{_file}"))
      end

      # Copy custom CSS, if any
      if @css
        begin
          _src = File.expand_path("#{@dest}/../#{@css}")
          FileUtils.cp(_src, File.join(@dest, "custom.css"))
        rescue
          @css = nil
        end
      end
      
      create_index(File.join(@dest, "index.html"))

      each_file_pair_sorted do |filename, fileinfo|
        create_file(File.join(@dest, mangle_filename(filename)), fileinfo)
      end
    end

    private

    class SummaryFileInfo  # :nodoc:
      def initialize(obj)
        @o = obj 
      end

      def num_lines
        @o.num_lines
      end

      def num_code_lines
        @o.num_code_lines
      end

      def code_coverage
        @o.code_coverage
      end

      def code_coverage_for_report
        code_coverage * 100
      end

      def total_coverage
        @o.total_coverage
      end

      def total_coverage_for_report
        total_coverage * 100
      end

      def name
        "TOTAL" 
      end
    end

    def create_index(destname)

      doc = Rcov::Formatters::HtmlErbTemplate.new('index.html.erb',
        :project_name => project_name,
        :generated_on => Time.now,
        :css => @css,
        :rcov => Rcov,
        :formatter => self,
        :output_threshold => @output_threshold,
        :total => SummaryFileInfo.new(self),
        :files => each_file_pair_sorted.map{|k,v| v}
      )
      File.open(destname, "w") { |f| f.puts doc.render }
    end

    def create_file(destfile, fileinfo)
      doc = Rcov::Formatters::HtmlErbTemplate.new('detail.html.erb',
        :project_name => project_name,
        :rcov_page_title => fileinfo.name, 
        :css => @css,
        :generated_on => Time.now,
        :rcov => Rcov,
        :formatter => self,
        :output_threshold => @output_threshold,
        :fileinfo => fileinfo
      )
      File.open(destfile, "w")  { |f| f.puts doc.render }
    end
    
    private
    
    def project_name
      Dir.pwd.split('/')[-1].split(/[^a-zA-Z0-9]/).map{|i| i.gsub(/[^a-zA-Z0-9]/,'').capitalize} * " " || ""
    end
    
  end

  class HTMLProfiling < HTMLCoverage # :nodoc:
    DEFAULT_OPTS = {:destdir => "profiling"}
    def initialize(opts = {})
      options = DEFAULT_OPTS.clone.update(opts)
      super(options)
      @max_cache = {}
      @median_cache = {}
    end

    def default_title
      "Bogo-profile information"
    end

    def default_color
      if @color
        "rgb(179,205,255)"
      else
        "rgb(255, 255, 255)"
      end
    end

    def output_color_table?
      false
    end

    def span_class(sourceinfo, marked, count)
      full_scale_range = @fsr # dB
      nz_count = sourceinfo.counts.select{|x| x && x != 0}
      nz_count << 1 # avoid div by 0
      max = @max_cache[sourceinfo] ||= nz_count.max
      #avg = @median_cache[sourceinfo] ||= 1.0 *
      #    nz_count.inject{|a,b| a+b} / nz_count.size
      median = @median_cache[sourceinfo] ||= 1.0 * nz_count.sort[nz_count.size/2]
      max ||= 2
      max = 2 if max == 1
      if marked == true
        count = 1 if !count || count == 0
        idx = 50 + 1.0 * (500/full_scale_range) * Math.log(count/median) / Math.log(10)
        idx = idx.to_i
        idx = 0 if idx < 0
        idx = 100 if idx > 100
        "run#{idx}"
      else
        nil
      end
    end
  end

  class RubyAnnotation < BaseFormatter # :nodoc:
    DEFAULT_OPTS = { :destdir => "coverage" }
    def initialize(opts = {})
      options = DEFAULT_OPTS.clone.update(opts)
      super(options)
      @dest = options[:destdir]
      @do_callsites = true
      @do_cross_references = true

      @mangle_filename = Hash.new{ |h,base|
        h[base] = Pathname.new(base).cleanpath.to_s.gsub(%r{^\w:[/\\]}, "").gsub(/\./, "_").gsub(/[\\\/]/, "-") + ".rb"
      }
    end

    def execute
      return if @files.empty?
      FileUtils.mkdir_p @dest
      each_file_pair_sorted do |filename, fileinfo|
        create_file(File.join(@dest, mangle_filename(filename)), fileinfo)
      end
    end

    private

    def format_lines(file)
      result = ""
      format_line = "%#{file.num_lines.to_s.size}d"
      file.num_lines.times do |i|
        line = file.lines[i].chomp
        marked = file.coverage[i]
        count = file.counts[i]
        result << create_cross_refs(file.name, i+1, line, marked) + "\n"
      end
      result
    end

    def create_cross_refs(filename, lineno, linetext, marked)
      return linetext unless @callsite_analyzer && @do_callsites
      ref_blocks = []
      _get_defsites(ref_blocks, filename, lineno, linetext, ">>") do |ref|
        if ref.file
          ref.file.sub!(%r!^./!, '')
          where = "at #{mangle_filename(ref.file)}:#{ref.line}"
        else
          where = "(C extension/core)"
        end
        "#{ref.klass}##{ref.mid} " + where + ""
      end
      _get_callsites(ref_blocks, filename, lineno, linetext, "<<") do |ref| # "
        ref.file.sub!(%r!^./!, '')
        "#{mangle_filename(ref.file||'C code')}:#{ref.line} " + "in #{ref.klass}##{ref.mid}"
      end

      create_cross_reference_block(linetext, ref_blocks, marked)
    end

    def create_cross_reference_block(linetext, ref_blocks, marked)
      codelen = 75
      if ref_blocks.empty?
        if marked
          return "%-#{codelen}s #o" % linetext
        else
          return linetext
        end
      end
      ret = ""
      @cross_ref_idx ||= 0
      @known_files ||= sorted_file_pairs.map{|fname, finfo| normalize_filename(fname)}
      ret << "%-#{codelen}s # " % linetext
      ref_blocks.each do |refs, toplabel, label_proc|
        unless !toplabel || toplabel.empty?
          ret << toplabel << " "
        end
        refs.each do |dst|
          dstfile = normalize_filename(dst.file) if dst.file
          dstline = dst.line
          label = label_proc.call(dst)
          if dst.file && @known_files.include?(dstfile)
            ret << "[[" << label << "]], "
          else
            ret << label << ", "
          end
        end
      end
      ret
    end

    def create_file(destfile, fileinfo)
      #body = format_lines(fileinfo)
      #File.open(destfile, "w") do |f|
        #f.puts body
        #f.puts footer(fileinfo)
      #end
    end

    def footer(fileinfo)
      s  = "# Total lines    : %d\n" % fileinfo.num_lines
      s << "# Lines of code  : %d\n" % fileinfo.num_code_lines
      s << "# Total coverage : %3.1f%%\n" % [ fileinfo.total_coverage*100 ]
      s << "# Code coverage  : %3.1f%%\n\n" % [ fileinfo.code_coverage*100 ]
      # prevents false positives on Emacs
      s << "# Local " "Variables:\n" "# mode: " "rcov-xref\n" "# End:\n"
    end
  end
end