root/inc/Test/Base.pm

Revision 21807, 36.5 kB (checked in by audreyt, 5 months ago)

* Upgrade inc/Test/ to newest version.

Line 
1# TODO:
2#
3package Test::Base;
4use 5.006001;
5use Spiffy 0.30 -Base;
6use Spiffy ':XXX';
7our $VERSION = '0.54';
8
9my @test_more_exports;
10BEGIN {
11    @test_more_exports = qw(
12        ok isnt like unlike is_deeply cmp_ok
13        skip todo_skip pass fail
14        eq_array eq_hash eq_set
15        plan can_ok isa_ok diag
16        use_ok
17        $TODO
18    );
19}
20
21use Test::More import => \@test_more_exports;
22use Carp;
23
24our @EXPORT = (@test_more_exports, qw(
25    is no_diff
26
27    blocks next_block first_block
28    delimiters spec_file spec_string
29    filters filters_delay filter_arguments
30    run run_compare run_is run_is_deeply run_like run_unlike
31    WWW XXX YYY ZZZ
32    tie_output no_diag_on_only
33
34    find_my_self default_object
35
36    croak carp cluck confess
37));
38
39field '_spec_file';
40field '_spec_string';
41field _filters => [qw(norm trim)];
42field _filters_map => {};
43field spec =>
44      -init => '$self->_spec_init';
45field block_list =>
46      -init => '$self->_block_list_init';
47field _next_list => [];
48field block_delim =>
49      -init => '$self->block_delim_default';
50field data_delim =>
51      -init => '$self->data_delim_default';
52field _filters_delay => 0;
53field _no_diag_on_only => 0;
54
55field block_delim_default => '===';
56field data_delim_default => '---';
57
58my $default_class;
59my $default_object;
60my $reserved_section_names = {};
61
62sub default_object { 
63    $default_object ||= $default_class->new;
64    return $default_object;
65}
66
67my $import_called = 0;
68sub import() {
69    $import_called = 1;
70    my $class = (grep /^-base$/i, @_) 
71    ? scalar(caller)
72    : $_[0];
73    if (not defined $default_class) {
74        $default_class = $class;
75    }
76#     else {
77#         croak "Can't use $class after using $default_class"
78#           unless $default_class->isa($class);
79#     }
80
81    unless (grep /^-base$/i, @_) {
82        my @args;
83        for (my $ii = 1; $ii <= $#_; ++$ii) {
84            if ($_[$ii] eq '-package') {
85                ++$ii;
86            } else {
87                push @args, $_[$ii];
88            }
89        }
90        Test::More->import(import => \@test_more_exports, @args)
91            if @args;
92     }
93   
94    _strict_warnings();
95    goto &Spiffy::import;
96}
97
98# Wrap Test::Builder::plan
99my $plan_code = \&Test::Builder::plan;
100my $Have_Plan = 0;
101{
102    no warnings 'redefine';
103    *Test::Builder::plan = sub {
104        $Have_Plan = 1;
105        goto &$plan_code;
106    };
107}
108
109my $DIED = 0;
110$SIG{__DIE__} = sub { $DIED = 1; die @_ };
111
112sub block_class  { $self->find_class('Block') }
113sub filter_class { $self->find_class('Filter') }
114
115sub find_class {
116    my $suffix = shift;
117    my $class = ref($self) . "::$suffix";
118    return $class if $class->can('new');
119    $class = __PACKAGE__ . "::$suffix";
120    return $class if $class->can('new');
121    eval "require $class";
122    return $class if $class->can('new');
123    die "Can't find a class for $suffix";
124}
125
126sub check_late {
127    if ($self->{block_list}) {
128        my $caller = (caller(1))[3];
129        $caller =~ s/.*:://;
130        croak "Too late to call $caller()"
131    }
132}
133
134sub find_my_self() {
135    my $self = ref($_[0]) eq $default_class
136    ? splice(@_, 0, 1)
137    : default_object();
138    return $self, @_;
139}
140
141sub blocks() {
142    (my ($self), @_) = find_my_self(@_);
143
144    croak "Invalid arguments passed to 'blocks'"
145      if @_ > 1;
146    croak sprintf("'%s' is invalid argument to blocks()", shift(@_))
147      if @_ && $_[0] !~ /^[a-zA-Z]\w*$/;
148
149    my $blocks = $self->block_list;
150   
151    my $section_name = shift || '';
152    my @blocks = $section_name
153    ? (grep { exists $_->{$section_name} } @$blocks)
154    : (@$blocks);
155
156    return scalar(@blocks) unless wantarray;
157   
158    return (@blocks) if $self->_filters_delay;
159
160    for my $block (@blocks) {
161        $block->run_filters
162          unless $block->is_filtered;
163    }
164
165    return (@blocks);
166}
167
168sub next_block() {
169    (my ($self), @_) = find_my_self(@_);
170    my $list = $self->_next_list;
171    if (@$list == 0) {
172        $list = [@{$self->block_list}, undef];
173        $self->_next_list($list);
174    }
175    my $block = shift @$list;
176    if (defined $block and not $block->is_filtered) {
177        $block->run_filters;
178    }
179    return $block;
180}
181
182sub first_block() {
183    (my ($self), @_) = find_my_self(@_);
184    $self->_next_list([]);
185    $self->next_block;
186}
187
188sub filters_delay() {
189    (my ($self), @_) = find_my_self(@_);
190    $self->_filters_delay(defined $_[0] ? shift : 1);
191}
192
193sub no_diag_on_only() {
194    (my ($self), @_) = find_my_self(@_);
195    $self->_no_diag_on_only(defined $_[0] ? shift : 1);
196}
197
198sub delimiters() {
199    (my ($self), @_) = find_my_self(@_);
200    $self->check_late;
201    my ($block_delimiter, $data_delimiter) = @_;
202    $block_delimiter ||= $self->block_delim_default;
203    $data_delimiter ||= $self->data_delim_default;
204    $self->block_delim($block_delimiter);
205    $self->data_delim($data_delimiter);
206    return $self;
207}
208
209sub spec_file() {
210    (my ($self), @_) = find_my_self(@_);
211    $self->check_late;
212    $self->_spec_file(shift);
213    return $self;
214}
215
216sub spec_string() {
217    (my ($self), @_) = find_my_self(@_);
218    $self->check_late;
219    $self->_spec_string(shift);
220    return $self;
221}
222
223sub filters() {
224    (my ($self), @_) = find_my_self(@_);
225    if (ref($_[0]) eq 'HASH') {
226        $self->_filters_map(shift);
227    }
228    else {   
229        my $filters = $self->_filters;
230        push @$filters, @_;
231    }
232    return $self;
233}
234
235sub filter_arguments() {
236    $Test::Base::Filter::arguments;
237}
238
239sub have_text_diff {
240    eval { require Text::Diff; 1 } &&
241        $Text::Diff::VERSION >= 0.35 &&
242        $Algorithm::Diff::VERSION >= 1.15;
243}
244
245sub is($$;$) {
246    (my ($self), @_) = find_my_self(@_);
247    my ($actual, $expected, $name) = @_;
248    local $Test::Builder::Level = $Test::Builder::Level + 1;
249    if ($ENV{TEST_SHOW_NO_DIFFS} or
250         not defined $actual or
251         not defined $expected or
252         $actual eq $expected or 
253         not($self->have_text_diff) or 
254         $expected !~ /\n./s
255    ) {
256        Test::More::is($actual, $expected, $name);
257    }
258    else {
259        $name = '' unless defined $name;
260        ok $actual eq $expected,
261           $name . "\n" . Text::Diff::diff(\$expected, \$actual);
262    }
263}
264
265sub run(&;$) {
266    (my ($self), @_) = find_my_self(@_);
267    my $callback = shift;
268    for my $block (@{$self->block_list}) {
269        $block->run_filters unless $block->is_filtered;
270        &{$callback}($block);
271    }
272}
273
274my $name_error = "Can't determine section names";
275sub _section_names {
276    return @_ if @_ == 2;
277    my $block = $self->first_block
278      or croak $name_error;
279    my @names = grep {
280        $_ !~ /^(ONLY|LAST|SKIP)$/;
281    } @{$block->{_section_order}[0] || []};
282    croak "$name_error. Need two sections in first block"
283      unless @names == 2;
284    return @names;
285}
286
287sub _assert_plan {
288    plan('no_plan') unless $Have_Plan;
289}
290
291sub END {
292    run_compare() unless $Have_Plan or $DIED or not $import_called;
293}
294
295sub run_compare() {
296    (my ($self), @_) = find_my_self(@_);
297    $self->_assert_plan;
298    my ($x, $y) = $self->_section_names(@_);
299    local $Test::Builder::Level = $Test::Builder::Level + 1;
300    for my $block (@{$self->block_list}) {
301        next unless exists($block->{$x}) and exists($block->{$y});
302        $block->run_filters unless $block->is_filtered;
303        if (ref $block->$x) {
304            is_deeply($block->$x, $block->$y,
305                $block->name ? $block->name : ());
306        }
307        elsif (ref $block->$y eq 'Regexp') {
308            my $regexp = ref $y ? $y : $block->$y;
309            like($block->$x, $regexp, $block->name ? $block->name : ());
310        }
311        else {
312            is($block->$x, $block->$y, $block->name ? $block->name : ());
313        }
314    }
315}
316
317sub run_is() {
318    (my ($self), @_) = find_my_self(@_);
319    $self->_assert_plan;
320    my ($x, $y) = $self->_section_names(@_);
321    local $Test::Builder::Level = $Test::Builder::Level + 1;
322    for my $block (@{$self->block_list}) {
323        next unless exists($block->{$x}) and exists($block->{$y});
324        $block->run_filters unless $block->is_filtered;
325        is($block->$x, $block->$y, 
326           $block->name ? $block->name : ()
327          );
328    }
329}
330
331sub run_is_deeply() {
332    (my ($self), @_) = find_my_self(@_);
333    $self->_assert_plan;
334    my ($x, $y) = $self->_section_names(@_);
335    for my $block (@{$self->block_list}) {
336        next unless exists($block->{$x}) and exists($block->{$y});
337        $block->run_filters unless $block->is_filtered;
338        is_deeply($block->$x, $block->$y, 
339           $block->name ? $block->name : ()
340          );
341    }
342}
343
344sub run_like() {
345    (my ($self), @_) = find_my_self(@_);
346    $self->_assert_plan;
347    my ($x, $y) = $self->_section_names(@_);
348    for my $block (@{$self->block_list}) {
349        next unless exists($block->{$x}) and defined($y);
350        $block->run_filters unless $block->is_filtered;
351        my $regexp = ref $y ? $y : $block->$y;
352        like($block->$x, $regexp,
353             $block->name ? $block->name : ()
354            );
355    }
356}
357
358sub run_unlike() {
359    (my ($self), @_) = find_my_self(@_);
360    $self->_assert_plan;
361    my ($x, $y) = $self->_section_names(@_);
362    for my $block (@{$self->block_list}) {
363        next unless exists($block->{$x}) and defined($y);
364        $block->run_filters unless $block->is_filtered;
365        my $regexp = ref $y ? $y : $block->$y;
366        unlike($block->$x, $regexp,
367               $block->name ? $block->name : ()
368              );
369    }
370}
371
372sub _pre_eval {
373    my $spec = shift;
374    return $spec unless $spec =~
375      s/\A\s*<<<(.*?)>>>\s*$//sm;
376    my $eval_code = $1;
377    eval "package main; $eval_code";
378    croak $@ if $@;
379    return $spec;
380}
381
382sub _block_list_init {
383    my $spec = $self->spec;
384    $spec = $self->_pre_eval($spec);
385    my $cd = $self->block_delim;
386    my @hunks = ($spec =~ /^(\Q${cd}\E.*?(?=^\Q${cd}\E|\z))/msg);
387    my $blocks = $self->_choose_blocks(@hunks);
388    $self->block_list($blocks); # Need to set early for possible filter use
389    my $seq = 1;
390    for my $block (@$blocks) {
391        $block->blocks_object($self);
392        $block->seq_num($seq++);
393    }
394    return $blocks;
395}
396
397sub _choose_blocks {
398    my $blocks = [];
399    for my $hunk (@_) {
400        my $block = $self->_make_block($hunk);
401        if (exists $block->{ONLY}) {
402            diag "I found ONLY: maybe you're debugging?"
403                unless $self->_no_diag_on_only;
404            return [$block];
405        }
406        next if exists $block->{SKIP};
407        push @$blocks, $block;
408        if (exists $block->{LAST}) {
409            return $blocks;
410        }
411    }
412    return $blocks;
413}
414
415sub _check_reserved {
416    my $id = shift;
417    croak "'$id' is a reserved name. Use something else.\n"
418      if $reserved_section_names->{$id} or
419         $id =~ /^_/;
420}
421
422sub _make_block {
423    my $hunk = shift;
424    my $cd = $self->block_delim;
425    my $dd = $self->data_delim;
426    my $block = $self->block_class->new;
427    $hunk =~ s/\A\Q${cd}\E[ \t]*(.*)\s+// or die;
428    my $name = $1;
429    my @parts = split /^\Q${dd}\E +\(?(\w+)\)? *(.*)?\n/m, $hunk;
430    my $description = shift @parts;
431    $description ||= '';
432    unless ($description =~ /\S/) {
433        $description = $name;
434    }
435    $description =~ s/\s*\z//;
436    $block->set_value(description => $description);
437   
438    my $section_map = {};
439    my $section_order = [];
440    while (@parts) {
441        my ($type, $filters, $value) = splice(@parts, 0, 3);
442        $self->_check_reserved($type);
443        $value = '' unless defined $value;
444        $filters = '' unless defined $filters;
445        if ($filters =~ /:(\s|\z)/) {
446            croak "Extra lines not allowed in '$type' section"
447              if $value =~ /\S/;
448            ($filters, $value) = split /\s*:(?:\s+|\z)/, $filters, 2;
449            $value = '' unless defined $value;
450            $value =~ s/^\s*(.*?)\s*$/$1/;
451        }
452        $section_map->{$type} = {
453            filters => $filters,
454        };
455        push @$section_order, $type;
456        $block->set_value($type, $value);
457    }
458    $block->set_value(name => $name);
459    $block->set_value(_section_map => $section_map);
460    $block->set_value(_section_order => $section_order);
461    return $block;
462}
463
464sub _spec_init {
465    return $self->_spec_string
466      if $self->_spec_string;
467    local $/;
468    my $spec;
469    if (my $spec_file = $self->_spec_file) {
470        open FILE, $spec_file or die $!;
471        $spec = <FILE>;
472        close FILE;
473    }
474    else {   
475        $spec = do { 
476            package main; 
477            no warnings 'once';
478            <DATA>;
479        };
480    }
481    return $spec;
482}
483
484sub _strict_warnings() {
485    require Filter::Util::Call;
486    my $done = 0;
487    Filter::Util::Call::filter_add(
488        sub {
489            return 0 if $done;
490            my ($data, $end) = ('', '');
491            while (my $status = Filter::Util::Call::filter_read()) {
492                return $status if $status < 0;
493                if (/^__(?:END|DATA)__\r?$/) {
494                    $end = $_;
495                    last;
496                }
497                $data .= $_;
498                $_ = '';
499            }
500            $_ = "use strict;use warnings;$data$end";
501            $done = 1;
502        }
503    );
504}
505
506sub tie_output() {
507    my $handle = shift;
508    die "No buffer to tie" unless @_;
509    tie $handle, 'Test::Base::Handle', $_[0];
510}
511
512sub no_diff {
513    $ENV{TEST_SHOW_NO_DIFFS} = 1;
514}
515
516package Test::Base::Handle;
517
518sub TIEHANDLE() {
519    my $class = shift;
520    bless \ $_[0], $class;
521}
522
523sub PRINT {
524    $$self .= $_ for @_;
525}
526
527#===============================================================================
528# Test::Base::Block
529#
530# This is the default class for accessing a Test::Base block object.
531#===============================================================================
532package Test::Base::Block;
533our @ISA = qw(Spiffy);
534
535our @EXPORT = qw(block_accessor);
536
537sub AUTOLOAD {
538    return;
539}
540
541sub block_accessor() {
542    my $accessor = shift;
543    no strict 'refs';
544    return if defined &$accessor;
545    *$accessor = sub {
546        my $self = shift;
547        if (@_) {
548            Carp::croak "Not allowed to set values for '$accessor'";
549        }
550        my @list = @{$self->{$accessor} || []};
551        return wantarray
552        ? (@list)
553        : $list[0];
554    };
555}
556
557block_accessor 'name';
558block_accessor 'description';
559Spiffy::field 'seq_num';
560Spiffy::field 'is_filtered';
561Spiffy::field 'blocks_object';
562Spiffy::field 'original_values' => {};
563
564sub set_value {
565    no strict 'refs';
566    my $accessor = shift;
567    block_accessor $accessor
568      unless defined &$accessor;
569    $self->{$accessor} = [@_];
570}
571
572sub run_filters {
573    my $map = $self->_section_map;
574    my $order = $self->_section_order;
575    Carp::croak "Attempt to filter a block twice"
576      if $self->is_filtered;
577    for my $type (@$order) {
578        my $filters = $map->{$type}{filters};
579        my @value = $self->$type;
580        $self->original_values->{$type} = $value[0];
581        for my $filter ($self->_get_filters($type, $filters)) {
582            $Test::Base::Filter::arguments =
583              $filter =~ s/=(.*)$// ? $1 : undef;
584            my $function = "main::$filter";
585            no strict 'refs';
586            if (defined &$function) {
587                local $_ = join '', @value;
588                my $old = $_;
589                @value = &$function(@value);
590                if (not(@value) or 
591                    @value == 1 and $value[0] =~ /\A(\d+|)\z/
592                ) {
593                    if ($value[0] && $_ eq $old) {
594                        Test::Base::diag("Filters returning numbers are supposed to do munging \$_: your filter '$function' apparently doesn't.");
595                    }
596                    @value = ($_);
597                }
598            }
599            else {
600                my $filter_object = $self->blocks_object->filter_class->new;
601                die "Can't find a function or method for '$filter' filter\n"
602                  unless $filter_object->can($filter);
603                $filter_object->current_block($self);
604                @value = $filter_object->$filter(@value);
605            }
606            # Set the value after each filter since other filters may be
607            # introspecting.
608            $self->set_value($type, @value);
609        }
610    }
611    $self->is_filtered(1);
612}
613
614sub _get_filters {
615    my $type = shift;
616    my $string = shift || '';
617    $string =~ s/\s*(.*?)\s*/$1/;
618    my @filters = ();
619    my $map_filters = $self->blocks_object->_filters_map->{$type} || [];
620    $map_filters = [ $map_filters ] unless ref $map_filters;
621    my @append = ();
622    for (
623        @{$self->blocks_object->_filters}, 
624        @$map_filters,
625        split(/\s+/, $string),
626    ) {
627        my $filter = $_;
628        last unless length $filter;
629        if ($filter =~ s/^-//) {
630            @filters = grep { $_ ne $filter } @filters;
631        }
632        elsif ($filter =~ s/^\+//) {
633            push @append, $filter;
634        }
635        else {
636            push @filters, $filter;
637        }
638    }
639    return @filters, @append;
640}
641
642{
643    %$reserved_section_names = map {
644        ($_, 1);
645    } keys(%Test::Base::Block::), qw( new DESTROY );
646}
647
648__DATA__
649
650=head1 NAME
651
652Test::Base - A Data Driven Testing Framework
653
654=head1 SYNOPSIS
655
656A new test module:
657
658    # lib/MyProject/Test.pm
659    package MyProject::Test;
660    use Test::Base -Base;
661   
662    use MyProject;
663   
664    package MyProject::Test::Filter;
665    use Test::Base::Filter -base;
666
667    sub my_filter {
668        return MyProject->do_something(shift);
669    }
670
671A sample test:   
672   
673    # t/sample.t
674    use MyProject::Test;
675   
676    plan tests => 1 * blocks;
677   
678    run_is input => 'expected';
679
680    sub local_filter {
681        s/my/your/;
682    }
683   
684    __END__
685   
686    === Test one (the name of the test)
687    --- input my_filter local_filter
688    my
689    input
690    lines
691    --- expected
692    expected
693    output