package Test::Pod::Snippets; use warnings; use strict; use Carp; use Object::InsideOut; use Test::Pod::Snippets::Parser; use Module::Locate qw/ locate /; use Params::Validate qw/ validate_with validate /; our $VERSION = '0.06'; #<<< my @parser_of :Field :Get(parser); my @do_verbatim :Field :Default(1) :Arg(verbatim) :Get(is_extracting_verbatim) :Set(extracts_verbatim) ; my @do_methods :Field :Default(0) :Arg(methods) :Get(is_extracting_methods) :Set(extracts_methods) ; my @do_functions :Field :Default(0) :Arg(functions) :Get(is_extracting_functions) :Set(extracts_functions) ; my @preserve_lines :Field :Default(1) :Arg(preserve_lines) :Std(preserve_lines) ; #>>> my @object_name :Field :Default('$thingy') :Arg(object_name); sub _init :Init { my $self = shift; $self->init_parser; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sub init_parser { my $self = shift; $parser_of[ $$self ] = Test::Pod::Snippets::Parser->new; $parser_of[ $$self ]->{tps} = $self; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sub get_object_name { my $self = shift; return $object_name[ $$self ]; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sub generate_snippets { my( $self, @files ) = @_; my $i = 1; print "generating snippets\n"; for ( @files ) { my $testfile = sprintf "t/pod-snippets-%02d.t", $i++; print "\t$_ => $testfile\n"; open my $fh, '>', $testfile or die "can't open $testfile for writing: $!\n"; print {$fh} $self->extract_snippets( $_ ); close $fh; } } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sub extract_from_string { my ( $self, $string ) = @_; open my $pod_fh, '<', \$string; return $self->extract_snippets( $pod_fh ); } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sub generate_test { my $self = shift; my %param = validate( @_, { pod => 0, file => 0, fh => 0, module => 0, standalone => 0, testgroup => 0, sanity_tests => { default => 1 }, } ); my @type = grep { $param{$_} } qw/ pod file fh module /; croak "method requires one of those parameters: pod, file, fh, module" unless @type; if ( @type > 1 ) { croak "can only accept one of those parameters: @type"; } my $code = $self->parse( $type[0], $param{ $type[0] } ); if ($param{standalone} or $param{testgroup} ) { $param{sanity_tests} = 1; } if( $param{sanity_tests} ) { no warnings qw/ uninitialized /; $code = <<"END_CODE"; ok 1 => 'the tests compile'; $code ok 1 => 'we reached the end!'; END_CODE } if ( $param{testgroup} ) { my $name = $param{file} ? $param{file} : $param{module} ? $param{module} : 'unknown' ; $code = qq#use Test::Group; # . qq#Test::Group::test "$name" => sub { $code }; #; } my $plan = $param{standalone} ? '"no_plan"' : '' ; return <<"END_CODE"; use Test::More $plan; { no warnings; no strict; # things are likely to be sloppy $code } END_CODE } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sub parse { my ( $self, $type, $input ) = @_; my $output; open my $output_fh, '>', \$output; if ( $type eq 'pod' ) { my $copy = $input; $input = undef; open $input, '<', \$copy; $type = 'fh'; } if ( $type eq 'module' ) { my $location = locate $input or croak "$input not found in \@INC"; $input = $location; $type = 'file'; } $self->init_parser; if ( $type eq 'file' ) { $self->parser->parse_from_file( $input, $output_fh ); } elsif( $type eq 'fh' ) { $self->parser->parse_from_filehandle( $input, $output_fh ); } else { die "type $type unknown"; } return $output; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~, sub extract_snippets { my( $self, $file ) = @_; my $filename_call = 'GLOB' ne ref $file; if( $filename_call and not -f $file ) { croak "$file doesn't seem to exist"; } my $output; open my $fh, '>', \$output; if ( $filename_call ) { $parser_of[ $$self ]->parse_from_file( $file, $fh ); } else { $parser_of[ $$self ]->parse_from_filehandle( $file, $fh ); } my $filename = $filename_call ? $file : 'unknown'; return <<"END_TESTS"; use Test::More qw/ no_plan /; no warnings; no strict; # things are likely to be sloppy ok 1 => 'the tests compile'; $output ok 1 => 'we reached the end!'; END_TESTS } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sub runtest { my ( $self, @args ) = @_; my $code = $self->generate_test( @args ); eval $code; if ( $@ ) { croak "couldn't compile test: $@"; } } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sub snippets_ok { my( $self, $file ) = @_; my $code = $self->extract_snippets( $file ); eval $code; warn $@ if $@; return not $@; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sub generate_test_file { my $self = shift; my %param = validate_with( params => \@_, spec => { output => 0 }, allow_extra => 1, ); unless( $param{output} ) { my $i; my $name; do { $i++; $name = sprintf "tps-%04d.t", $i } while -f $name; $param{output} = $name; } my $filename = $param{output}; croak "file '$filename' already exists" if -f $filename; open my $fh, '>', $filename or croak "can't create file '$filename': $!"; delete $param{output}; print {$fh} $self->generate_test( %param ); return $filename; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1; # End of Test::Pod::Snippets __END__ =head1 NAME Test::Pod::Snippets - Generate tests from pod code snippets =head1 SYNOPSIS use Test::More tests => 3; use Test::Pod::Snippets; my $tps = Test::Pod::Snippets->new; my @modules = qw/ Foo Foo::Bar Foo::Baz /; $tps->runtest( module => $_, testgroup => 1 ) for @modules; =head1 DESCRIPTION =over =item Fact 1 In a perfect world, a module's full API should be covered by an extensive battery of testcases neatly tucked in the distribution's C directory. But then, in a perfect world each backyard would have a marshmallow tree and postmen would consider their duty to circle all the real good deals in pamphlets before stuffing them in your mailbox. Obviously, we're not living in a perfect world. =item Fact 2 Typos and minor errors in module documentation. Let's face it: it happens to everyone. And while it's never the end of the world and is prone to rectify itself in time, it's always kind of embarassing. A little bit like electronic zits on prepubescent docs, if you will. =back Test::Pod::Snippets's goal is to address those issues. Quite simply, it extracts verbatim text off pod documents -- which it assumes to be code snippets -- and generate test files out of them. =head1 HOW TO USE TEST::POD::SNIPPETS IN YOUR DISTRIBUTION The easiest way is to create a test.t file calling Test::Pod::Snippets as shown in the synopsis. If, however, you don't want to add T:P:S to your module's dependencies, you can add the following to your Build.PL: =for test ignore my $builder = Module::Build->new( # ... your M::B parameters PL_files => { 'script/test-pod-snippets.PL' => q{} }, add_to_cleanup => [ 't/tps-*.t' ], ); Then create the file F