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   184850 use strict;
  14         50  
  14         422  
55 14     14   78 use warnings;
  14         22  
  14         362  
56              
57 14     14   7435 use URI;
  14         69748  
  14         414  
58 14     14   6413 use URI::file;
  14         68070  
  14         487  
59 14     14   7743 use Template;
  14         286557  
  14         514  
60 14     14   7696 use POSIX qw( ceil );
  14         96167  
  14         81  
61 14     14   28258 use IO::File;
  14         17194  
  14         1767  
62 14     14   1965 use File::Temp qw( tempfile tempdir );
  14         22814  
  14         1060  
63 14     14   6763 use File::Spec::Functions qw( catdir catfile file_name_is_absolute rel2abs );
  14         12210  
  14         1083  
64              
65 14     14   6654 use TAP::Formatter::HTML::Session;
  14         36  
  14         511  
66              
67             # DEBUG:
68             #use Data::Dumper 'Dumper';
69              
70 14     14   95 use base qw( TAP::Base );
  14         25  
  14         1164  
71 14         88 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   93 css_uris js_uris inline_css inline_js abs_file_paths force_inline_css force_inline_js );
  14         25  
74              
75 14     14   10402 use constant default_session_class => 'TAP::Formatter::HTML::Session';
  14         30  
  14         896  
76 14     14   91 use constant default_template => 'TAP/Formatter/HTML/default_report.tt2';
  14         21  
  14         824  
77 14         839 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   91 'file:TAP/Formatter/HTML/default_report.js'];
  14         43  
80 14         1164 use constant default_css_uris => ['file:TAP/Formatter/HTML/default_page.css',
81 14     14   85 'file:TAP/Formatter/HTML/default_report.css'];
  14         23  
82              
83 14         40579 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   179 };
  14         46  
