[Rubygems-developers] Code coverage of unit tests

Mauricio Fernández batsman.geo at yahoo.com
Sun Mar 21 15:14:00 EST 2004


Hi,

I've been using Simon Strandgaard's code to generate code coverage info
corresponding to the available unit tests. I hope rubygems can benefit
from this.

The generated information looks like
  http://thekode.net/misc/rubygems-coverage/

With the following patch, the information will be generated by doing
  cd test        (within the rubygems source dir)
  ruby -rcoverage test_all.rb

The code is quite messy atm., I'm planning to rewrite it later.

diff -Nr -u rubygems/test/coverage.rb rubygems.new/test/coverage.rb
--- rubygems/test/coverage.rb   1970-01-01 01:00:00.000000000 +0100
+++ rubygems.new/test/coverage.rb       2004-03-21 14:30:29.000000000 +0100
@@ -0,0 +1,236 @@
+
+# module COVERAGE__ originally (c) NAKAMURA Hiroshi, under Ruby's license
+# module PrettyCoverage originally (c) Simon Strandgaard, believed to be under
+#                                      Ruby's license
+# minor modifications by Mauricio Julio Fernández Pradier
+
+require 'rbconfig'
+include Config
+
+module PrettyCoverage
+
+class HTML
+    def initialize
+        @files = {}
+        @files_codeonly = {}
+    end
+
+    def execute
+        puts "execute"
+        create_file_index
+        @files.each do |file, line_marked|
+            create_file(file, line_marked, @files_codeonly[file])
+        end
+    end
+
+    def mk_filename(name)
+        total = File.expand_path(name)
+        return nil if total =~ /\A#{Regexp.escape(CONFIG["libdir"])}/
+        return nil if name =~ /test_/
+        name = name.gsub(/\./, "_").gsub(/\//, "-")
+        name +".coverage.html"
+    end
+
+    def create_file_index
+        output_filename = "index.html"
+        rows = []
+        filestats = {}
+        filestats_code = {}
+        @files.sort_by{|k,v| k}.each do|file, line_marked|
+            filename = mk_filename(file)
+            next unless filename
+            next if file =~ /test_/
+            percent = "%02.1f" % calc_coverage(line_marked)
+            percent2 = "%02.1f" % calc_coverage(@files_codeonly[file])
+            numlines = line_marked.transpose[1].size
+            filestats[file] = [calc_coverage(line_marked), numlines]
+            filestats_code[file] = [calc_coverage(@files_codeonly[file]),
+                                    numlines]
+            rows << "<tr>"
+            rows << "<td><a href=\"#{filename}\">#{file}</a></td>"
+            rows << "<td><tt>#{numlines}</tt></td>"
+            rows << "<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>"
+            rows << "<td><tt>#{percent}%</tt></td>"
+            rows << "<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>"
+            rows << "<td><tt>#{percent2}%</tt></td>"
+            rows << "</tr>"
+        end
+        result = rows.join("\n")
+        total_cov = 1.0 *
+            filestats.inject(0){|a,(k,v)| a + v[0] * v[1]} /
+            filestats.inject(0){|a,(k,v)| a + v[1]}
+        total_code_cov = 1.0 *
+            filestats_code.inject(0) {|a,(k,v)| a + v[0] * v[1]} /
+            filestats_code.inject(0){|a,(k,v)| a + v[1]}
+        body = "<h1>Average (with comments): %02.1f%%</h1>" % total_cov
+        body << "<h1>Average (code only): %02.1f%%</h1>" % total_code_cov
+        body << "<table>#{result}</table>"
+        title = "coverage"
+        css = <<EOCSS
+body {
+  background-color: rgb(180, 180, 180);
+}
+div.marked {
+  background-color: rgb(185, 200, 200);
+}
+div.overview {
+  border-bottom: 8px solid black;
+}
+EOCSS
+            html = <<EOHTML
+<html><head><title>#{title}</title>
+<style type="text/css">#{css}</style></head>
+<body>#{body}</body></html>
+EOHTML
+            File.open(output_filename, "w+") do |f|
+                f.puts html
+            end
+    end
+
+    def add_file(file, line_marked)
+        percent = calc_coverage(line_marked)
+        path = File.expand_path(file)
+        return nil if path =~ /\A#{Regexp.escape(CONFIG["rubylibdir"])}/
+        return nil if path =~ /\A#{Regexp.escape(CONFIG["sitelibdir"])}/
+
+      #printf("file #{file} coverage=%02.1f%\n", percent)
+
+      # comments and empty lines.. we must
+      # propagate marked-value backwards
+        line_marked << ["", false]
+        (line_marked.size).downto(1) do |index|
+            line, marked = line_marked[index-1]
+            next_line, next_marked = line_marked[index]
+            if line =~ /^\s*(#|$)/ and marked == false
+                marked = next_marked
+          #line = "hest" + line
+                line_marked[index-1] = [line, marked]
+            end
+        end
+        line_marked.pop
+
+        @files[file] = line_marked
+        @files_codeonly[file] = line_marked.select do |(line, marked)|
+            line !~ /^\s*(#|$)/
+        end
+    end
+
+    def calc_coverage(line_marked)
+        marked = line_marked.transpose[1]
+        n = marked.inject(0) {|r, i| (i) ? (r+1) : r }
+        percent = n.to_f * 100 / marked.size
+    end
+
+    def format_overview(file, line_marked, code_marked)
+        percent = "%02.1f" % calc_coverage(line_marked)
+        percent2 = "%02.1f" % calc_coverage(code_marked)
+        html = <<EOHTML
+<div class="overview">
+<table>
+<tr><td>filename</td><td><tt>#{file}</tt></td></tr>
+<tr><td>total coverage</td><td>#{percent}</td></tr>
+<tr><td>code coverage</td><td>#{percent2}</td></tr>
+</table>
+</div>
+EOHTML
+        html
+    end
+
+    def format_lines(line_marked)
+        result = ""
+        last = nil
+        end_of_div = ""
+        format_line = "%#{line_marked.size.to_s.size}d"
+        line_no = 1
+        line_marked.each do |(line, marked)|
+            if marked != last
+                result += end_of_div
+                case marked
+                when true
+                    result += "<div class=\"marked\">"
+                    end_of_div = "</div>"
+                when false
+                    end_of_div = ""
+                end
+            end
+            result += (format_line % line_no) + " " + line + "\n"
+            last = marked
+            line_no += 1
+        end
+        result += end_of_div
+        "<pre>#{result}</pre>"
+    end
+
+    def create_file(file, line_marked, code_marked)
+        output_filename = mk_filename(file)
+        return unless output_filename
+        puts "outputting #{output_filename.inspect}"
+        body = format_overview(file, line_marked, code_marked) +
+            format_lines(line_marked)
+        title = file + " - coverage"
+        css = <<EOCSS
+body {
+  background-color: rgb(180, 180, 180);
+}
+div.marked {
+  background-color: rgb(185, 200, 200);
+}
+div.overview {
+  border-bottom: 8px solid black;
+}
+EOCSS
+        html = <<EOHTML
+<html><head><title>#{title}</title>
+<style type="text/css">#{css}</style></head>
+<body>#{body}</body></html>
+EOHTML
+        File.open(output_filename, "w+") do |f|
+            f.puts html
+        end
+    end
+end
+end
+
+SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__
+
+module COVERAGE__
+    COVER = {}
+    def self.trace_func(event, file, line, id, binding, klass)
+        case event
+        when 'c-call', 'c-return', 'class'
+            return
+        end
+        COVER[file] ||= []
+        COVER[file][line] ||= 0
+        COVER[file][line] += 1
+    end
+
+    END {
+        set_trace_func(nil)
+        printer = PrettyCoverage::HTML.new
+        COVER.each do |file, lines|
+            next if SCRIPT_LINES__.has_key?(file) == false
+            lines = SCRIPT_LINES__[file]
+            covers = COVER[file]
+            line_status = []
+            0.upto(lines.size - 1) do |c|
+                line = lines[c].chomp
+                marked = false
+                if covers[c + 1]
+                    marked = true
+                elsif /^\s*(?:begin\s*(?:#.*)?|ensure\s*(?:#.*)?|else\s*(?:#.*)?)$/ =~ line and covers[c + 1 + 1]
+                    covers[c + 1] = covers[c + 1 + 1]
+                    marked = true
+                elsif /^\s*(?:end|})\s*$/ =~ line && covers[c + 1 - 1]
+                    covers[c + 1] = covers[c + 1 - 1]
+                    marked = true
+                end
+                line_status << [line, marked]
+            end
+            printer.add_file(file, line_status)
+        end
+        printer.execute
+    }
+
+    set_trace_func(COVERAGE__.method(:trace_func).to_proc)
+end
diff -Nr -u rubygems/test/test_all.rb rubygems.new/test/test_all.rb
--- rubygems/test/test_all.rb   1970-01-01 01:00:00.000000000 +0100
+++ rubygems.new/test/test_all.rb       2004-03-21 14:30:34.000000000 +0100
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+
+$:.unshift "../lib"
+Dir["test_*.rb"].each {|f| require f}
+

-- 
Running Debian GNU/Linux Sid (unstable)
batsman dot geo at yahoo dot com

Vini, vidi, Linux!
	-- Unknown source


More information about the Rubygems-developers mailing list