#!/usr/bin/perl -w
# $Id$
#
# Copyright 2006, Philip Gwyn.
#
# This script will compare a diff against a coverage. The idea is to
# make sure that changes you have made are verified by your unit tests.
#
# This script was the original motivation behind Text::Diff::Parser.
#
# To use it, install ExtUtils::MakeMaker::Coverage. Then add the following
# to your Makefile.PL. It will silently ignore failure, so that you don't
# annoy people who don't have ExtUtils::MM::Coverage installed
# eval q{
# use ExtUtils::MakeMaker::Coverage;
# # I keep many modules outside of /usr/lib/perl5
# my $config = ExtUtils::MakeMaker::Coverage->config;
# $config->ignore( ['site_perl'] );
# };
#
# Then generate a coverage report :
# make testcover
#
# Create a diff of your recent changes :
# cvs diff >changes.diff
# svn diff >changes.diff
#
# Then run this script on those changes :
# cover-diff --cover-db=cover_db --diff=changes.diff --strip=1
#
# The report is in cover_db/diff.html :
# htmlview cover_db/diff.html
#
# Note that perl's definition of an executable line is some times different
# from what you expect. This means that the conditionals if()s will often
# not be marked as run, even if they have been. This is a limit of
# ExtUtils::MM::Coverage.
#
use strict;
use Devel::Cover::DB;
use Text::Diff::Parser;
use POSIX qw(strftime);
use Getopt::Long;
my $DIFF = 'diff';
my $COVER_DB = 'cover_db';
my @PREFIX;
my $SIMPLIFY=0;
my $HELP=0;
my $STRIP=0;
my $ret = GetOptions( 'diff=s' => \$DIFF,
'cover-db=s' => \$COVER_DB,
'prefix=s' => \@PREFIX,
'simplify' => \$SIMPLIFY,
'strip=i' => \$STRIP,
'help' => \$HELP
);
unless( $ret ) {
usage();
exit 3;
}
if( $HELP ) {
usage();
exit 0;
}
my $OUTPUT = "$COVER_DB/diff.html";
#########################################################
my $diff = Text::Diff::Parser->new( File => $DIFF,
Simplify => $SIMPLIFY,
Strip => $STRIP );
my $db = Devel::Cover::DB->new( db=>$COVER_DB );
my $cover = $db->cover;
my $currentfile = '';
my %LAST;
my %WARNED;
my $lastline;
my $href;
my $totals;
my @report;
foreach my $change ( $diff->changes ) {
my( $file, $c );
($file, $c)= ( '', '');
PREFIX:
foreach my $dir ( '', @PREFIX ) {
foreach my $tf ( join( '/', $dir, $change->filename2 ),
join( '/', 'blib', $dir, $change->filename2 ),
join( '/', 'blib', 'lib', $dir, $change->filename2 )
) {
$tf =~ s(//)(/)g;
$c = $cover->file( $tf );
next unless $c;
$file = $tf;
last PREFIX;
}
}
unless( $c ) {
my $file = $change->filename2;
warn "$file not in cover_db\n" if $file =~ /\.p[ml]$/
and not $WARNED{$file}++;
next;
}
my $crit = $c->criterion( 'statement' );
my $last = $LAST{ $file } ||= ( sort { $b<=>$a } $crit->items )[0];
if( $currentfile ne $file ) {
$href = $file;
$href =~ s/\W/-/g;
$href .= ".html";
push @report, ['html_newfile', $file, $href ];
$currentfile = $file;
undef( $lastline );
}
my $line = $change->line2;
my $size = $change->size;
if( $lastline and not ($line <= $lastline+1 and
$lastline <= $line+$size)) {
push @report, ['html_newchunk'];
}
for( my $n =0; $n < $size ; $n++ ) {
push @report, ['html_line', {href=>$href, line=>$line+$n}];
my $text = $change->text( $n );
my $check = $line+$n;
$check = 0 if $line + $n > $last; # past end of coverage -> POD?
$check = 0 unless $text =~ /\S/; # empty line
$check = 0 if $text =~ /^\s*#/; # comments
# we can't have run a line that was removed, so we just make sure
# that the equiv of the first line that currently exists was run.
$check = $change->line2
if $change->type eq 'REMOVE';
# Better : if line2 isn't present, check to see if line2-1 and
# line2+1 are presente. Give c1 or c2 class if not, or if they are
# covered+not covered.
my $class = '';
if( $check ) {
my $l = $crit->location( $check );
if( $l ) {
if( $l->[0]->covered ) {
$class = 'c3';
$totals->{$file}{good}++;
}
else {
$class = 'c0';
$totals->{$file}{bad}++;
}
}
}
$report[-1][1]{class2} = $class;
$class = '' unless $change->type; # null operation
$report[-1][1]{text} = $text;
$report[-1][1]{class} = $class;
$report[-1][1]{type} = $change->type;
}
$lastline = $line+$size;
}
#########################################################
open OUT, ">$OUTPUT" or die "Unable to create $OUTPUT: $!\n";
print OUT html_preamble();
print OUT html_report( $totals );
print OUT qq(
\n);
foreach my $line ( @report ) {
my( $func, @args ) = @$line;
print OUT 'main'->can($func)->( @args );
}
print OUT qq(
\n);
print OUT html_postamble();
close OUT;
patch_css();
print "Report created in $OUTPUT\n";
#########################################################
sub html_preamble
{
my $diff_age = strftime "%Y/%m/%d %H:%M:%S %Z",
localtime((stat $DIFF)[9]);
my $db_age = strftime "%Y/%m/%d %H:%M:%S %Z",
localtime((stat "$COVER_DB/cover.12")[9]);
return <
Change Coverage: $DIFF
Change Coverage
Database:
$COVER_DB
Generated $db_age
DIFF:
$DIFF
Generated $diff_age
HTML
}
#########################################################
sub html_report
{
my( $totals ) = @_;
my @ret;
my $total = 0;
my $covered = 0;
push @ret, qq(