File Coverage

blib/lib/Test/Pod/Snippets.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1             package Test::Pod::Snippets;
2             BEGIN {
3 1     1   46154 $Test::Pod::Snippets::AUTHORITY = 'cpan:YANICK';
4             }
5             {
6             $Test::Pod::Snippets::VERSION = '0.07';
7             }
8             # ABSTRACT: Generate tests from pod code snippets
9              
10 1     1   12 use warnings;
  1         2  
  1         32  
11 1     1   6 use strict;
  1         2  
  1         126  
12 1     1   6 use Carp;
  1         3  
  1         94  
13              
14 1     1   483 use Moose;
  0            
  0            
15             use MooseX::SemiAffordanceAccessor;
16              
17             use Module::Locate qw/ locate /;
18             use Params::Validate qw/ validate_with validate /;
19              
20             has parser => (
21             is => 'ro',
22             default => sub {
23             my $tps = Test::Pod::Snippets::Parser->new;
24             $tps->{tps} = shift;
25             return $tps;
26             },
27             );
28              
29             has verbatim => (
30             reader => 'is_extracting_verbatim',
31             writer => 'extracts_verbatim',
32             default => 1,
33             );
34              
35             has methods => (
36             reader => 'is_extracting_methods',
37             writer => 'extracts_methods',
38             default => 0,
39             );
40              
41             has functions => (
42             reader => 'is_extracting_functions',
43             writer => 'extracts_functions',
44             default => 0,
45             );
46              
47             has preserve_lines => (
48             is => 'rw',
49             default => 1,
50             );
51              
52             has object_name => (
53             is => 'ro',
54             default => '$thingy',
55             );
56              
57             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
58              
59              
60             sub generate_snippets {
61             my( $self, @files ) = @_;
62             my $i = 1;
63              
64             print "generating snippets\n";
65              
66             for ( @files ) {
67             my $testfile = sprintf "t/pod-snippets-%02d.t", $i++;
68             print "\t$_ => $testfile\n";
69            
70             open my $fh, '>', $testfile
71             or die "can't open $testfile for writing: $!\n";
72             print {$fh} $self->extract_snippets( $_ );
73             close $fh;
74             }
75             }
76              
77             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
78              
79             sub generate_test {
80             my $self = shift;
81              
82             my %param = validate( @_, {
83             pod => 0,
84             file => 0,
85             fh => 0,
86             module => 0,
87             standalone => 0,
88             testgroup => 0,
89             sanity_tests => { default => 1 },
90             } );
91              
92             my @type = grep { $param{$_} } qw/ pod file fh module /;
93              
94             croak "method requires one of those parameters: pod, file, fh, module"
95             unless @type;
96              
97             if ( @type > 1 ) {
98             croak "can only accept one of those parameters: @type";
99             }
100              
101             my $code = $self->_parse( $type[0], $param{ $type[0] } );
102              
103             if ($param{standalone} or $param{testgroup} ) {
104             $param{sanity_tests} = 1;
105             }
106              
107             if( $param{sanity_tests} ) {
108             no warnings qw/ uninitialized /;
109             $code = <<"END_CODE";
110             ok 1 => 'the tests compile';
111              
112             $code
113              
114             ok 1 => 'we reached the end!';
115             END_CODE
116             }
117              
118             if ( $param{testgroup} ) {
119             my $name = $param{file} ? $param{file}
120             : $param{module} ? $param{module}
121             : 'unknown'
122             ;
123             $code = qq#use Test::Group; #
124             . qq#Test::Group::test "$name" => sub { $code }; #;
125             }
126              
127             my $plan = $param{standalone} ? '"no_plan"' : '' ;
128              
129             return <<"END_CODE";
130             use Test::More $plan;
131             {
132             no warnings;
133             no strict; # things are likely to be sloppy
134              
135             $code
136             }
137             END_CODE
138              
139             }
140              
141             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
142              
143              
144             sub _parse {
145             my ( $self, $type, $input ) = @_;
146              
147             my $output;
148             open my $output_fh, '>', \$output;
149              
150             if ( $type eq 'pod' ) {
151             my $copy = $input;
152             $input = undef;
153             open $input, '<', \$copy;
154             $type = 'fh';
155             }
156              
157             if ( $type eq 'module' ) {
158             my $location = locate $input
159             or croak "$input not found in \@INC";
160             $input = $location;
161             $type = 'file';
162             }
163              
164             if ( $type eq 'file' ) {
165             $self->parser->parse_from_file( $input, $output_fh );
166             }
167             elsif( $type eq 'fh' ) {
168             $self->parser->parse_from_filehandle( $input, $output_fh );
169             }
170             else {
171             die "type $type unknown";
172             }
173              
174             return $output;
175             }
176              
177              
178             sub extract_snippets_from_file {
179             my( $self, $file ) = @_;
180              
181             if( not -f $file ) {
182             croak "$file doesn't seem to exist";
183             }
184              
185             my $output;
186             open my $fh, '>', \$output;
187              
188             $self->parser->parse_from_file( $file, $fh );
189              
190             return $self->_extract($output);
191             }
192              
193              
194             sub extract_snippets {
195             my( $self, $pod ) = @_;
196              
197             open my $file, '<', \$pod;
198              
199             my $output;
200             open my $fh, '>', \$output;
201              
202             $self->parser->parse_from_filehandle( $file, $fh );
203              
204             return $self->_extract($output);
205             }
206              
207             sub _extract {
208             my( $self, $output ) = @_;
209              
210             return <<"END_TESTS";
211             use Test::More qw/ no_plan /;
212              
213             no warnings;
214             no strict; # things are likely to be sloppy
215              
216             ok 1 => 'the tests compile';
217              
218             $output
219              
220             ok 1 => 'we reached the end!';
221              
222             END_TESTS
223              
224             }
225              
226             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
227              
228             sub runtest {
229             my ( $self, @args ) = @_;
230              
231             my $code = $self->generate_test( @args );
232              
233             eval $code;
234              
235             if ( $@ ) {
236             croak "couldn't compile test: $@";
237             }
238             }
239              
240             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
241              
242              
243             sub snippets_ok {
244             my( $self, $file ) = @_;
245              
246             my $code = $self->extract_snippets( $file );
247              
248             eval $code;
249              
250             warn $@ if $@;
251              
252             return not $@;
253             }
254              
255             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
256              
257             sub generate_test_file {
258             my $self = shift;
259              
260             my %param = validate_with( params => \@_,
261             spec => { output => 0 },
262             allow_extra => 1,
263             );
264              
265             unless( $param{output} ) {
266             my $i;
267             my $name;
268             do {
269             $i++;
270             $name = sprintf 'tps-%04d.t', $i
271             } while -f $name;
272              
273             $param{output} = $name;
274             }
275              
276             my $filename = $param{output};
277              
278             croak "file '$filename' already exists" if -f $filename;
279              
280             open my $fh, '>', $filename
281             or croak "can't create file '$filename': $!";
282              
283             delete $param{output};
284              
285             print {$fh} $self->generate_test( %param );
286              
287             return $filename;
288             }
289              
290             1;
291              
292             package # hide from PAUSE
293             Test::Pod::Snippets::Parser;
294              
295             use strict;
296             use warnings;
297              
298             no warnings 'redefine';
299              
300             use parent qw/ Pod::Parser /;
301              
302             sub initialize {
303             $_[0]->SUPER::initialize;
304             $_[0]->{$_} = 0 for qw/ tps_ignore tps_ignore_all tps_within_begin_test /;
305             $_[0]->{tps_method_level} = 0;
306             $_[0]->{tps_function_level} = 0;
307             }
308              
309             sub command {
310             my ($parser, $command, $paragraph, $line_nbr ) = @_;
311              
312             if ( $command eq 'for' ) {
313             my( $target, $directive, $rest ) = split ' ', $paragraph, 3;
314              
315             return unless $target eq 'test';
316              
317             return $parser->{tps_ignore} = 1 if $directive eq 'ignore';
318             return $parser->{tps_ignore_all} = 1 if $directive eq 'ignore_all';
319              
320             $parser->{tps_ignore} = 0;
321             no warnings qw/ uninitialized /;
322             print {$parser->output_handle} join ' ', $directive, $rest;
323             }
324             elsif( $command eq 'begin' ) {
325             my( $target, $rest ) = split ' ', $paragraph, 2;
326             return unless $target eq 'test';
327             $parser->{tps_within_begin_test} = 1;
328             print {$parser->output_handle} $rest;
329             }
330             elsif( $command eq 'end' ) {
331             my( $target ) = split ' ', $paragraph, 2;
332             return unless $target eq 'test';
333              
334             $parser->{tps_within_begin_test} = 0;
335             }
336             elsif( $command =~ /^head(\d+)/ ) {
337              
338             return unless $parser->{tps}->is_extracting_functions
339             or $parser->{tps}->is_extracting_methods;
340              
341             my $level = $1;
342              
343             for my $type ( qw/ tps_method_level tps_function_level / ) {
344             if ( $level <= $parser->{$type} ) {
345             $parser->{$type} = 0;
346             }
347             }
348              
349             if ( $paragraph =~ /^\s*METHODS\s*$/ ) {
350             $parser->{tps_method_level} =
351             $parser->{tps}->is_extracting_methods && $level;
352             return;
353             }
354              
355             if ( $paragraph =~ /^\s*FUNCTIONS\s*$/ ) {
356             $parser->{tps_function_level} =
357             $parser->{tps}->is_extracting_functions && $level;
358             return;
359             }
360              
361             return if $parser->{tps_ignore} or $parser->{tps_ignore_all};
362              
363             my $master_level = $parser->{tps_method_level}
364             || $parser->{tps_function_level}
365             || return ;
366              
367             # functions and methods are deeper than
368             # their main header
369             return unless $level > $master_level;
370              
371             $paragraph =~ s/[IBC]<(.*?)>/$1/g; # remove markups
372              
373             $paragraph =~ s/^\s+//;
374             $paragraph =~ s/\s+$//;
375              
376             if ( $parser->{tps_method_level} ) {
377             if ( $paragraph =~ /^new/ ) {
378             print {$parser->output_handle}
379             $parser->{tps}->get_object_name,
380             ' = $class->', $paragraph, ";\n";
381             return;
382             }
383             else {
384             $paragraph = $parser->{tps}->object_name.'->'.$paragraph;
385             }
386             }
387              
388             my $line_ref;
389             $line_ref = "\n#line $line_nbr " . ( $parser->input_file || 'unknown')
390             . "\n"
391             if $parser->{tps}->preserve_lines;
392              
393             print {$parser->output_handle}
394             $line_ref,
395             '@result = ', $paragraph, ";\n";
396             }
397             }
398              
399             sub textblock {
400             return unless $_[0]->{tps_within_begin_test};
401              
402             print_paragraph( @_ );
403             }
404              
405             sub interior_sequence {}
406              
407             sub verbatim {
408             my $self = shift;
409              
410             return unless $self->{tps}->is_extracting_verbatim;
411              
412             return if ( $self->{tps_ignore} or $self->{tps_ignore_all} )
413             and not $self->{tps_within_begin_test};
414              
415             print_paragraph( $self, @_ );
416             }
417              
418             sub print_paragraph {
419             my ( $parser, $paragraph, $line_no ) = @_;
420              
421             $DB::single = 1;
422             my $filename = $parser->input_file || 'unknown';
423              
424             # remove the indent
425             $paragraph =~ /^(\s*)/;
426             my $indent = $1;
427             $paragraph =~ s/^$indent//mg;
428             $paragraph = "\n#line $line_no $filename\n".$paragraph
429             if $parser->{tps}->preserve_lines;
430              
431             $paragraph .= ";\n";
432              
433             print {$parser->output_handle} $paragraph;
434             }
435              
436              
437             'end of Test::Pod::Snippets::Parser';
438              
439              
440             =pod
441              
442             =head1 NAME
443              
444             Test::Pod::Snippets - Generate tests from pod code snippets
445              
446             =head1 VERSION
447              
448             version 0.07
449              
450             =head1 SYNOPSIS
451              
452             use Test::More tests => 3;
453              
454             use Test::Pod::Snippets;
455              
456             my $tps = Test::Pod::Snippets->new;
457              
458             my @modules = qw/ Foo Foo::Bar Foo::Baz /;
459              
460             $tps->runtest( module => $_, testgroup => 1 ) for @modules;
461              
462             =head1 DESCRIPTION
463              
464             =over
465              
466             =item Fact 1
467              
468             In a perfect world, a module's full API should be covered by an extensive
469             battery of testcases neatly tucked in the distribution's C<t/> directory.
470             But then, in a perfect world each backyard would have a marshmallow tree and
471             postmen would consider their duty to circle all the real good deals in pamphlets
472             before stuffing them in your mailbox. Obviously, we're not living in a perfect
473             world.
474              
475             =item Fact 2
476              
477             Typos and minor errors in module documentation. Let's face it: it happens to everyone.
478             And while it's never the end of the world and is prone to rectify itself in
479             time, it's always kind of embarassing. A little bit like electronic zits on
480             prepubescent docs, if you will.
481              
482             =back
483              
484             Test::Pod::Snippets's goal is to address those issues. Quite simply,
485             it extracts verbatim text off pod documents -- which it assumes to be
486             code snippets -- and generate test files out of them.
487              
488             =head1 METHODS
489              
490             =head2 new( %options )
491              
492             Creates a new B<Test::Pod::Snippets> object. The method accepts
493             the following options:
494              
495             =over
496              
497             =item verbatim => I<$boolean>
498              
499             If set to true, incorporates the pod's verbatim parts to the test.
500              
501             Set to true by default.
502              
503             =item functions => I<$boolean>
504              
505             If set to true, extracts function definitions from the pod.
506             More specifically, Test::Pod::Snippets looks for a pod section
507             called FUNCTIONS, and assumes the title of all its
508             subsections to be functions.
509              
510             For example, the pod
511              
512             =head1 FUNCTIONS
513              
514             =head2 play_song( I<$artist>, I<$song_title> )
515              
516             Play $song_title from $artist.
517              
518             =head2 set_lighting( I<$intensity> )
519              
520             Set the room's light intensity (0 is pitch black
521             and 1 is supernova white, -1 triggers the stroboscope).
522              
523             would generate the code
524              
525             @result = play_song( $artist, $song_title );
526             @result = set_lightning( $intensity );
527              
528             Pod markups are automatically stripped from the headers.
529              
530             =item methods => I<$boolean>
531              
532             Same as C<functions>, but with methods. In this
533             case, Test::Pod::Snippets looks for a pod section called METHODS.
534             The object used for the tests is assumed to be '$thingy'
535             (but can be overriden using the argument C<object_name>,
536             and its class must be given by a variable '$class'.
537              
538             For example, the pod
539              
540             =head1 METHODS
541              
542             =for test
543             $class = 'Amphibian::Frog';
544              
545             =head2 new( $name )
546              
547             Create a new froggy!
548              
549             =head2 jump( $how_far )
550              
551             Make it jumps.
552              
553             will produces
554              
555             $class = 'Amphibian::Frog';
556             $thingy = $class->new( $name );
557             @result = $thingy->jump( $how_far );
558              
559             =item object_name => I<$identifier>
560              
561             The name of the object (with the leading '$') to be
562             used for the methods if the T:P:S object is set to
563             extract methods.
564              
565             =item preserve_lines => I<$boolean>
566              
567             If sets to true (which is the default), the generated code
568             will be peppered with '#line' pre-compiler lines that will
569             have any failing test point to the test's original file.
570              
571             =back
572              
573             =head2 is_extracting_verbatim
574              
575             =head2 is_extracting_functions
576              
577             =head2 is_extracting_methods
578              
579             Returns true if the object is configured to
580             extract that part of the pod, false otherwise.
581              
582             =head2 extracts_verbatim( I<$boolean> )
583              
584             =head2 extracts_functions( I<$boolean> )
585              
586             =head2 extracts_methods( I<$boolean> )
587              
588             Configure the object to extract (or not) the given
589             pod parts.
590              
591             =head2 generate_test( $input_type => I<$input>, %options )
592              
593             Extracts the pod off I<$input> and generate tests out of it.
594             I<$input_type> can be 'file' (a filename),
595             'fh' (a filehandler), 'pod' (a string containing pod) or
596             'module' (a module name).
597              
598             The method returns the generate tests as a string.
599              
600             The method accepts the following options:
601              
602             =over
603              
604             =item standalone => I<$boolean>
605              
606             If standalone is true, the generated
607             code will be a self-sufficient test script.
608             Defaults to 'false'.
609              
610             # create a test script out of the module Foo::Bar
611             open my $test_fh, '>', 't/foo-bar.t' or die;
612             print {$test_fh} $tps->generate_test(
613             module => 'Foo::Bar',
614             standalone => 1 ,
615             );
616              
617             =item sanity_tests => I<$boolean>
618              
619             If true (which is the default), two tests are added to the
620             very beginning and end of the extracted code, like so:
621              
622             ok 1 => 'the tests compile';
623             $extracted_code
624             ok 1 => 'we reached the end!';
625              
626             =item testgroup => I<$boolean>
627              
628             If true, the extracted code will be wrapped in a L<Test::Group>
629             test, which will report a single 'ok' for the whole series of test
630             (but will give more details if something goes wrong). Is set
631             to 'false' by default.
632              
633             =back
634              
635             =head2 generate_test_file( $input_type => I<$input>, %options )
636              
637             Does the same as C<generate_test>, but save the generated
638             code in a file. The name of the file is the value of the
639             option B<output>, if given. If the file already exist,
640             the method dies. If B<output> is not given,
641             the filename will be
642             of the format 'tps-XXXX.t', where XXXX is choosen not to
643             interfere with existing tests. Exception made of C<output>,
644             the options accepted by the method are the same than for
645             C<generate_test>.
646              
647             Returns the name of the created file.
648              
649             =head2 runtest( $input_type => I<$input>, %options )
650              
651             Does the same than C<generate_test>, except that it
652             executes the generated code rather than return it.
653             The arguments are treated the same as for C<generate_test>.
654              
655             =head2 generate_snippets( @filenames )
656              
657             For each file in I<@filenames>, generates a I<pod-snippets-X.t>
658             file in the C<t/> directory.
659              
660             =head2 extract_snippets_from_file( $filename )
661              
662             Extracts the snippets from the file and returns a string containing
663             the generated tests.
664              
665             =head2 extract_snippets( $pod )
666              
667             Extracts the snippets from the string I<$pod> and
668             returns a string containing the generated tests.
669              
670             =head2 snippets_ok( $pod )
671              
672             Extracts the snippets from I<$pod> (which can be a string or a filename) and
673             run the code, returning b<true> if the code run and b<false> if it fails.
674              
675             =head1 HOW TO USE TEST::POD::SNIPPETS IN YOUR DISTRIBUTION
676              
677             The easiest way is to create a test.t file calling Test::Pod::Snippets
678             as shown in the synopsis. If, however, you don't want to
679             add T:P:S to your module's dependencies, you can
680             add the following to your Build.PL:
681              
682             my $builder = Module::Build->new(
683             # ... your M::B parameters
684             PL_files => { 'script/test-pod-snippets.PL' => q{} },
685             add_to_cleanup => [ 't/tps-*.t' ],
686             );
687              
688             Then create the file F<script/test-pod-snippets.PL>, which should contains
689              
690             use Test::Pod::Snippets;
691              
692             my $tps = Test::Pod::Snippets->new;
693              
694             my @files = qw#
695             lib/your/module.pm
696             lib/your/documentation.pod
697             #;
698            
699             print "generating tps tests...\n";
700             print $tps->generate_test_file( $_ ), "created\n" for @files;
701             print "done\n";
702              
703             And you're set! Running B<Build> should now generate one test file
704             for each given file.
705              
706             =head1 SYNTAX
707              
708             By default, Test::Pod::Snippets considers all verbatim pod text to be
709             code snippets. To tell T::P::S to ignore subsequent pieces of verbatim text,
710             add a C<=for test ignore> to the pod. Likely, to return to the normal behavior,
711             insert C<=for test>. For example:
712              
713             A sure way to make your script die is to do:
714              
715             =for test ignore
716              
717             $y = 0; $x = 1/$y;
718              
719             The right (or safe) way to do it is rather:
720              
721             =for test
722              
723             $y = 0; $x = eval { 1/$y };
724             warn $@ if $@;
725              
726             C<=for test> and C<=begin test ... =end test> can also be used to
727             add code that should be include in the tests but not in the documentation.
728              
729             Example:
730              
731             The right way to do it is:
732              
733             $y = 0; $x = eval { 1/$y };
734              
735             =for test
736             # make sure an error happened
737             is $x => undef;
738             ok length($@), 'error is reported';
739              
740             =head1 SEE ALSO
741              
742             L<podsnippets>
743              
744             =head2 Test::Inline
745              
746             Whereas L<Test::Pod::Snippets> extracts
747             tests out of the modules' documentation, Adam Kennedy's I<Test::Inline>
748             allows to insert tests within a module, side-by-side with its code
749             and documentation.
750              
751             For example, the following code using I<Test::Pod::Snippets>
752              
753             =head2 shout()
754              
755             Shoutify the passed string.
756              
757             # set $x to 'CAN YOU HEAR ME NOW?'
758             my $x = shout( 'can you hear me now?' );
759              
760             =for test
761             is $x => 'CAN YOU HEAR ME NOW?';
762              
763             is equivalent to this code, using I<Test::Inline>:
764              
765             =head2 shout()
766              
767             Shoutify the passed string.
768              
769             # set $x to 'CAN YOU HEAR ME NOW?'
770             my $x = shout( 'can you hear me now?' );
771              
772             =begin testing
773             my $x = shout( 'can you hear me now?' );
774             is $x => 'CAN YOU HEAR ME NOW?';
775             =end testing
776              
777             =head1 AUTHOR
778              
779             Yanick Champoux <yanick@cpan.org>
780              
781             =head1 COPYRIGHT AND LICENSE
782              
783             This software is copyright (c) 2006 by Yanick Champoux.
784              
785             This is free software; you can redistribute it and/or modify it under
786             the same terms as the Perl 5 programming language system itself.
787              
788             =cut
789              
790              
791             __END__
792              
793