97              
98             our $VERSION = '0.12';
99             our $FAKE_WIN32_URIS = 0; # for testing only
100              
101             sub _initialize {
102 16     16   30208 my ($self, $args) = @_;
103              
104 16   100     75 $args ||= {};
105 16         110 $self->SUPER::_initialize($args);
106              
107 16 50       360 my $stdout_fh = IO::File->new_from_fd( fileno(STDOUT), 'w' )
108             or die "Error opening STDOUT for writing: $!";
109              
110 16         1522 $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         355728 $self->check_for_overrides_in_env;
129              
130             # Laziness...
131             # trust the user knows what they're doing with the args:
132 16         89 foreach my $key (keys %$args) {
133 30 100       293 $self->$key( $args->{$key} ) if ($self->can( $key ));
134             }
135              
136 16         120 $self->html_id_iterator( $self->create_iterator( $args ) );
137              
138 16         127 return $self;
139             }
140              
141             sub check_for_overrides_in_env {
142 16     16 0 38 my $self = shift;
143              
144 16 100       107 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       74 if (defined( $force_css )) {
150 3         10 $self->force_inline_css( $force_css );
151             }
152              
153 16         48 my $force_js = $ENV{TAP_FORMATTER_HTML_FORCE_INLINE_JS};
154 16 100       50 if (defined( $force_js )) {
155 1         3 $self->force_inline_js( $force_js );
156             }
157              
158 16 100       105 if (my $uris = $ENV{TAP_FORMATTER_HTML_CSS_URIS}) {
159 2         11 my $list = [ split( ':', $uris ) ];
160 2         8 $self->css_uris( $list );
161             }
162              
163 16 100       78 if (my $uris = $ENV{TAP_FORMATTER_HTML_JS_URIS}) {
164 2         10 my $list = [ split( ':', $uris ) ];
165 2         9 $self->js_uris( $list );
166             }
167              
168 16 100       68 if (my $file = $ENV{TAP_FORMATTER_HTML_TEMPLATE}) {
169 1         2 $self->template( $file );
170             }
171              
172 16         35 return $self;
173             }
174              
175             sub default_template_processor {
176 16     16 0 658 my $path = __FILE__;
177 16         99 $path =~ s/.TAP.Formatter.HTML.pm$//;
178 16         95 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       21 my $fh = IO::File->new( $file, 'w' )
190             or die "Error opening '$file' for writing: $!";
191 3         463 $self->output_fh( $fh );
192             }
193              
194             sub create_iterator {
195 16     16 0 35 my $self = shift;
196 16   50     50 my $args = shift || {};
197 16   50     119 my $prefix = $args->{html_id_prefix} || 't';
198 16         38 my $i = 0;
199 16     1206   137 my $iter = sub { return $prefix . $i++ };
  1206         4583  
200             }
201              
202             sub verbose {
203 22     22 0 67 my $self = shift;
204             # emulate a classic accessor for compat w/TAP::Formatter::Console:
205 22 50       89 if (@_) { $self->verbosity(1) }
  0         0  
206 22         72 return $self->verbosity >= 1;
207             }
208              
209             sub quiet {
210 608     608 0 715 my $self = shift;
211             # emulate a classic accessor for compat w/TAP::Formatter::Console:
212 608 100       1247 if (@_) { $self->verbosity(-1) }
  1         5  
213 608         1218 return $self->verbosity <= -1;
214             }
215              
216             sub really_quiet {
217 1208     1208 0 1342 my $self = shift;
218             # emulate a classic accessor for compat w/TAP::Formatter::Console:
219 1208 100       2273 if (@_) { $self->verbosity(-2) }
  2         8  
220 1208         2210 return $self->verbosity <= -2;
221             }
222              
223             sub silent {
224 793     793 0 877 my $self = shift;
225             # emulate a classic accessor for compat w/TAP::Formatter::Console:
226 793 100       1300 if (@_) { $self->verbosity(-3) }
  8         31  
227 793         1328 return $self->verbosity <= -3;
228             }
229              
230             # Called by Test::Harness before any test output is generated.
231             sub prepare {
232 15     15 0 144813 my ($self, @tests) = @_;
233             # warn ref($self) . "->prepare called with args:\n" . Dumper( \@tests );
234 15         91 $self->info( 'running ', scalar @tests, ' tests' );
235 15         144 $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 842045 my ($self, $test, $parser) = @_;
248             #warn ref($self) . "->open_test called with args: " . Dumper( [$test, $parser] );
249 38         872 my $session = $self->session_class->new({ test => $test,
250             parser => $parser,
251             formatter => $self });
252 38         119 push @{ $self->sessions }, $session;
  38         338  
253 38         658 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 9891 my ($self, $aggregate) = @_;
262             #warn ref($self) . "->summary called with args: " . Dumper( [$aggregate] );
263              
264             # farmed out to make sub-classing easy:
265 14         204 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     104 if (! $self->silent or $self->output_fh->fileno != fileno(STDOUT)) {
270 11         402 print { $self->output_fh } ${ $self->html };
  11         51  
  11         69  
271 11         1035 $self->output_fh->flush;
272             }
273              
274 14         1286 return $self;
275             }
276              
277             sub generate_report {
278 14     14 0 78 my ($self, $r) = @_;
279              
280 14         122 $self->check_uris;
281 14 100       99 if($self->force_inline_css) {
282 6         76 $self->slurp_css;
283 6         77 $self->css_uris([]);
284             }
285 14 100       172 if($self->force_inline_js) {
286 1         11 $self->slurp_js;
287 1         102 $self->js_uris([]);
288             }
289              
290 14         173 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         993 my $html = '';
301 14 50       134 $self->template_processor->process( $self->template, $params, \$html )
302             || die $self->template_processor->error;
303              
304 14         4330 $self->html( \$html );
305 14 50       153 $self->minify_report if $self->minify;
306              
307 14         88 return $self;
308             }
309              
310             # try and reduce the size of the report
311             sub minify_report {
312 14     14 0 146 my $self = shift;
313 14         43 my $html_ref = $self->html;
314 14         3284 $$html_ref =~ s/^\t+//mg;
315 14         70 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 58 my ($self) = @_;
322              
323 14         154 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         596 my @uris = splice( @$uri_list, 0, scalar @$uri_list );
326 28         85 foreach my $uri (@uris) {
327 70 100 66     708 if (($^O =~ /win32/i or $FAKE_WIN32_URIS)
      66        
328             and $uri =~ /^(?:(?:file)|(?:\w:)?\\)/) {
329 4         46 $uri = URI::file->new($uri, 'win32');
330             } else {
331 66         506 $uri = URI->new( $uri );
332             }
333 70 100 66     11632 if ($uri->scheme && $uri->scheme eq 'file') {
334 64         3936 my $path = $uri->path;
335 64 100       1141 unless (file_name_is_absolute($path)) {
336 55         611 my $new_path;
337 55 50       713 if (-e $path) {
338 0 0       0 $new_path = rel2abs( $path ) if ($self->abs_file_paths);
339             } else {
340 55         223 $new_path = $self->find_in_INC( $path );
341             }
342 55 50       167 if ($new_path) {
343 55 50 33     389 if (($^O =~ /win32/i or $FAKE_WIN32_URIS)) {
344 0         0 $uri = URI::file->new("file://$new_path", 'win32');
345             } else {
346 55         214 $uri->path( $new_path );
347             }
348             }
349             }
350             }
351 70         2912 push @$uri_list, $uri;
352             }
353             }
354              
355 14         42 return $self;
356             }
357              
358             sub prepare_report {
359 14     14 0 85 my ($self, $a) = @_;
360              
361 14         156 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         4029 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         1749 $r->{$key} = $a->$key;
384             }
385              
386             # do some other handy calcs:
387 14 100       159 if ($r->{total}) {
388 13         273 $r->{percent_passed} = sprintf('%.1f', $r->{passed} / $r->{total} * 100);
389             } else {
390 1         4 $r->{percent_passed} = 0;
391             }
392              
393             # estimate # files (# sessions could be different?):
394 14         44 $r->{num_files} = scalar @{ $self->sessions };
  14         110  
