# Copyright 2001-2007, Paul Johnson (pjcj@cpan.org) # This software is free. It is licensed under the same terms as Perl itself. # The latest version of this software should be available from my homepage: # http://www.pjcj.net package Devel::Cover::Report::Html_basic; use strict; use warnings; our $VERSION = "0.61"; use Devel::Cover::DB 0.61; use Getopt::Long; use Template 2.00; my ($HAVE_HIGHLIGHTER,$HAVE_PPI,$HAVE_PERLTIDY); BEGIN { eval "use PPI; use PPI::HTML;"; $HAVE_PPI = !$@; eval "use Perl::Tidy"; $HAVE_PERLTIDY = !$@; $HAVE_HIGHLIGHTER = $HAVE_PPI || $HAVE_PERLTIDY; } my $Template; my %R; sub print_stylesheet { my $file = "$R{options}{outputdir}/cover.css"; open CSS, '>', $file or return; my $p = tell DATA; print CSS ; seek DATA, $p, 0; close CSS; } sub oclass { my ($o, $criterion) = @_; $o ? class($o->percentage, $o->error, $criterion) : "" } sub class { my ($pc, $err, $criterion) = @_; return "" if $criterion eq "time"; !$err ? "c3" : $pc < 75 ? "c0" : $pc < 90 ? "c1" : $pc < 100 ? "c2" : "c3" } sub get_summary { my ($file, $criterion) = @_; my %vals; @vals{"pc", "class"} = ("n/a", ""); my $part = $R{db}->summary($file); return \%vals unless exists $part->{$criterion}; my $c = $part->{$criterion}; $vals{class} = class($c->{percentage}, $c->{error}, $criterion); return \%vals unless defined $c->{percentage}; $vals{pc} = sprintf "%4.1f", $c->{percentage}; $vals{covered} = $c->{covered} || 0; $vals{total} = $c->{total}; $vals{details} = "$vals{covered} / $vals{total}"; my $cr = $criterion eq "pod" ? "subroutine" : $criterion; return \%vals if $cr !~ /^branch|condition|subroutine$/ || !exists $R{filenames}{$file}; $vals{link} = "$R{filenames}{$file}--$cr.html"; \%vals }; sub print_summary { my $vars = { R => \%R, files => [ grep($R{db}->summary($_), @{$R{options}{file}}), "Total" ], }; my $html = "$R{options}{outputdir}/$R{options}{option}{outputfile}"; $Template->process("summary", $vars, $html) or die $Template->error(); $html } sub _highlight_ppi { my @all_lines = @_; my $code = join "", @all_lines; my $document = PPI::Document->new(\$code); my $highlight = PPI::HTML->new(line_numbers => 1); my $pretty = $highlight->html($document); my $split = ''; # turn significant whitespace into   @all_lines = map { $_ =~ s{( +)}{"" . (" " x length($1))}e; "$split$_"; } split /$split/, $pretty; # remove the line number @all_lines = map { s{.*?}{}; $_; } @all_lines; @all_lines = map { s{}{}; $_; } @all_lines; # remove the BR @all_lines = map { s{
$}{}; $_; } @all_lines; @all_lines = map { s{
\n
}{}; $_; } @all_lines; shift @all_lines if $all_lines[0] eq ""; return @all_lines; } sub _highlight_perltidy { my @all_lines = @_; my @coloured = (); Perl::Tidy::perltidy( source => \@all_lines, destination => \@coloured, argv => '-html -pre -nopod2html', stderr => '-', errorfile => '-', ); # remove the PRE shift @coloured; pop @coloured; @coloured = grep { !/cover->file($R{file}); open F, $R{file} or warn("Unable to open $R{file}: $!\n"), return; my @all_lines = ; @all_lines = _highlight(@all_lines) if $HAVE_HIGHLIGHTER; my $linen = 1; LINE: while (defined(my $l = shift @all_lines)) { my $n = $linen++; chomp $l; my %criteria; for my $c (@{$R{showing}}) { my $criterion = $f->$c(); if ($criterion) { my $l = $criterion->location($n); $criteria{$c} = $l ? [@$l] : undef; } } my $count = 0; my $more = 1; while ($more) { my %line; $count++; $line{number} = length $n ? $n : " "; $line{text} = length $l ? $l : " "; my $error = 0; $more = 0; for my $ann (@{$R{options}{annotations}}) { for my $a (0 .. $ann->count - 1) { my $text = $ann->text ($R{file}, $n, $a); $text = " " unless $text && length $text; push @{$line{criteria}}, { text => $text, class => $ann->class($R{file}, $n, $a), }; $error ||= $ann->error($R{file}, $n, $a); } } for my $c (@{$R{showing}}) { my $o = shift @{$criteria{$c}}; $more ||= @{$criteria{$c}}; my $link = $c !~ /statement|time/; my $pc = $link && $c !~ /subroutine|pod/; my $text = $o ? $pc ? $o->percentage : $o->covered : " "; my %criterion = ( text => $text, class => oclass($o, $c) ); my $cr = $c eq "pod" ? "subroutine" : $c; $criterion{link} = "$R{filenames}{$R{file}}--$cr.html#$n-$count" if $o && $link; push @{$line{criteria}}, \%criterion; $error ||= $o->error if $o; } push @lines, \%line; last LINE if $l =~ /^__(END|DATA)__/; $n = $l = ""; } } close F or die "Unable to close $R{file}: $!"; my $vars = { R => \%R, lines => \@lines, }; my $html = "$R{options}{outputdir}/$R{filenames}{$R{file}}.html"; $Template->process("file", $vars, $html) or die $Template->error(); } sub print_branches { my $branches = $R{db}->cover->file($R{file})->branch; return unless $branches; my @branches; for my $location (sort { $a <=> $b } $branches->items) { my $count = 0; for my $b (@{$branches->location($location)}) { $count++; my $text = $b->text; ($text) = _highlight($text) if $HAVE_HIGHLIGHTER; push @branches, { number => $count == 1 ? $location : "", parts => [ map { text => $b->value($_), class => class($b->value($_), $b->error($_), "branch") }, 0 .. $b->total - 1 ], text => $text, }; } } my $vars = { R => \%R, branches => \@branches, }; my $html = "$R{options}{outputdir}/$R{filenames}{$R{file}}--branch.html"; $Template->process("branches", $vars, $html) or die $Template->error(); } sub print_conditions { my $conditions = $R{db}->cover->file($R{file})->condition; return unless $conditions; my %r; for my $location (sort { $a <=> $b } $conditions->items) { my %count; for my $c (@{$conditions->location($location)}) { $count{$c->type}++; # print "-- [$count{$c->type}][@{[$c->text]}]}]\n"; my $text = $c->text; ($text) = _highlight($text) if $HAVE_HIGHLIGHTER; push @{$r{$c->type}}, { number => $count{$c->type} == 1 ? $location : "", condition => $c, parts => [ map { text => $c->value($_), class => class($c->value($_), $c->error($_), "condition") }, 0 .. $c->total - 1 ], text => $text, }; } } my @types = map { name => do { my $n = $_; $n =~ s/_/ /g; $n }, headers => $r{$_}[0]{condition}->headers, conditions => $r{$_}, }, sort keys %r; my $vars = { R => \%R, types => \@types, }; # use Data::Dumper; print Dumper \@types; my $html = "$R{options}{outputdir}/$R{filenames}{$R{file}}--condition.html"; $Template->process("conditions", $vars, $html) or die $Template->error(); } sub print_subroutines { my $subroutines = $R{db}->cover->file($R{file})->subroutine; return unless $subroutines; my $s = $R{options}{show}{subroutine}; my $pods; $pods = $R{db}->cover->file($R{file})->pod if $R{options}{show}{pod}; my $subs; for my $line (sort { $a <=> $b } $subroutines->items) { my @p; if ($pods) { my $l = $pods->location($line); @p = @$l if $l; } for my $o (@{$subroutines->location($line)}) { my $p = shift @p; push @$subs, { line => $line, name => $o->name, count => $s ? $o->covered : "", class => $s ? oclass($o, "subroutine") : "", pod => $p ? $p->covered ? "Yes" : "No" : "n/a", pclass => $p ? oclass($p, "pod") : "", }; } } my $vars = { R => \%R, subs => $subs, }; my $html = "$R{options}{outputdir}/$R{filenames}{$R{file}}--subroutine.html"; $Template->process("subroutines", $vars, $html) or die $Template->error(); } sub get_options { my ($self, $opt) = @_; $opt->{option}{outputfile} = "coverage.html"; die "Invalid command line options" unless GetOptions($opt->{option}, qw( outputfile=s )); } sub report { my ($pkg, $db, $options) = @_; $Template = Template->new ({ LOAD_TEMPLATES => [ Devel::Cover::Report::Html_basic::Template::Provider->new({}), ], }); %R = ( db => $db, options => $options, showing => [ grep $options->{show}{$_}, $db->criteria ], headers => [ map { ($db->criteria_short)[$_] } grep { $options->{show}{($db->criteria)[$_]} } (0 .. $db->criteria - 1) ], annotations => [ map { my $a = $_; map $a->header($_), 0 .. $a->count - 1 } @{$options->{annotations}} ], filenames => { map { $_ => do { (my $f = $_) =~ s/\W/-/g; $f } } @{$options->{file}} }, exists => { map { $_ => -e } @{$options->{file}} }, get_summary => \&get_summary, ); print_stylesheet; my $html = print_summary; for (@{$options->{file}}) { $R{file} = $_; my $show = $options->{show}; print_file; print_branches if $show->{branch}; print_conditions if $show->{condition}; print_subroutines if $show->{subroutine} || $show->{pod}; } print "HTML output sent to $html\n"; } 1; package Devel::Cover::Report::Html_basic::Template::Provider; use strict; use warnings; our $VERSION = "0.61"; use base "Template::Provider"; my %Templates; sub fetch { my $self = shift; my ($name) = @_; # print "Looking for <$name>\n"; $self->SUPER::fetch(exists $Templates{$name} ? \$Templates{$name} : $name) } $Templates{html} = <<'EOT'; [% title %] [% content %] EOT $Templates{header} = <<'EOT'; [% FOREACH criterion = criteria %] [% vals = R.get_summary(R.file, criterion) %] [% END %]
[% R.file %]
Criterion Covered Total %
[% criterion %] [% vals.covered %] [% vals.total %] [% IF vals.link.defined %] [% vals.pc %] [% ELSE %] [% vals.pc %] [% END %]


EOT $Templates{summary} = <<'EOT'; [% WRAPPER html %]

Coverage Summary

Database: [% R.db.db %]


[% FOREACH header = R.headers %] [% END %] [% FOREACH file = files %] [% FOREACH criterion = R.showing %] [% vals = R.get_summary(file, criterion) %] [% IF vals.class %] [% END %] [% vals = R.get_summary(file, "total") %] [% END %]
file [% header %] total
[% IF R.exists.$file %] [% file %] [% ELSE %] [% file %] [% END %] [% ELSE %] [% END %] [% IF vals.link.defined %] [% vals.pc %] [% ELSE %] [% vals.pc %] [% END %] [% vals.pc %]
[% END %] EOT $Templates{file} = <<'EOT'; [% WRAPPER html %]

File Coverage

[% crit = []; FOREACH criterion = R.showing; crit.push(criterion) UNLESS criterion == "time"; END; crit.push("total"); PROCESS header criteria = crit; %] [% FOREACH header = R.annotations.merge(R.headers) %] [% END %] [% FOREACH line = lines %] [% FOREACH cr = line.criteria %] [% END %] [% END %]
line [% header %] code
[% line.number %] [% IF cr.link.defined %] [% END %] [% cr.text %] [% IF cr.link.defined %] [% END %] [% line.text %]
[% END %] EOT $Templates{branches} = <<'EOT'; [% WRAPPER html %]

Branch Coverage

[% PROCESS header criteria = [ "branch" ] %] [% FOREACH branch = branches %] [% FOREACH part = branch.parts %] [% END %] [% END %]
line true false branch
[% branch.number %] [% part.text %] [% branch.text %]
[% END %] EOT $Templates{conditions} = <<'EOT'; [% WRAPPER html %]

Condition Coverage

[% PROCESS header criteria = [ "condition" ] %] [% FOREACH type = types %]

[% type.name %] conditions

[% FOREACH header = type.headers %] [% END %] [% FOREACH condition = type.conditions %] [% FOREACH part = condition.parts %] [% END %] [% END %]
line [% header %] condition
[% condition.number %] [% part.text %] [% condition.text %]
[% END %] [% END %] EOT $Templates{subroutines} = <<'EOT'; [% WRAPPER html %]

Subroutine Coverage

[% crit = []; crit.push("subroutine") IF R.options.show.subroutine; crit.push("pod") IF R.options.show.pod; PROCESS header criteria = crit; %] [% IF R.options.show.subroutine %] [% END %] [% IF R.options.show.pod %] [% END %] [% FOREACH sub = subs %] [% IF R.options.show.subroutine %] [% END %] [% IF R.options.show.pod %] [% END %] [% END %]
line count pod subroutine
[% sub.line %] [% sub.count %] [% sub.pod %] [% sub.name %]
[% END %] EOT # remove some whitespace from templates s/^\s+//gm for values %Templates; 1; =head1 NAME Devel::Cover::Report::Html_basic - Backend for HTML reporting of coverage statistics =head1 SYNOPSIS use Devel::Cover::Report::Html_basic; Devel::Cover::Report::Html_basic->report($db, $options); =head1 DESCRIPTION This module provides a HTML reporting mechanism for coverage data. It is designed to be called from the C program. It will add syntax highlighting if C or C is installed. =head1 SEE ALSO Devel::Cover =head1 BUGS Huh? =head1 VERSION Version 0.61 - 10th January 2007 =head1 LICENCE Copyright 2001-2007, Paul Johnson (pjcj@cpan.org) This software is free. It is licensed under the same terms as Perl itself. The latest version of this software should be available from my homepage: http://www.pjcj.net =cut package Devel::Cover::Report::Html_basic; __DATA__ /* Stylesheet for Devel::Cover HTML reports */ /* You may modify this file to alter the appearance of your coverage * reports. If you do, you should probably flag it read-only to prevent * future runs from overwriting it. */ /* Note: default values use the color-safe web palette. */ body { font-family: sans-serif; } h1 { text-align : center; background-color: #cc99ff; border: solid 1px #999999; padding: 0.2em; -moz-border-radius: 10px; } a { color: #000000; } a:visited { color: #333333; } table { border-spacing: 0px; } tr { text-align : center; vertical-align: top; } th,.h,.hh { background-color: #cccccc; border: solid 1px #333333; padding: 0em 0.2em; width: 2.5em; -moz-border-radius: 4px; } .hh { width: 25%; } td { border: solid 1px #cccccc; border-top: none; border-left: none; -moz-border-radius: 4px; } .hblank { height: 0.5em; } .dblank { border: none; } /* source code */ pre,.s { text-align: left; font-family: monospace; white-space: pre; padding: 0.2em 0.5em 0em 0.5em; } /* Classes for color-coding coverage information: * c0 : path not covered or coverage < 75% * c1 : coverage >= 75% * c2 : coverage >= 90% * c3 : path covered or coverage = 100% */ .c0 { background-color: #ff9999; border: solid 1px #cc0000; } .c1 { background-color: #ffcc99; border: solid 1px #ff9933; } .c2 { background-color: #ffff99; border: solid 1px #cccc66; } .c3 { background-color: #99ff99; border: solid 1px #009900; } /* For syntax highlighting with PPI::HTML */ .line_number { color: #aaaaaa; } .comment { color: #228B22; } .symbol { color: #00688B; } .word { color: #8B008B; font-weight:bold; } .pragma { color: #8B008B; font-weight:bold; } .structure { color: #000000; } .number { color: #B452CD; } .single { color: #CD5555;} .double { color: #CD5555;} .match { color: #CD5555;} .substitute { color: #CD5555;} .heredoc_content { color: #CD5555;} .interpolate { color: #CD5555;} .words { color: #CD5555;} /* for syntax highlighting with Perl::Tidy */ .c { color: #228B22; } /* comment */ .cm { color: #000000; } /* comma */ .co { color: #000000; } /* colon */ .h { color: #CD5555; font-weight:bold; } /* here-doc-target */ .hh { color: #CD5555; font-style:italic; } /* here-doc-text */ .i { color: #00688B; } /* identifier */ .j { color: #000000; font-weight:bold; } /* label */ .k { color: #8B4513; font-weight:bold; } /* keyword */ .m { color: #FF0000; font-weight:bold; } /* subroutine */ .n { color: #B452CD; } /* numeric */ .p { color: #000000; } /* paren */ .pd { color: #228B22; font-style:italic; } /* pod-text */ .pu { color: #000000; } /* punctuation */ .q { color: #CD5555; } /* quote */ .s { color: #000000; } /* structure */ .sc { color: #000000; } /* semicolon */ .v { color: #B452CD; } /* v-string */ .w { color: #000000; } /* bareword */