File Coverage

lib/Test/DocClaims.pm
Criterion Covered Total %
statement 157 176 89.2
branch 52 64 81.2
condition 14 18 77.7
subroutine 19 19 100.0
pod 2 2 100.0
total 244 279 87.4


line stmt bran cond sub pod time code
1             package Test::DocClaims;
2              
3             # Copyright (C) Scott E. Lee
4              
5 9     9   18587 use 5.008009;
  9         24  
6 9     9   39 use strict;
  9         15  
  9         150  
7 9     9   34 use warnings;
  9         18  
  9         223  
8 9     9   34 use Carp;
  9         11  
  9         419  
9 9     9   44 use File::Find;
  9         22  
  9         447  
10              
11 9     9   429 use Test::DocClaims::Lines;
  9         17  
  9         327  
12             our $VERSION = '0.001';
13              
14 9     9   43 use Test::Builder::Module;
  9         13  
  9         53  
15             our @ISA = qw< Test::Builder::Module >;
16             our @EXPORT = qw<
17             doc_claims
18             all_doc_claims
19             >;
20              
21             our $doc_file_re = qr/\.(pl|pm|pod|md)$/i;
22             our @doc_ignore_list;
23              
24             our $TODO;
25              
26             =head1 NAME
27              
28             Test::DocClaims - Help assure documentation claims are tested
29              
30             =head1 SYNOPSIS
31              
32             To automatically scan for source files containing POD, find the
33             corresponding tests and verify that those tests match the POD, create the
34             file t/doc_claims.t with the following lines:
35              
36             use Test::More;
37             eval "use Test::DocClaims";
38             plan skip_all => "Test::DocClaims not found" if $@;
39             all_doc_claims();
40              
41             Or, for more control over the POD files and which tests correspond to them:
42              
43             use Test::More;
44             eval "use Test::DocClaims";
45             plan skip_all => "Test::DocClaims not found" if $@;
46             plan tests => 2;
47             doc_claims( "lib/Foo/Bar.pm", "t/doc-Foo-Bar.t",
48             "doc claims in Foo/Bar.pm" );
49             doc_claims( "lib/Foo/Bar/Baz.pm", "t/doc-Foo-Bar-Baz.t",
50             "doc claims in Foo/Bar/Baz.pm" );
51              
52             If a source file (lib/Foo/Bar.pm) contains:
53              
54             =head2 add I I
55              
56             This adds two numbers.
57              
58             =cut
59              
60             sub add {
61             return $_[0] + $_[1];
62             }
63              
64             then the corresponding test (t/doc-Foo-Bar.t) might have:
65              
66             =head2 add I I
67              
68             This adds two numbers.
69              
70             =cut
71              
72             is( add(1,2), 3, "can add one and two" );
73             is( add(2,3), 5, "can add two and three" );
74              
75             =head1 DESCRIPTION
76              
77             A module should have documentation that defines its interface. All claims in
78             that documentation should have corresponding tests to verify that they are
79             true. Test::DocClaims is designed to help assure that those tests are written
80             and maintained.
81              
82             It would be great if software could read the documentation, enumerate all
83             of the claims made and then generate the tests to assure
84             that those claims are properly tested.
85             However, that level of artificial intelligence does not yet exist.
86             So, humans must be trusted to enumerate the claims and write the tests.
87              
88             How can Test::DocClaims help?
89             As the code and its documentation evolve, the test suite can fall out of
90             sync, no longer testing the new or modified claims.
91             This is where Test::DocClaims can assist.
92             First, a copy of the POD documentation must be placed in the test suite.
93             Then, after each claim, a test of that claim should be inserted.
94             Test::DocClaims compares the documentation in the code with the documentation
95             in the test suite and reports discrepancies.
96             This will act as a trigger to remind the human to update the test suite.
97             It is up to the human to actually edit the tests, not just sync up the
98             documentation.
99              
100             The comparison is done line by line.
101             Trailing white space is ignored.
102             Any white space sequence matches any other white space sequence.
103             Blank lines as well as "=cut" and "=pod" lines are ignored.
104             This allows tests to be inserted even in the middle of a paragraph by
105             placing a "=cut" line before and a "=pod" line after the test.
106              
107             Additionally, a special marker, of the form "=for DC_TODO", can be placed
108             in the test suite in lieu of writing a test.
109             This serves as a reminder to write the test later, but allows the
110             documentation to be in sync so the Test::DocClaims test will pass with a
111             todo warning.
112             Any text on the line after DC_TODO is ignored and can be used as a comment.
113              
114             Especially in the SYNOPSIS section, it is common practice to include
115             example code in the documentation.
116             In the test suite, if this code is surrounded by "=begin DC_CODE" and "=end
117             DC_CODE", it will be compared as if it were part of the POD, but can run as
118             part of the test.
119             For example, if this is in the documentation
120              
121             Here is an example:
122              
123             $obj->process("this is some text");
124              
125             this could be in the test
126              
127             Here is an example:
128              
129             =begin DC_CODE
130              
131             =cut
132              
133             $obj->process("this is some text");
134              
135             =end DC_CODE
136              
137             Example code that uses print or say and has a comment at the end will also
138             match a call to is() in the test.
139             For example, this in the documentation POD
140              
141             The add function will add two numbers:
142              
143             say add(1,2); # 3
144             say add(50,100); # 150
145              
146             will match this in the test.
147              
148             The add function will add two numbers:
149              
150             =begin DC_CODE
151              
152             =cut
153              
154             is(add(1,2), 3);
155             is(add(50,100), 150);
156              
157             =end DC_CODE
158              
159             When comparing code inside DC_CODE markers, all leading white space is
160             ignored.
161              
162             When the documentation file type does not support POD (such as mark down
163             files, *.md) then the entire file is assumed to be documentation and must
164             match the POD in the test file.
165             For these files, leading white space is ignored.
166             This allows a leading space to be added in the POD if necessary.
167              
168             =head1 FUNCTIONS
169              
170             =head2 doc_claims I I [ I ]
171              
172             Verify that the lines of documentation in I match the ones in
173             I.
174             The I and I arguments specify a list of one or more
175             files.
176             Each of the arguments can be one of:
177              
178             - a string which is the path to a file or a wildcard which is
179             expanded by the glob built-in function.
180             - a ref to a hash with these keys:
181             - path: path or wildcard (required)
182             - has_pod: true if the file can have POD (optional)
183             - a ref to an array, where each element is a path, wildcard or hash
184             as above
185              
186             If a list of files is given, those files are read in order and the
187             documentation in each is concatenated.
188             This is useful when a module file requires many tests that are best split
189             into multiple files in the test suite.
190             For example:
191              
192             doc_claims( "lib/Foo/Bar.pm", "t/Bar-*.t", "doc claims" );
193              
194             If a wildcard is used, be sure that the generated list of files is in the
195             correct order. It may be useful to number them (such as Foo-01-SYNOPSIS.t,
196             Foo-02-DESCRIPTION.t, etc).
197              
198             =cut
199              
200             sub doc_claims {
201 67     67 1 14468 my ( $doc_spec, $test_spec, $name ) = @_;
202 67 100       154 $name = "documentation claims are tested" unless defined $name;
203 67         194 my $doc = Test::DocClaims::Lines->new($doc_spec);
204 67         140 my $test = Test::DocClaims::Lines->new($test_spec);
205 67         148 _dbg_file( "D", $doc );
206 67         122 _dbg_file( "T", $test );
207 67         73 my @error;
208 67         93 my ( $test_line, $doc_line );
209 67         88 my $todo = 0;
210              
211 67   100     145 while ( !$doc->is_eof && !$test->is_eof ) {
212 715         1184 $doc_line = $doc->current_line;
213 715         1041 $test_line = $test->current_line;
214              
215             # Skip over the line if it is blank or is a non-POD line in a file
216             # that supports POD.
217 715         777 my $last = 0;
218 715   100     1094 while ( !$doc_line->is_doc || $doc_line->text =~ /^\s*$/ ) {
219 1875         2920 _dbg_line( "D", "s", $doc_line );
220 1875 100       2595 if ( $doc->advance_line ) {
221 1855         2534 $doc_line = $doc->current_line;
222             } else {
223 20         33 $last = 1;
224 20         35 last;
225             }
226             }
227 715   100     1181 while ( !$test_line->is_doc || $test_line->text =~ /^\s*$/ ) {
228 1984 100       2925 if ( $test_line->todo ) {
229 3         5 $todo++;
230             }
231 1984         2982 _dbg_line( "T", "s", $test_line );
232 1984 100       2679 if ( $test->advance_line ) {
233 1964         2632 $test_line = $test->current_line;
234             } else {
235 20         48 $last = 1;
236 20         26 last;
237             }
238             }
239 715 100       1065 last if $last;
240              
241 695 100       900 if ( _diff( $doc_line, $test_line ) ) {
242 693         1185 _dbg_line( "D", "M", $doc_line );
243 693         1032 _dbg_line( "T", "M", $test_line );
244 693         1276 $test->advance_line;
245 693         1013 $doc->advance_line;
246             } else {
247 2         8 _dbg_line( "D", "X", $doc_line );
248 2         3 _dbg_line( "T", "X", $test_line );
249 2         8 my $tb = Test::DocClaims->builder;
250 2         24 my $fail = $tb->ok( 0, $name );
251 2         25 _diff_error( $test_line, $doc_line, $name );
252 2         131 return $fail;
253             }
254             }
255              
256             # Ignore blank lines at the end of file.
257 65   66     126 while ( !$doc->is_eof && $doc->current_line =~ /^\s*$/ ) {
258 1         3 _dbg_line( "D", "e", $doc->current_line );
259 1         2 $doc->advance_line;
260             }
261 65   66     149 while ( !$test->is_eof && $test->current_line =~ /^\s*$/ ) {
262 13         35 _dbg_line( "T", "e", $test->current_line );
263 13         18 $test->advance_line;
264             }
265              
266 65 50 33     128 if ( !$test->is_eof || !$doc->is_eof ) {
267 0         0 my $tb = Test::DocClaims->builder;
268 0         0 my $fail = $tb->ok( 0, $name );
269 0         0 _diff_error( $test->current_line, $doc->current_line, $name );
270 0         0 return $fail;
271             } else {
272 65         222 my $tb = Test::DocClaims->builder;
273             TODO: {
274 65 100       563 local $TODO = "$todo DC_TODO lines" if $todo;
  65         114  
275 65 100       212 return $tb->ok( $todo ? 0 : 1, $name );
276             }
277             }
278             }
279              
280             # For debugging only.
281             sub _dbg_file {
282 134 50   134   222 if ( $ENV{DOCCLAIMS_TRACE} ) {
283 0         0 my $letter = shift;
284 0         0 my $file = shift;
285 0         0 my $path = join " ", $file->paths;
286 0         0 print STDERR "$letter ----- $path\n";
287             }
288             }
289              
290             # For debugging only.
291             sub _dbg_line {
292 5263 50   5263   7121 if ( $ENV{DOCCLAIMS_TRACE} ) {
293 0         0 my $letter = shift;
294 0         0 my $action = shift;
295 0         0 my $line = shift;
296 0         0 my $text = $line->text;
297 0 0       0 my $isdoc = $line->is_doc ? "d" : ".";
298 0 0       0 my $iscode = $line->code ? "c" : ".";
299 0         0 print STDERR "$letter:$isdoc$iscode$action '$text'\n";
300             }
301             }
302              
303             # A list of diff routines to handle special cases for lines in a DC_CODE
304             # section. In the future other keys will be added to this hash that match
305             # words at the end of the DC_CODE directive. The ones under the "" key are
306             # tried on every line.
307             our %code_diff = (
308             "" => [
309             sub {
310             my ( $doc, $test ) = @_;
311             if (
312             $doc =~ /
313             ^ \s* (print|say) \s* (.+?) \s* ; \s+ \# \s* (.+?) \s* $
314             /x
315             )
316             {
317             my ( $left, $right ) = ( $2, $3 );
318             $left =~ s/ ^ \( \s* (.*?) \s* \) $ /$1/x; # remove ()
319             $right =~ s/^"(.*)"$/$1/;
320             $right =~ s/^'(.*)'$/$1/;
321             if (
322             $test =~ /^
323             \s* is \s* \(? \s* \Q$left\E \s*
324             , \s* ["']? \Q$right\E \s* ["']? \s*
325             ( , .* )?
326             \s* \)? \s* ;
327             /x
328             )
329             {
330             return 1;
331             }
332             }
333             return 0;
334             },
335             ],
336             );
337              
338             # Given doc and test Test::DocClaims::Line objects, return true if they
339             # match. This takes white space rules, etc. into account.
340             sub _diff {
341 695     695   770 my $doc_line = shift;
342 695         695 my $test_line = shift;
343 695         1012 my $doc = $doc_line->text;
344 695         988 my $test = $test_line->text;
345 695         2916 $doc =~ s/\s+/ /g;
346 695         2292 $test =~ s/\s+/ /g;
347 695         1413 $doc =~ s/\s+$//;
348 695         1218 $test =~ s/\s+$//;
349 695 100       1071 if ( $test_line->code ) {
350 117         241 $doc =~ s/^\s+//;
351 117         223 $test =~ s/^\s+//;
352             }
353 695 100       1524 return 1 if $test eq $doc;
354              
355             # Try special diff routines for DC_CODE sections.
356 10         16 foreach my $subr ( @{ $code_diff{""} } ) {
  10         22  
357 10 100       18 return 1 if $subr->( $doc, $test );
358             }
359              
360 2         3 return 0;
361             }
362              
363             sub _diff_error {
364 2     2   3 my ( $test_line, $doc_line, $name ) = @_;
365 2         2 my @error;
366 2         2 my $prefix = " got";
367 2         3 foreach my $line ( $test_line, $doc_line ) {
368 4 50       6 if ( ref $line ) {
369 4         28 my $text = $line->text;
370 4         11 push @error, "$prefix: '$text'";
371 4         7 push @error, "at " . $line->path . " line " . $line->lnum;
372 4 100       18 ( $error[-1], $error[-2] ) = ( $error[-2], $error[-1] )
373             if $prefix =~ /got/;
374             } else {
375 0         0 push @error, "$prefix: eof";
376             }
377 4         5 $prefix = "expected";
378             }
379 2         4 my $tb = Test::DocClaims->builder;
380 2         14 $tb->diag( map { " $_\n" } @error );
  8         14  
381             }
382              
383             =head2 all_doc_claims [ I [ I ] ]
384              
385             This is the easiest way to test the documentation claims.
386             It automatically searches for documentation and then locates the
387             corresponding test file or files.
388             By default, it searches the lib, bin and scripts directories and their
389             subdirectories for documentation.
390             For each of these files it looks in (by default) the t
391             directory for one or more matching files.
392             It does this with the following patterns, where PATH is the path of the
393             documentation file with the suffix removed (e.g., .pm or .pl) and slashes
394             (/) converted to dashes (-).
395             The patterns are tried in this order until one matches.
396              
397             doc-PATH-[0-9]*.t
398             doc-PATH.t
399             PATH-[0-9]*.t
400             PATH.t
401              
402             If none of the patterns match, the left most directory of the PATH is
403             removed and the patterns are tried again.
404             This is repeated until a match is found or the PATH is exhausted.
405             If the pattern patches multiple files, these files are processed in
406             alphabetical order and their documentation is concatenated to match against
407             the documentation file.
408              
409             If I is missing or undef, its default value of
410             [qw< lib bin scripts >] is used.
411             If I is missing or undef, its default value of
412             [qw< t >] is used.
413              
414             When searching for documentation files, any file with one of these suffixes
415             is used:
416              
417             *.pl
418             *.pm
419             *.pod
420             *.md
421              
422             Also, any file who's first line matches /^#!.*perl/i is used.
423              
424             The number of tests run is determined by the number of documentation files
425             found.
426             Do not set the number of tests before calling all_doc_claims because it
427             will do that automatically.
428              
429             =cut
430              
431             # TODO add option to change suffixes
432             sub all_doc_claims {
433 4     4 1 3335 my $doc_arg = shift;
434 4         7 my $test_arg = shift;
435 4         9 my @docs = _find_docs($doc_arg);
436 4         21 my $tb = Test::DocClaims->builder;
437 4         44 $tb->plan( tests => scalar @docs );
438 4         1112 foreach my $doc_file (@docs) {
439 27 100       7160 my $doc_path = ref $doc_file ? $doc_file->{path} : $doc_file;
440 27         48 my $test_file = _find_tests( $doc_path, $test_arg );
441 27 50       58 if ( length $test_file ) {
442 27         62 doc_claims( $doc_file, $test_file, "doc claims in $doc_path" );
443             } else {
444 0         0 $tb->ok( 0, "doc claims in $doc_path" );
445 0         0 $tb->diag(" no test file(s) found for $doc_path");
446             }
447             }
448             }
449              
450             sub _find_docs {
451 4     4   5 my $dirs = shift;
452 4 100       14 $dirs = [qw< lib bin scripts >] unless defined $dirs;
453 4 50       10 $dirs = [$dirs] unless ref $dirs;
454 4         4 my @files;
455 4         9 foreach my $path ( sort { $a cmp $b } _list_files($dirs) ) {
  59         541  
456 29 100       133 if ( $path =~ m/$doc_file_re/ ) {
    100          
457 24         47 push @files, $path;
458             } elsif ( _read_first_block($path) =~ /^#!.*perl/i ) {
459 3         20 push @files, { path => $path, has_pod => 1 };
460             }
461             }
462 4         15 return @files;
463             }
464              
465             # Given a list of files and/or directories, search them and return a list
466             # of all existing files.
467             sub _list_files {
468 2     2   4 my $dirs = shift;
469 2         2 my @files;
470             find(
471             {
472 16 100   16   796 wanted => sub { push @files, $_ if -f $_; },
473             no_chdir => 1,
474             },
475 2         9 grep { -e $_ } @$dirs
  6         314  
476             );
477 2         14 return @files;
478             }
479              
480             # Return the first block of data from a file. This is used for checking the
481             # first line for #!perl. But, because it reads a fixed amount will not
482             # cause issues if the file is binary.
483             sub _read_first_block {
484 2     2   5 my $path = shift;
485 2         2 my $data = "";
486 2 50       59 if ( open my $fh, "<", $path ) {
487 2         6 binmode $fh;
488 2         38 read( $fh, $data, 4096 );
489 2         22 close $fh;
490             }
491 2         18 return $data;
492             }
493              
494             sub _find_tests {
495 27     27   37 my $path = shift;
496 27         29 my $dirs = shift;
497 27 100       56 $dirs = [qw< t >] unless defined $dirs;
498 27 50       62 $dirs = [$dirs] unless ref $dirs;
499              
500             # Construct a list of file names to look for. If the input path is
501             # "lib/Foo/Bar" then @names becomes "lib-Foo-Bar", "Foo-Bar", "Bar".
502             # One could argue that "lib-Foo-Bar" shouldn't be in the list, but it
503             # shouldn't cause problems and dealing with the general case would
504             # require a complex algorithm.
505 27         100 $path =~ s/\.\w+$//;
506 27         41 my @names;
507 27         29 while (1) {
508 76         99 push @names, map { my $p = $_; $p =~ s{/}{-}g; $p } $path;
  76         90  
  76         145  
  76         143  
509 76 100       231 $path =~ s{^[^/]*/}{} or last;
510             }
511              
512             # Note that the pattern is returned with single quotes ('). This helps
513             # with the case where there is a space in the path. Unfortunately, glob
514             # interprets a space to mean separation of multiple patterns unless the
515             # pattern is quoted.
516 27         40 foreach my $dir (@$dirs) {
517 27         35 foreach my $name (@names) {
518 50         106 foreach my $pat (
519             qw< doc-PATH-[0-9]*.t doc-PATH.t PATH-[0-9]*.t PATH.t >)
520             {
521 163         418 ( my $pattern = $pat ) =~ s/PATH/$name/;
522 163         272 $pattern = "$dir/$pattern";
523 163         268 my @list = _glob($pattern);
524 163 100       21670 return "'$pattern'" if @list;
525             }
526             }
527             }
528 0         0 return "";
529             }
530              
531             # This wrapper for the glob function can be overridden at run time (by the
532             # TestTester module), where the system glob can only be overridden at
533             # compile time.
534             sub _glob {
535 38     38   45 my $pattern = shift;
536 38 100       79 if ( $pattern =~ /[*]/ ) {
537 20         1363 return glob("'$pattern'");
538             } else {
539 18 100       214 return -f $pattern ? ($pattern) : ();
540             }
541             }
542              
543             =head1 SEE ALSO
544              
545             L,
546             L,
547             L.
548             L,
549             L,
550             L,
551             L,
552             L.
553              
554             =head1 AUTHOR
555              
556             Scott E. Lee, EScottLee@cpan.orgE
557              
558             =head1 COPYRIGHT AND LICENSE
559              
560             Copyright (C) 2009-2016 by Scott E. Lee
561              
562             This library is free software; you can redistribute it and/or modify
563             it under the same terms as Perl itself, either Perl version 5.18.2 or,
564             at your option, any later version of Perl 5 you may have available.
565              
566             =cut
567              
568             1;
569