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