The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Test::Base::SubTest;
use strict;
use warnings;
use utf8;
use parent qw(Exporter);
our @EXPORT = (@Test::More::EXPORT, qw/filters blocks register_filter run run_is run_is_deeply/);

our $VERSION = '0.5';

use parent qw/
    Test::Base::Less
    Test::Builder::Module Exporter
/;
use Test::More;
use Carp qw/croak/;
use Text::TestBase::SubTest;

my $SKIP;

sub blocks() { croak 'block() is not supported. Use run{} instead.' }

{
    no warnings 'once';
    *filters         = \&Test::Base::Less::filters;
    *register_filter = \&Test::Base::Less::register_filter;
}

sub run(&) {
    my $code = shift;

    my $content = _get_data_section();
    my $node = Text::TestBase::SubTest->new->parse($content);

    _exec_each_test($node, sub {
        my $block = shift;
        $code->($block);
    });
}

sub run_is {
    my ($a, $b) = @_;
    $a ||= 'input';
    $b ||= 'expected';
    my $content = _get_data_section();
    my $node = Text::TestBase::SubTest->new->parse($content);

    _exec_each_test($node, sub {
        my $block = shift;
        __PACKAGE__->builder->is_eq(
            $block->get_section($a),
            $block->get_section($b),
            $block->name || 'L: ' . $block->get_lineno
        );
    });
}

sub run_is_deeply($$) {
    my ($a, $b) = @_;
    $a ||= 'input';
    $b ||= 'expected';
    my $package = scalar(caller(0));

    my $content = _get_data_section();
    my $node = Text::TestBase::SubTest->new->parse($content);

    _exec_each_test($node, sub {
        my $block = shift;
        local $Test::Builder::Level = $Test::Builder::Level + 1;
        Test::More::is_deeply(
            $block->get_section($a),
            $block->get_section($b),
            $block->name || 'L: ' . $block->get_lineno
        );
    });
}

sub _exec_each_test {
    my ($subtest, $code) = @_;

    my $executer = sub {
        for my $node (@{ $subtest->child_nodes }) {
            if ($node->is_subtest) {
                _exec_each_test($node, $code);
            } else {
                next if $SKIP;

                my @names = $node->get_section_names;
                for my $section_name (@names) {
                    my @data = $node->get_section($section_name);
                    if (my $filter_names = $Test::Base::Less::FILTER_MAP{$section_name}) {
                        for my $filter_stuff (@$filter_names) {
                            if (ref $filter_stuff eq 'CODE') { # filters { input => [\&code] };
                                @data = $filter_stuff->(@data);
                            } else { # filters { input => [qw/eval/] };
                                my $filter = $Test::Base::Less::FILTERS{$filter_stuff};
                                unless ($filter) {
                                    Carp::croak "Unknown filter name: $filter_stuff";
                                }
                                @data = $filter->(@data);
                            }
                        }
                    }
                    $node->set_section($section_name => @data);
                }
                if ($node->has_section('ONLY')) {
                    Carp::croak "Sorry, section 'ONLY' is not implemented... Patches welcome.";
                    __PACKAGE__->builder->diag("I found ONLY: maybe you're debugging?");
                    $SKIP = 1;
                    $code->($node);
                    next;
                }
                if ($node->has_section('SKIP')) {
                    next;
                }
                if ($node->has_section('LAST')) {
                    $SKIP = 1;
                    $code->($node);
                    next;
                }

                $code->($node);
            }
        }
    };
    if ($subtest->is_root) {
        $executer->();
    } else {
        return if $SKIP;
        __PACKAGE__->builder->subtest(
            ($subtest->name || 'L: ' . $subtest->get_lineno) => $executer
        );
    }
}

sub _get_data_section {
    my $package = scalar(caller(1));
    my $d = do { no strict 'refs'; \*{"${package}::DATA"} };
    unless (defined fileno $d) {
        Carp::croak("Missing __DATA__ section in $package.");
    }
    seek $d, 0, 0;

    return join '', <$d>;
}

1;
__END__

=head1 NAME

Test::Base::SubTest - Enables Test::Base to use subtest

=head1 SYNOPSIS

    use Test::Base::SubTest;

    filters { input => [qw/eval/] };
    run {
        my $block = shift;
        is $block->input, $block->expected, $block->name;
    };
    done_testing;

    __DATA__

    ### subtest 1
        === test 1-1
        --- input:    4*2
        --- expected: 8

        === test 1-2
        --- input :   3*3
        --- expected: 9

    ### subtest 2
        === test 2-1
        --- input:    4*3
        --- expected: 12

=begin html

<div><img src="http://cdn-ak.f.st-hatena.com/images/fotolife/C/Cside/20140116/20140116204246.png?1389872580"></div>

=end html

=head1 DESCRIPTION

Test::Base::SubTest is a extension of L<Test::Base::Less>.

"### TEST NAME" is a delimiter of a subtest. Indentaion is necessary.

=head1 FUNCTIONS

This module exports all Test::More's exportable functions, and following functions:

=over 4

=item filters(+{ } : HashRef);

    filters {
        input => [qw/eval/],
    };

Set a filter for the section name.

=item run(\&subroutine)

    run {
        my $block = shift;
        is $block->input, $block->expected, $block->name;
    };

Calls the sub for each block. It passes the current block object to the subroutine.

=item run_is([data_name1, data_name2])

    run_is input => 'expected';

=item run_is_deeply([data_name1, data_name2])

=item register_filter($name: Str, $code: CodeRef)

Register a filter for $name using $code.

=back

=head1 DEFAULT FILTERS

This module provides only few filters. If you want to add more filters, pull-reqs welcome.
(I only merge a patch using no depended modules)

=over 4

=item eval

eval() the code.

=item chomp

C<chomp()> the arguments.

=item uc

C<uc()> the arguments.

=item trim

Remove extra blank lines from the beginning and end of the data. This
allows you to visually separate your test data with blank lines.

=back

=head1 REGISTER YOUR OWN FILTER

You can register your own filter by following form:

    use Digest::MD5 qw/md5_hex/;
    Test::Base::Less::register_filter(md5_hex => \&md5_hex);

=head1 USE CODEREF AS FILTER

You can use a CodeRef as filter.

    use Digest::MD5 qw/md5_hex/;
    filters {
        input => [\&md5_hex],
    };

=head1 SEE ALSO

Most of code is taken from L<Test::Base::Less>. Thank you very match, tokuhirom.

=head1 AUTHOR

Hiroki Honda E<lt>cside.story@gmail.comE<gt>

=head1 LICENSE

Copyright (C) Hiroki Honda

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut