File Coverage

lib/TAP/Formatter/HTML.pm
Criterion Covered Total %
statement 233 245 95.1
branch 62 82 75.6
condition 18 31 58.0
subroutine 43 43 100.0
pod 2 22 9.0
total 358 423 84.6


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   169100 use strict;
  14         29  
  14         575  
55 14     14   77 use warnings;
  14         26  
  14         456  
56              
57 14     14   13960 use URI;
  14         87672  
  14         837  
58 14     14   13721 use URI::file;
  14         120247  
  14         511  
59 14     14   26137 use Template;
  14         467400  
  14         496  
60 14     14   15121 use POSIX qw( ceil );
  14         140184  
  14         153  
61 14     14   37302 use IO::File;
  14         26928  
  14         2788  
62 14     14   4007 use File::Temp qw( tempfile tempdir );
  14         46326  
  14         1292  
63 14     14   18128 use File::Spec::Functions qw( catdir catfile file_name_is_absolute rel2abs );
  14         19560  
  14         1253  
64              
65 14     14   9539 use TAP::Formatter::HTML::Session;
  14         44  
  14         845  
66              
67             # DEBUG:
68             #use Data::Dumper 'Dumper';
69              
70 14     14   95 use base qw( TAP::Base );
  14         27  
  14         1518  
71 14         87 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   79 css_uris js_uris inline_css inline_js abs_file_paths force_inline_css force_inline_js );
  14         59  
74              
75 14     14   9417 use constant default_session_class => 'TAP::Formatter::HTML::Session';
  14         30  
  14         812  
76 14     14   132 use constant default_template => 'TAP/Formatter/HTML/default_report.tt2';
  14         29  
  14         1104  
77 14         770 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   126 'file:TAP/Formatter/HTML/default_report.js'];
  14         27  
80 14         1050 use constant default_css_uris => ['file:TAP/Formatter/HTML/default_page.css',
81 14     14   62 'file:TAP/Formatter/HTML/default_report.css'];
  14         24  
82              
83 14         51892 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   64 };
  14         29  
