For example, there is Test::Pod::Snippets which is really close, but considers all verbatim sections to be code (by default).
There is also Test::Inline, which allows tests to be in pod sections alongside code as well, but requires explicit testing of results.
Lastly, I found Test::Snippet, which does have a REPL loop but still wasn't quite as lightweight as I wanted.
All of the modules above required some additional non-core dependencies too, which I find irksome. So, below is my crack at it.
package Test::Doctest;
use 5.005;
use strict;
require Exporter;
require Pod::Parser;
use vars qw(@ISA @EXPORT $VERSION);
@ISA = qw(Exporter Pod::Parser);
@EXPORT = qw(runtests);
$VERSION = '0.01';
use Carp;
use Test::Builder;
use File::Spec::Functions qw(devnull);
=head1 NAME
Test::Doctest - extract and evaluate tests from pod fragments
=head1 SYNOPSIS
perl -MTest::Doctest -e 'runtests @ARGV' lib/Some/Module.pm
- or -
use Test::Doctest;
runtests($filepath);
- or -
use Test::Doctest;
my $p = Test::Doctest->new;
$p->parse_from_filehandle(\*STDIN);
$p->test;
=head1 DESCRIPTION
B<runtests> uses B<Pod::Parser> to extract pod text from the files
specified, evaluates each line begining with a prompt ($ by default),
and finally compares the results with the expected output using
B<is_eq> from B<Test::Builder>.
=head1 EXAMPLES
$ 1 + 1
2
$ my @a = qw(2 3 4)
3
$ use Pod::Parser;
$ my $p = Pod::Parser->new;
$ ref $p;
Pod::Parser
=head1 EXPORTS
=head2 B<runtests()>
Extract and run tests from pod for each file argument.
=begin runtests
$ use Test::Doctest
$ runtests
0
=end
=cut
sub runtests {
my ($total, $success, @tests) = (0, 0);
my $test = Test::Builder->new;
for (@_) {
my $t = Test::Doctest->new;
$t->parse_from_file($_, devnull);
$total += @{$t->{tests}};
push @tests, $t;
}
if (!$test->has_plan) {
$test->plan(tests => $total);
}
for (@tests) {
$success += $_->test == @{$_->{tests}}
}
return $success;
}
=head1 METHODS
=head2 B<initialize()>
Initialize this B<Test::Doctest> pod parser. This method is
not typically called directly, but rather, is called by
B<Pod::Parser::new> when creating a new parser.
=begin initialize
$ use Test::Doctest
$ my $t = Test::Doctest->new
$ @{$t->{tests}}
0
=end
=begin custom prompt
$ use Test::Doctest
$ my $t = Test::Doctest->new(prompt => 'abc')
$ $t->{prompt}
abc
=end
=cut
sub initialize {
my ($self) = @_;
$self->SUPER::initialize;
$self->{tests} = [];
}
=head2 B<command()>
Override B<Pod::Parser::command> to save the name of the
current section which is used to name the tests.
=begin command
$ use Test::Doctest
$ my $t = Test::Doctest->new
$ $t->command('head1', "EXAMPLES\nthese are examples", 1)
$ $t->{name}
EXAMPLES
=end
=cut
sub command {
my ($self, $cmd, $par, $line) = @_;
$self->{name} = (split /(?:\r|\n|\r\n)/, $par, 2)[0];
}
=head2 B<textblock()>
Override B<Pod::Parser::textblock> to ignore normal blocks of pod text.
=begin textblock
$ use Test::Doctest
$ my $t = Test::Doctest->new
$ not defined $t->textblock
1
=end
=cut
sub textblock { }
=head2 B<verbatim()>
Override B<Pod::Parser::verbatim> to search verbatim paragraphs for
doctest code blocks. Each block found, along with information about
its location in the file and its expected output is appended to the
list of tests to be executed.
=begin verbatim
$ use Test::Doctest
$ my $t = Test::Doctest->new
$ $t->verbatim(" \$ 1+1\n 2", 1)
1
=end
=begin verbatim no prompt
$ use Test::Doctest
$ my $t = Test::Doctest->new
$ $t->verbatim("abc", 1)
0
=end
=begin verbatim custom prompt
$ use Test::Doctest
$ my $t = Test::Doctest->new(prompt => '#\s+')
$ $t->verbatim(" # 1+1\n 2", 1)
1
=end
=cut
sub verbatim {
my ($self, $par, $line) = @_;
my $prompt = $self->{prompt} ? $self->{prompt} : '\$\s+';
my $name = $self->{name} ? $self->{name} : q{};
my @lines = split /(?:\r|\n|\r\n)/, $par;
my @code;
for (@lines) {
if (/^\s+$prompt(.+)/) {
# capture code
push @code, $1;
} elsif (/^\s+(.+)/ and @code) {
# on first non-code line, with valid code accumlated
my $file = $self->input_file ? $self->input_file : 'stdin';
push @{$self->{tests}}, [$name, $file, $line, $1, @code];
@code = ();
} elsif (/^=cut/) {
# stop processing on =cut (even without a leading blank line)
last;
}
}
return @{$self->{tests}};
}
=head2 B<test()>
Evaluates each test discovered via parsing and compares the results
with the expected output using B<Test::Builder::is_eq>.
=begin test empty
$ use Test::Doctest
$ my $t = Test::Doctest->new
$ $t->test
0
=end
=begin test non-empty
$ use Test::Doctest
$ my $t = Test::Doctest->new
$ $t->command('begin', 'test', 1)
$ $t->verbatim(" \$ 1+1\n 2", 2)
$ @{$t->{tests}}
1
=end
=cut
sub test {
my ($self) = @_;
my @tests = @{$self->{tests}};
my $run = 0;
my $test = Test::Builder->new;
if (!$test->has_plan) {
$test->plan(tests => scalar @tests);
}
for (@{$self->{tests}}) {
local $" = ';';
my ($name, $file, $line, $expect, @code) = @{$_};
my $result = eval "sub { @code }->()";
if ($@) {
croak $@;
}
$test->is_eq($result, $expect, "$name ($file, $line)");
$run++;
}
return $run;
}
1;
__END__
=head1 HISTORY
=over 8
=item 0.01
Original version
=back
=head1 SEE ALSO
L<Pod::Parser>, L<Test::Builder>
B<Pod::Parser> defines the parser interface used to extract the tests.
B<Test::Builder> is used to plan the tests and determine the results.
=head1 AUTHOR
Bryan Cardillo E<lt>dillo@cpan.org<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2009 by Bryan Cardillo
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut