File Coverage

lib/TAP/Formatter/HTML.pm
Criterion Covered Total %
statement 236 248 95.1
branch 62 82 75.6
condition 19 31 61.2
subroutine 43 43 100.0
pod 2 22 9.0
total 362 426 84.9


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             TAP::Formatter::HTML - TAP Test Harness output delegate for html output
4              
5             =head1 SYNOPSIS
6              
7             ##
8             ## command-line usage (alpha):
9             ##
10             prove -m -Q -P HTML=outfile:out.html,css_uri:style.css,js_uri:foo.js,force_inline_css:0
11              
12             # backwards compat usage:
13             prove -m -Q --formatter=TAP::Formatter::HTML >output.html
14              
15             # for more detail:
16             perldoc App::Prove::Plugin::HTML
17              
18             ##
19             ## perl usage:
20             ##
21             use TAP::Harness;
22              
23             my @tests = glob( 't/*.t' );
24             my $harness = TAP::Harness->new({ formatter_class => 'TAP::Formatter::HTML',
25             merge => 1 });
26             $harness->runtests( @tests );
27             # prints HTML to STDOUT by default
28              
29             # or if you really don't want STDERR merged in:
30             my $harness = TAP::Harness->new({ formatter_class => 'TAP::Formatter::HTML' });
31              
32             # to use a custom formatter:
33             my $fmt = TAP::Formatter::HTML->new;
34             $fmt->css_uris([])->inline_css( $my_css )
35             ->js_uris(['http://mysite.com/jquery.js', 'http://mysite.com/custom.js'])
36             ->inline_js( '$(div.summary).hide()' );
37              
38             my $harness = TAP::Harness->new({ formatter => $fmt, merge => 1 });
39              
40             # to output HTML to a file[handle]:
41             $fmt->output_fh( $fh );
42             $fmt->output_file( '/tmp/foo.html' );
43              
44             # you can use your own customized templates too:
45             $fmt->template('custom.tt2')
46             ->template_processor( Template->new )
47             ->force_inline_css(0)
48             ->force_inline_js(0);
49              
50             =cut
51              
52             package TAP::Formatter::HTML;
53              
54 14     14   181177 use strict;
  14         48  
  14         410  
55 14     14   69 use warnings;
  14         23  
  14         343  
56              
57 14     14   7376 use URI;
  14         68237  
  14         402  
58 14     14   6136 use URI::file;
  14         67593  
  14         475  
59 14     14   7417 use Template;
  14         282310  
  14         514  
60 14     14   7396 use POSIX qw( ceil );
  14         93221  
  14         81  
61 14     14   27498 use IO::File;
  14         16531  
  14         1801  
62 14     14   1792 use File::Temp qw( tempfile tempdir );
  14         21612  
  14         1094  
63 14     14   6503 use File::Spec::Functions qw( catdir catfile file_name_is_absolute rel2abs );
  14         11772  
  14         1131  
64              
65 14     14   6300 use TAP::Formatter::HTML::Session;
  14         51  
  14         510  
66              
67             # DEBUG:
68             #use Data::Dumper 'Dumper';
69              
70 14     14   84 use base qw( TAP::Base );
  14         29  
  14         1204  
71 14         79 use accessors qw( verbosity stdout output_fh escape_output tests session_class sessions
72             template_processor template html html_id_iterator minify color
73 14     14   92 css_uris js_uris inline_css inline_js abs_file_paths force_inline_css force_inline_js );
  14         24  
74              
75 14     14   10253 use constant default_session_class => 'TAP::Formatter::HTML::Session';
  14         37  
  14         877  
76 14     14   94 use constant default_template => 'TAP/Formatter/HTML/default_report.tt2';
  14         27  
  14         834  
77 14         904 use constant default_js_uris => ['file:TAP/Formatter/HTML/jquery-1.4.2.min.js',
78             'file:TAP/Formatter/HTML/jquery.tablesorter-2.0.3.min.js',
79 14     14   89 'file:TAP/Formatter/HTML/default_report.js'];
  14         56  
80 14         1203 use constant default_css_uris => ['file:TAP/Formatter/HTML/default_page.css',
81 14     14   87 'file:TAP/Formatter/HTML/default_report.css'];
  14         21  