97              
98             our $VERSION = '0.11';
99             our $FAKE_WIN32_URIS = 0; # for testing only
100              
101             sub _initialize {
102 16     16   36989 my ($self, $args) = @_;
103              
104 16   100     100 $args ||= {};
105 16         150 $self->SUPER::_initialize($args);
106              
107 16 50       7050 my $stdout_fh = IO::File->new_from_fd( fileno(STDOUT), 'w' )
108             or die "Error opening STDOUT for writing: $!";
109              
110 16         1395 $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         496998 $self->check_for_overrides_in_env;
129              
130             # Laziness...
131             # trust the user knows what they're doing with the args:
132 16         80 foreach my $key (keys %$args) {
133 28 100       323 $self->$key( $args->{$key} ) if ($self->can( $key ));
134             }
135              
136 16         155 $self->html_id_iterator( $self->create_iterator( $args ) );
137              
138 16         155 return $self;
139             }
140              
141             sub check_for_overrides_in_env {
142 16     16 0 47 my $self = shift;
143              
144 16 100       148 if (my $file = $ENV{TAP_FORMATTER_HTML_OUTFILE}) {
145 3         15 $self->output_file( $file );
146             }
147              
148 16         71 my $force_css = $ENV{TAP_FORMATTER_HTML_FORCE_INLINE_CSS};
149 16 100       66 if (defined( $force_css )) {
150 3         14 $self->force_inline_css( $force_css );
151             }
152              
153 16         63 my $force_js = $ENV{TAP_FORMATTER_HTML_FORCE_INLINE_JS};
154 16 100       63 if (defined( $force_js )) {
155 1         4 $self->force_inline_js( $force_js );
156             }
157              
158 16 100       85 if (my $uris = $ENV{TAP_FORMATTER_HTML_CSS_URIS}) {
159 2         15 my $list = [ split( ':', $uris ) ];
160 2         10 $self->css_uris( $list );
161             }
162              
163 16 100       85 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       92 if (my $file = $ENV{TAP_FORMATTER_HTML_TEMPLATE}) {
169 1         3 $self->template( $file );
170             }
171              
172 16         43 return $self;
173             }
174              
175             sub default_template_processor {
176 16     16 0 813 my $path = __FILE__;
177 16         305 $path =~ s/.TAP.Formatter.HTML.pm$//;
178 16         110 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 8 my ($self, $file) = @_;
189 3 50       33 my $fh = IO::File->new( $file, 'w' )
190             or die "Error opening '$file' for writing: $!";
191 3         592 $self->output_fh( $fh );
192             }
193              
194             sub create_iterator {
195 16     16 0 40 my $self = shift;
196 16   50     72 my $args = shift || {};
197 16   50     145 my $prefix = $args->{html_id_prefix} || 't';
198 16         30 my $i = 0;
199 16     1206   167 my $iter = sub { return $prefix . $i++ };
  1206         6156  
200             }
201              
202             sub verbose {
203 22     22 0 53 my $self = shift;
204             # emulate a classic accessor for compat w/TAP::Formatter::Console:
205 22 50       104 if (@_) { $self->verbosity(1) }
  0         0  
206 22         89 return $self->verbosity >= 1;
207             }
208              
209             sub quiet {
210 607     607 0 867 my $self = shift;
211             # emulate a classic accessor for compat w/TAP::Formatter::Console:
212 607 100       1467 if (@_) { $self->verbosity(-1) }
  1         6  
213 607         1787 return $self->verbosity <= -1;
214             }
215              
216             sub really_quiet {
217 1208     1208 0 1586 my $self = shift;
218             # emulate a classic accessor for compat w/TAP::Formatter::Console:
219 1208 100       3211 if (@_) { $self->verbosity(-2) }
  2         12  
220 1208         2999 return $self->verbosity <= -2;
221             }
222              
223             sub silent {
224 793     793 0 999 my $self = shift;
225             # emulate a classic accessor for compat w/TAP::Formatter::Console:
226 793 100       1866 if (@_) { $self->verbosity(-3) }
  8         42  
227 793         2025 return $self->verbosity <= -3;
228             }
229              
230             # Called by Test::Harness before any test output is generated.
231             sub prepare {
232 15     15 0 210109 my ($self, @tests) = @_;
233             # warn ref($self) . "->prepare called with args:\n" . Dumper( \@tests );
234 15         98 $self->info( 'running ', scalar @tests, ' tests' );
235 15         171 $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 1436492 my ($self, $test, $parser) = @_;
248             #warn ref($self) . "->open_test called with args: " . Dumper( [$test, $parser] );
249 38         994 my $session = $self->session_class->new({ test => $test,
250             parser => $parser,
251             formatter => $self });
252 38         138 push @{ $self->sessions }, $session;
  38         325  
253 38         564 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 6339 my ($self, $aggregate) = @_;
262             #warn ref($self) . "->summary called with args: " . Dumper( [$aggregate] );
263              
264             # farmed out to make sub-classing easy:
265 14         151 my $report = $self->prepare_report( $aggregate );
266 14         101 $self->generate_report( $report );
267              
268             # if silent is set, only print HTML if we're not printing to stdout
269 14 100 100     80 if (! $self->silent or $self->output_fh->fileno != fileno(STDOUT)) {
270 11         378 print { $self->output_fh } ${ $self->html };
  11         58  
  11         85  
271 11         1297 $self->output_fh->flush;
272             }
273              
274 14         1812 return $self;
275             }
276              
277             sub generate_report {
278 14     14 0 58 my ($self, $r) = @_;
279              
280 14         64 $self->check_uris;
281 14 100       68 $self->slurp_css if $self->force_inline_css;
282 14 100       178 $self->slurp_js if $self->force_inline_js;
283              
284 14         308 my $params = {
285             report => $r,
286             js_uris => $self->js_uris,
287             css_uris => $self->css_uris,
288             inline_js => $self->inline_js,
289             inline_css => $self->inline_css,
290             formatter => { class => ref( $self ),
291             version => $self->VERSION },
292             };
293              
294 14         876 my $html = '';
295 14 50       134 $self->template_processor->process( $self->template, $params, \$html )
296             || die $self->template_processor->error;
297              
298 14         4834 $self->html( \$html );
299 14 50       169 $self->minify_report if $self->minify;
300              
301 14         83 return $self;
302             }
303              
304             # try and reduce the size of the report
305             sub minify_report {
306 14     14 0 156 my $self = shift;
307 14         62 my $html_ref = $self->html;
308 14         5033 $$html_ref =~ s/^\t+//mg;
309 14         48 return $self;
310             }
311              
312             # convert all uris to URI objs
313             # check file uris (if relative & not found, try & find them in @INC)
314             sub check_uris {
315 14     14 0 33 my ($self) = @_;
316              
317 14         135 foreach my $uri_list ($self->js_uris, $self->css_uris) {
318             # take them out of the list to verify, push them back on later
319 28         619 my @uris = splice( @$uri_list, 0, scalar @$uri_list );
320 28         119 foreach my $uri (@uris) {
321 70 100 33     607 if (($^O =~ /win32/i or $FAKE_WIN32_URIS)
      66        
322             and $uri =~ /^(?:(?:file)|(?:\w:)?\\)/) {
323 4         34 $uri = URI::file->new($uri, 'win32');
324             } else {
325 66         424 $uri = URI->new( $uri );
326             }
327 70 100 66     10473 if ($uri->scheme && $uri->scheme eq 'file') {
328 64         3886 my $path = $uri->path;
329 64 100       1902 unless (file_name_is_absolute($path)) {
330 55         518 my $new_path;
331 55 50       565 if (-e $path) {
332 0 0       0 $new_path = rel2abs( $path ) if ($self->abs_file_paths);
333             } else {
334 55         157 $new_path = $self->find_in_INC( $path );
335             }
336 55 50       155 if ($new_path) {
337 55 50 33     311 if (($^O =~ /win32/i or $FAKE_WIN32_URIS)) {
338 0         0 $uri = URI::file->new("file://$new_path", 'win32');
339             } else {
340 55         248 $uri->path( $new_path );
341             }
342             }
343             }
344             }
345 70         2431 push @$uri_list, $uri;
346             }
347             }
348              
349 14         39 return $self;
350             }
351              
352             sub prepare_report {
353 14     14 0 52 my ($self, $a) = @_;
354              
355 14         209 my $r = {
356             tests => [],
357             start_time => '?',
358             end_time => '?',
359             elapsed_time => $a->elapsed_timestr,
360             };
361              
362              
363             # add aggregate test info:
364 14         3394 for my $key (qw(
365             total
366             has_errors
367             has_problems
368             failed
369             parse_errors
370             passed
371             skipped
372             todo
373             todo_passed
374             wait
375             exit
376             )) {
377 154         1416 $r->{$key} = $a->$key;
378             }
379              
380             # do some other handy calcs:
381 14         144 $r->{actual_passed} = $r->{passed} + $r->{todo_passed};
382 14 100       292 if ($r->{total}) {
383 13         167 $r->{percent_passed} = sprintf('%.1f', $r->{actual_passed} / $r->{total} * 100);
384             } else {
385 1         4 $r->{percent_passed} = 0;
386             }
387              
388             # estimate # files (# sessions could be different?):
389 14         220 $r->{num_files} = scalar @{ $self->sessions };
  14         274  
390              
391             # add test results:
392 14         137 my $total_time = 0;
393 14         140 foreach my $s (@{ $self->sessions }) {
  14         61  
394 38         186 my $sr = $s->as_report;
395 38         70 push @{$r->{tests}}, $sr;
  38         91  
396 38   50     207 $total_time += $sr->{elapsed_time} || 0;
397             }
398 14         48 $r->{total_time} = $total_time;
399              
400             # estimate total severity:
401 14         373 my $smap = $self->severity_map;
402 14         33 my $severity = 0;
403 14   100     37 $severity += $smap->{$_->{severity} || ''} for @{$r->{tests}};
  14         342  
404 14         46 my $avg_severity = 0;
405 14 100       42 if (scalar @{$r->{tests}}) {
  14         72  
406 13         33 $avg_severity = ceil($severity / scalar( @{$r->{tests}} ));
  13         226  
407             }
408 14         118 $r->{severity} = $smap->{$avg_severity};
409              
410             # TODO: coverage?
411              
412 14         50 return $r;
413             }
414              
415             # adapted from Test::TAP::HTMLMatrix
416             # always return abs file paths if $self->abs_file_paths is on
417             sub find_in_INC {
418 55     55 0 90 my ($self, $file) = @_;
419              
420 55         119 foreach my $path (grep { not ref } @INC) {
  925         1551  
421 110         539 my $target = catfile($path, $file);
422 110 100       1867 if (-e $target) {
423 55 50       295 $target = rel2abs($target) if $self->abs_file_paths;
424 55         2628 return $target;
425             }
426             }
427              
428             # non-fatal
429 0         0 $self->log("Warning: couldn't find $file in \@INC");
430 0         0 return;
431             }
432              
433             # adapted from Test::TAP::HTMLMatrix
434             # slurp all 'file' uris, if possible
435             # note: doesn't remove them from the css_uris list, just in case...
436             sub slurp_css {
437 6     6 0 70 my ($self) = shift;
438 6         55 $self->info("slurping css files inline");
439              
440 6         55 my $inline_css = '';
441 6         35 $self->_slurp_uris( $self->css_uris, \$inline_css );
442              
443             # append any inline css so it gets interpreted last:
444 6 50       44 $inline_css .= "\n" . $self->inline_css if $self->inline_css;
445              
446 6         78 $self->inline_css( $inline_css );
447             }
448              
449             sub slurp_js {
450 1     1 0 10 my ($self) = shift;
451 1         3 $self->info("slurping js files inline");
452              
453 1         6 my $inline_js = '';
454 1         3 $self->_slurp_uris( $self->js_uris, \$inline_js );
455              
456             # append any inline js so it gets interpreted last:
457 1 50       9 $inline_js .= "\n" . $self->inline_js if $self->inline_js;
458              
459 1         16 $self->inline_js( $inline_js );
460             }
461              
462             sub _slurp_uris {
463 7     7   47 my ($self, $uris, $slurp_to_ref) = @_;
464              
465 7         37 foreach my $uri (@$uris) {
466 15         59 my $scheme = $uri->scheme;
467 15 50 33     291 if ($scheme && $scheme eq 'file') {
468 15         74 my $path = $uri->path;
469 15 50       578 if (-e $path) {
470 15 50       813 if (open my $fh, $path) {
471 15         93 local $/ = undef;
472 15         1092 $$slurp_to_ref .= <$fh>;
473 15         249 $$slurp_to_ref .= "\n";
474             } else {
475 0         0 $self->log("Warning: couldn't open $path: $!");
476             }
477             } else {
478 0         0 $self->log("Warning: couldn't read $path: file does not exist!");
479             }
480             } else {
481 0         0 $self->log("Warning: can't include $uri inline: not a file uri");
482             }
483             }
484              
485 7         19 return $slurp_to_ref;
486             }
487              
488              
489              
490             sub log {
491 771     771 0 1129 my $self = shift;
492 771 50       1376 push @_, "\n" unless grep {/\n/} @_;
  783         5501  
493 771         2220 $self->_output( @_ );
494 771         7417 return $self;
495             }
496              
497             sub info {
498 22     22 0 49 my $self = shift;
499 22 50       107 return unless $self->verbose;
500 0         0 return $self->log( @_ );
501             }
502              
503             sub log_test {
504 1206     1206 0 6831 my $self = shift;
505 1206 100       2693 return if $self->really_quiet;
506 561         4073 return $self->log( @_ );
507             }
508              
509             sub log_test_info {
510 606     606 0 3499 my $self = shift;
511 606 100       1631 return if $self->quiet;
512 210         1578 return $self->log( @_ );
513             }
514              
515             sub _output {
516 771     771   862 my $self = shift;
517 771 50       1493 return if $self->silent;
518 771 50 33     7123 if (ref($_[0]) && ref( $_[0]) eq 'SCALAR') {
519             # DEPRECATED: printing HTML:
520 0         0 print { $self->stdout } ${ $_[0] };
  0         0  
  0         0  
521             } else {
522 771 100       2048 unshift @_, '# ' if $self->escape_output;
523 771         5204 print { $self->stdout } @_;
  771         1745  
524             }
525             }
526              
527              
528             1;
529              
530              
531             __END__