395              
396             # add test results:
397 14         203 my $total_time = 0;
398 14         43 foreach my $s (@{ $self->sessions }) {
  14         72  
399 38         226 my $sr = $s->as_report;
400 38         73 push @{$r->{tests}}, $sr;
  38         117  
401 38   50     172 $total_time += $sr->{elapsed_time} || 0;
402             }
403 14         76 $r->{total_time} = $total_time;
404              
405             # estimate total severity:
406 14         480 my $smap = $self->severity_map;
407 14         60 my $severity = 0;
408 14   100     40 $severity += $smap->{$_->{severity} || ''} for @{$r->{tests}};
  14         308  
409 14         71 my $avg_severity = 0;
410 14 100       35 if (scalar @{$r->{tests}}) {
  14         114  
411 13         58 $avg_severity = ceil($severity / scalar( @{$r->{tests}} ));
  13         290  
412             }
413 14         159 $r->{severity} = $smap->{$avg_severity};
414              
415             # TODO: coverage?
416              
417 14         76 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 137 my ($self, $file) = @_;
424              
425 55         171 foreach my $path (grep { not ref } @INC) {
  925         1441  
426 110         651 my $target = catfile($path, $file);
427 110 100       1673 if (-e $target) {
428 55 50       698 $target = rel2abs($target) if $self->abs_file_paths;
429 55         2679 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 24 my ($self) = shift;
443 6         50 $self->info("slurping css files inline");
444              
445 6         54 my $inline_css = '';
446 6         45 $self->_slurp_uris( $self->css_uris, \$inline_css );
447              
448             # append any inline css so it gets interpreted last:
449 6 50       61 $inline_css .= "\n" . $self->inline_css if $self->inline_css;
450              
451 6         136 $self->inline_css( $inline_css );
452             }
453              
454             sub slurp_js {
455 1     1 0 3 my ($self) = shift;
456 1         3 $self->info("slurping js files inline");
457              
458 1         7 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         9 $self->inline_js( $inline_js );
465             }
466              
467             sub _slurp_uris {
468 7     7   62 my ($self, $uris, $slurp_to_ref) = @_;
469              
470 7         54 foreach my $uri (@$uris) {
471 15         101 my $scheme = $uri->scheme;
472 15 50 33     415 if ($scheme && $scheme eq 'file') {
473 15         65 my $path = $uri->path;
474 15 50       506 if (-e $path) {
475 15 50       847 if (open my $fh, $path) {
476 15         124 local $/ = undef;
477 15         999 $$slurp_to_ref .= <$fh>;
478 15         316 $$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         36 return $slurp_to_ref;
491             }
492              
493              
494              
495             sub log {
496 771     771 0 917 my $self = shift;
497 771 50       1195 push @_, "\n" unless grep {/\n/} @_;
  783         3196  
498 771         1689 $self->_output( @_ );
499 771         4161 return $self;
500             }
501              
502             sub info {
503 22     22 0 53 my $self = shift;
504 22 50       114 return unless $self->verbose;
505 0         0 return $self->log( @_ );
506             }
507              
508             sub log_test {
509 1206     1206 0 4731 my $self = shift;
510 1206 100       2059 return if $self->really_quiet;
511 561         2634 return $self->log( @_ );
512             }
513              
514             sub log_test_info {
515 607     607 0 2827 my $self = shift;
516 607 100       1236 return if $self->quiet;
517 210         1139 return $self->log( @_ );
518             }
519              
520             sub _output {
521 771     771   865 my $self = shift;
522 771 50       1188 return if $self->silent;
523 771 50 33     3865 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       1464 unshift @_, '# ' if $self->escape_output;
528 771         2708 print { $self->stdout } @_;
  771         1182  
529             }
530             }
531              
532              
533             1;
534              
535              
536             __END__