82              
83 14         39475 use constant severity_map => {
84             '' => 0,
85             'very-low' => 1,
86             'low' => 2,
87             'med' => 3,
88             'high' => 4,
89             'very-high' => 5,
90             0 => '',
91             1 => 'very-low',
92             2 => 'low',
93             3 => 'med',
94             4 => 'high',
95             5 => 'very-high',
96 14     14   138 };
  14         28  
97              
98             our $VERSION = '0.13';
99             our $FAKE_WIN32_URIS = 0; # for testing only
100              
101             sub _initialize {
102 16     16   32468 my ($self, $args) = @_;
103              
104 16   100     72 $args ||= {};
105 16         109 $self->SUPER::_initialize($args);
106              
107 16 50       401 my $stdout_fh = IO::File->new_from_fd( fileno(STDOUT), 'w' )
108             or die "Error opening STDOUT for writing: $!";
109              
110 16         1551 $self->verbosity( 0 )
111             ->stdout( $stdout_fh )
112             ->output_fh( $stdout_fh )
113             ->minify( 1 )
114             ->escape_output( 0 )
115             ->abs_file_paths( 1 )
116             ->abs_file_paths( 1 )
117             ->force_inline_css( 1 )
118             ->force_inline_js( 0 )
119             ->session_class( $self->default_session_class )
120             ->template_processor( $self->default_template_processor )
121             ->template( $self->default_template )
122             ->js_uris( $self->default_js_uris )
123             ->css_uris( $self->default_css_uris )
124             ->inline_js( '' )
125             ->inline_css( '' )
126             ->sessions( [] );
127              
128 16         350202 $self->check_for_overrides_in_env;
129              
130             # Laziness...
131             # trust the user knows what they're doing with the args:
132 16         72 foreach my $key (keys %$args) {
133 30 100       273 $self->$key( $args->{$key} ) if ($self->can( $key ));
134             }
135              
136 16         124 $self->html_id_iterator( $self->create_iterator( $args ) );
137              
138 16         125 return $self;
139             }
140              
141             sub check_for_overrides_in_env {
142 16     16 0 54 my $self = shift;
143              
144 16 100       79 if (my $file = $ENV{TAP_FORMATTER_HTML_OUTFILE}) {
145 3         10 $self->output_file( $file );
146             }
147              
148 16         64 my $force_css = $ENV{TAP_FORMATTER_HTML_FORCE_INLINE_CSS};
149 16 100       65 if (defined( $force_css )) {
150 3         11 $self->force_inline_css( $force_css );
151             }
152              
153 16         50 my $force_js = $ENV{TAP_FORMATTER_HTML_FORCE_INLINE_JS};
154 16 100       55 if (defined( $force_js )) {
155 1         4 $self->force_inline_js( $force_js );
156             }
157              
158 16 100       66 if (my $uris = $ENV{TAP_FORMATTER_HTML_CSS_URIS}) {
159 2         27 my $list = [ split( ':', $uris ) ];
160 2         9 $self->css_uris( $list );
161             }
162              
163 16 100       76 if (my $uris = $ENV{TAP_FORMATTER_HTML_JS_URIS}) {
164 2         9 my $list = [ split( ':', $uris ) ];
165 2         7 $self->js_uris( $list );
166             }
167              
168 16 100       58 if (my $file = $ENV{TAP_FORMATTER_HTML_TEMPLATE}) {
169 1         4 $self->template( $file );
170             }
171              
172 16         37 return $self;
173             }
174              
175             sub default_template_processor {
176 16     16 0 669 my $path = __FILE__;
177 16         93 $path =~ s/.TAP.Formatter.HTML.pm$//;
178 16         91 return Template->new(
179             # arguably shouldn't compile as this is only used once
180             COMPILE_DIR => catdir( tempdir( CLEANUP => 1 ), 'TAP-Formatter-HTML' ),
181             COMPILE_EXT => '.ttc',
182             INCLUDE_PATH => $path,
183             );
184             }
185              
186              
187             sub output_file {
188 3     3 1 7 my ($self, $file) = @_;
189 3 50       19 my $fh = IO::File->new( $file, 'w' )
190             or die "Error opening '$file' for writing: $!";
191 3         445 $self->output_fh( $fh );
192             }
193              
194             sub create_iterator {
195 16     16 0 38 my $self = shift;
196 16   50     47 my $args = shift || {};
197 16   50     112 my $prefix = $args->{html_id_prefix} || 't';
198 16         33 my $i = 0;
199 16     1206   152 my $iter = sub { return $prefix . $i++ };
  1206         4604  
200             }
201              
202             sub verbose {
203 22     22 0 62 my $self = shift;
204             # emulate a classic accessor for compat w/TAP::Formatter::Console:
205 22 50       101 if (@_) { $self->verbosity(1) }
  0         0  
206 22         74 return $self->verbosity >= 1;
207             }
208              
209             sub quiet {
210 608     608 0 771 my $self = shift;
211             # emulate a classic accessor for compat w/TAP::Formatter::Console:
212 608 100       1304 if (@_) { $self->verbosity(-1) }
  1         4  
213 608         1217 return $self->verbosity <= -1;
214             }
215              
216             sub really_quiet {
217 1208     1208 0 1404 my $self = shift;
218             # emulate a classic accessor for compat w/TAP::Formatter::Console:
219 1208 100       2327 if (@_) { $self->verbosity(-2) }
  2         8  
220 1208         2265 return $self->verbosity <= -2;
221             }
222              
223             sub silent {
224 793     793 0 897 my $self = shift;
225             # emulate a classic accessor for compat w/TAP::Formatter::Console:
226 793 100       1239 if (@_) { $self->verbosity(-3) }
  8         35  
227 793         1332 return $self->verbosity <= -3;
228             }
229              
230             # Called by Test::Harness before any test output is generated.
231             sub prepare {
232 15     15 0 143258 my ($self, @tests) = @_;
233             # warn ref($self) . "->prepare called with args:\n" . Dumper( \@tests );
234 15         83 $self->info( 'running ', scalar @tests, ' tests' );
235 15         155 $self->tests( [@tests] );
236             }
237              
238             # Called to create a new test session. A test session looks like this:
239             #
240             # my $session = $formatter->open_test( $test, $parser );
241             # while ( defined( my $result = $parser->next ) ) {
242             # $session->result($result);
243             # exit 1 if $result->is_bailout;
244             # }
245             # $session->close_test;
246             sub open_test {
247 38     38 0 777015 my ($self, $test, $parser) = @_;
248             #warn ref($self) . "->open_test called with args: " . Dumper( [$test, $parser] );
249 38         909 my $session = $self->session_class->new({ test => $test,
250             parser => $parser,
251             formatter => $self });
252 38         121 push @{ $self->sessions }, $session;
  38         299  
253 38         611 return $session;
254             }
255              
256             # $str = $harness->summary( $aggregate );
257             #
258             # C produces the summary report after all tests are run. The argument is
259             # an aggregate.
260             sub summary {
261 14     14 1 8641 my ($self, $aggregate) = @_;
262             #warn ref($self) . "->summary called with args: " . Dumper( [$aggregate] );
263              
264             # farmed out to make sub-classing easy:
265 14         201 my $report = $self->prepare_report( $aggregate );
266 14         99 $self->generate_report( $report );
267              
268             # if silent is set, only print HTML if we're not printing to stdout
269 14 100 100     82 if (! $self->silent or $self->output_fh->fileno != fileno(STDOUT)) {
270 11         379 print { $self->output_fh } ${ $self->html };
  11         49  
  11         61  
271 11         738 $self->output_fh->flush;
272             }
273              
274 14         1326 return $self;
275             }
276              
277             sub generate_report {
278 14     14 0 89 my ($self, $r) = @_;
279              
280 14         103 $self->check_uris;
281 14 100       96 if($self->force_inline_css) {
282 6         62 $self->slurp_css;
283 6         82 $self->css_uris([]);
284             }
285 14 100       230 if($self->force_inline_js) {
286 1         8 $self->slurp_js;
287 1         109 $self->js_uris([]);
288             }
289              
290 14         190 my $params = {
291             report => $r,
292             js_uris => $self->js_uris,
293             css_uris => $self->css_uris,
294             inline_js => $self->inline_js,
295             inline_css => $self->inline_css,
296             formatter => { class => ref( $self ),
297             version => $self->VERSION },
298             };
299              
300 14         946 my $html = '';
301 14 50       123 $self->template_processor->process( $self->template, $params, \$html )
302             || die $self->template_processor->error;
303              
304 14         4366 $self->html( \$html );
305 14 50       142 $self->minify_report if $self->minify;
306              
307 14         67 return $self;
308             }
309              
310             # try and reduce the size of the report
311             sub minify_report {
312 14     14 0 138 my $self = shift;
313 14         47 my $html_ref = $self->html;
314 14         3088 $$html_ref =~ s/^\t+//mg;
315 14         66 return $self;
316             }
317              
318             # convert all uris to URI objs
319             # check file uris (if relative & not found, try & find them in @INC)
320             sub check_uris {
321 14     14 0 42 my ($self) = @_;
322              
323 14         134 foreach my $uri_list ($self->js_uris, $self->css_uris) {
324             # take them out of the list to verify, push them back on later
325 28         575 my @uris = splice( @$uri_list, 0, scalar @$uri_list );
326 28         84 foreach my $uri (@uris) {
327 70 100 66     702 if (($^O =~ /win32/i or $FAKE_WIN32_URIS)
      66        
328             and $uri =~ /^(?:(?:file)|(?:\w:)?\\)/) {
329 4         40 $uri = URI::file->new($uri, 'win32');
330             } else {
331 66         503 $uri = URI->new( $uri );
332             }
333 70 100 66     11071 if ($uri->scheme && $uri->scheme eq 'file') {
334 64         3909 my $path = $uri->path;
335 64 100       1175 unless (file_name_is_absolute($path)) {
336 55         685 my $new_path;
337 55 50       754 if (-e $path) {
338 0 0       0 $new_path = rel2abs( $path ) if ($self->abs_file_paths);
339             } else {
340 55         251 $new_path = $self->find_in_INC( $path );
341             }
342 55 50       160 if ($new_path) {
343 55 50 33     396 if (($^O =~ /win32/i or $FAKE_WIN32_URIS)) {
344 0         0 $uri = URI::file->new("file://$new_path", 'win32');
345             } else {
346 55         182 $uri->path( $new_path );
347             }
348             }
349             }
350             }
351 70         3264 push @$uri_list, $uri;
352             }
353             }
354              
355 14         45 return $self;
356             }
357              
358             sub prepare_report {
359 14     14 0 74 my ($self, $a) = @_;
360              
361 14         121 my $r = {
362             tests => [],
363             start_time => '?',
364             end_time => '?',
365             elapsed_time => $a->elapsed_timestr,
366             };
367              
368              
369             # add aggregate test info:
370 14         3676 for my $key (qw(
371             total
372             has_errors
373             has_problems
374             failed
375             parse_errors
376             passed
377             skipped
378             todo
379             todo_passed
380             wait
381             exit
382             )) {
383 154         1603 $r->{$key} = $a->$key;
384             }
385              
386             # do some other handy calcs:
387 14 100       144 if ($r->{total}) {
388 13         221 $r->{percent_passed} = sprintf('%.1f', $r->{passed} / $r->{total} * 100);
389             } else {
390 1         5 $r->{percent_passed} = 0;
391             }
392              
393             # estimate # files (# sessions could be different?):
394 14         55 $r->{num_files} = scalar @{ $self->sessions };
  14         96  
395              
396             # add test results:
397 14         169 my $total_time = 0;
398 14         48 foreach my $s (@{ $self->sessions }) {
  14         85  
399 38         197 my $sr = $s->as_report;
400 38         64 push @{$r->{tests}}, $sr;
  38         96  
401 38   50     155 $total_time += $sr->{elapsed_time} || 0;
402             }
403 14         61 $r->{total_time} = $total_time;
404              
405             # estimate total severity:
406 14         448 my $smap = $self->severity_map;
407 14         60 my $severity = 0;
408 14   100     43 $severity += $smap->{$_->{severity} || ''} for @{$r->{tests}};
  14         264  
409 14         58 my $avg_severity = 0;
410 14 100       30 if (scalar @{$r->{tests}}) {
  14         112  
411 13         43 $avg_severity = ceil($severity / scalar( @{$r->{tests}} ));
  13         218  
412             }
413 14         128 $r->{severity} = $smap->{$avg_severity};
414              
415             # TODO: coverage?
416              
417 14         71 return $r;
418             }
419              
420             # adapted from Test::TAP::HTMLMatrix
421             # always return abs file paths if $self->abs_file_paths is on
422             sub find_in_INC {
423 55     55 0 119 my ($self, $file) = @_;
424              
425 55         176 foreach my $path (grep { not ref } @INC) {
  925         1495  
426 110         705 my $target = catfile($path, $file);
427 110 100       1649 if (-e $target) {
428 55 50       360 $target = rel2abs($target) if $self->abs_file_paths;
429 55         2587 return $target;
430             }
431             }
432              
433             # non-fatal
434 0         0 $self->log("Warning: couldn't find $file in \@INC");
435 0         0 return;
436             }
437              
438             # adapted from Test::TAP::HTMLMatrix
439             # slurp all 'file' uris, if possible
440             # note: doesn't remove them from the css_uris list, just in case...
441             sub slurp_css {
442 6     6 0 25 my ($self) = shift;
443 6         60 $self->info("slurping css files inline");
444              
445 6         60 my $inline_css = '';
446 6         29 $self->_slurp_uris( $self->css_uris, \$inline_css );
447              
448             # append any inline css so it gets interpreted last:
449 6 50       52 $inline_css .= "\n" . $self->inline_css if $self->inline_css;
450              
451 6         121 $self->inline_css( $inline_css );
452             }
453              
454             sub slurp_js {
455 1     1 0 2 my ($self) = shift;
456 1         3 $self->info("slurping js files inline");
457              
458 1         8 my $inline_js = '';
459 1         3 $self->_slurp_uris( $self->js_uris, \$inline_js );
460              
461             # append any inline js so it gets interpreted last:
462 1 50       6 $inline_js .= "\n" . $self->inline_js if $self->inline_js;
463              
464 1         10 $self->inline_js( $inline_js );
465             }
466              
467             sub _slurp_uris {
468 7     7   62 my ($self, $uris, $slurp_to_ref) = @_;
469              
470 7         42 foreach my $uri (@$uris) {
471 15         120 my $scheme = $uri->scheme;
472 15 50 33     403 if ($scheme && $scheme eq 'file') {
473 15         74 my $path = $uri->path;
474 15 50       474 if (-e $path) {
475 15 50       826 if (open my $fh, $path) {
476 15         117 local $/ = undef;
477 15         950 $$slurp_to_ref .= <$fh>;
478 15         284 $$slurp_to_ref .= "\n";
479             } else {
480 0         0 $self->log("Warning: couldn't open $path: $!");
481             }
482             } else {
483 0         0 $self->log("Warning: couldn't read $path: file does not exist!");
484             }
485             } else {
486 0         0 $self->log("Warning: can't include $uri inline: not a file uri");
487             }
488             }
489              
490 7         29 return $slurp_to_ref;
491             }
492              
493              
494              
495             sub log {
496 771     771 0 989 my $self = shift;
497 771 50       1242 push @_, "\n" unless grep {/\n/} @_;
  783         3418  
498 771         1818 $self->_output( @_ );
499 771         3945 return $self;
500             }
501              
502             sub info {
503 22     22 0 73 my $self = shift;
504 22 50       94 return unless $self->verbose;
505 0         0 return $self->log( @_ );
506             }
507              
508             sub log_test {
509 1206     1206 0 4704 my $self = shift;
510 1206 100       2088 return if $self->really_quiet;
511 561         2618 return $self->log( @_ );
512             }
513              
514             sub log_test_info {
515 607     607 0 2866 my $self = shift;
516 607 100       1492 return if $self->quiet;
517 210         1169 return $self->log( @_ );
518             }
519              
520             sub _output {
521 771     771   822 my $self = shift;
522 771 50       1129 return if $self->silent;
523 771 50 33     3821 if (ref($_[0]) && ref( $_[0]) eq 'SCALAR') {
524             # DEPRECATED: printing HTML:
525 0         0 print { $self->stdout } ${ $_[0] };
  0         0  
  0         0  
526             } else {
527 771 100       1390 unshift @_, '# ' if $self->escape_output;
528 771         2565 print { $self->stdout } @_;
  771         1107  
529             }
530             }
531              
532              
533             1;
534              
535              
536             __END__