root/lang/perl/Aspect/trunk/t/lib/Test/Class.pm @ 16531

Revision 16531, 9.4 kB (checked in by hanekomu, 5 years ago)

set module versions to latest Changes version

  • Property svn:executable set to *
Line 
1#! /usr/bin/perl -Tw
2
3package Test::Class;
4use 5.006;
5use strict;
6use warnings;
7
8use Attribute::Handlers;
9use Carp;
10use Class::ISA;
11use Devel::Symdump;
12use Storable qw(dclone);
13use Test::Builder;
14use Test::Class::MethodInfo;
15
16
17our $VERSION = '0.15';
18
19
20use constant NO_PLAN    => "no_plan";
21use constant SETUP              => "setup";
22use constant TEST               => "test";
23use constant TEARDOWN   => "teardown";
24use constant STARTUP    => "startup";
25use constant SHUTDOWN   => "shutdown";
26
27
28our     $Current_method = undef;
29sub current_method { $Current_method };
30
31
32my $Builder = Test::Builder->new;
33sub builder { $Builder };
34
35
36my $Tests = {};
37
38
39my %_Test;  # inside-out object field indexed on $self
40
41sub DESTROY {
42    my $self = shift;
43    delete $_Test{$self};
44};
45
46sub _test_info {
47        my $self = shift;
48        return(ref($self) ? $_Test{$self} : $Tests);
49};
50
51sub _method_info {
52        my ($self, $class, $method) = @_;
53        return(_test_info($self)->{$class}->{$method});
54};
55
56sub _methods_of_class {
57        my ($self, $class) = @_;
58        return(values %{_test_info($self)->{$class}});
59};
60
61sub _parse_attribute_args {
62    my $args = shift || '';
63        my $num_tests;
64        my $type;
65        $args =~ s/\s+//sg;
66        foreach my $arg (split /=>/, $args) {
67                if (Test::Class::MethodInfo->is_num_tests($arg)) {
68                        $num_tests = $arg;
69                } elsif (Test::Class::MethodInfo->is_method_type($arg)) {
70                        $type = $arg;
71                } else {
72                        die 'bad attribute args';
73                };
74        };
75        return( $type, $num_tests );
76};
77
78sub Test : ATTR(CODE,RAWDATA) {
79        my ($class, $symbol, $code_ref, $attr, $args) = @_;
80        if ($symbol eq "ANON") {
81                warn "cannot test anonymous subs\n";
82        } else {
83        my $name = *{$symbol}{NAME};
84        eval {
85            my ($type, $num_tests) = _parse_attribute_args($args);       
86            $Tests->{$class}->{$name} = Test::Class::MethodInfo->new(
87                name => $name,
88                num_tests => $num_tests,
89                type => $type,
90            ); 
91        } || warn "bad test definition '$args' in $class->$name\n";     
92    };
93};
94
95sub new {
96        my $proto = shift;
97        my $class = ref($proto) || $proto;
98        $proto = {} unless ref($proto);
99        my $self = bless { %$proto, @_ }, $class;
100        $_Test{$self} = dclone($Tests);
101        return($self);
102};
103
104sub _get_methods {
105        my ($self, @types) = @_;
106        my $test_class = ref($self) || $self;
107        my %methods = ();
108        foreach my $class (Class::ISA::self_and_super_path($test_class)) {
109                foreach my $info (_methods_of_class($self, $class)) {
110                        foreach my $type (@types) {
111                                $methods{$info->name} = 1 if $info->is_type($type);
112                        };
113                };
114        };
115        return(sort keys %methods);
116};
117
118sub _num_expected_tests {
119        my $self = shift;
120        my @startup_shutdown_methods =
121                        _get_methods($self, STARTUP, SHUTDOWN);
122        my $num_startup_shutdown_methods =
123                        _total_num_tests($self, @startup_shutdown_methods);
124        return(NO_PLAN) if $num_startup_shutdown_methods eq NO_PLAN;
125        my @fixture_methods = _get_methods($self, SETUP, TEARDOWN);
126        my $num_fixture_tests = _total_num_tests($self, @fixture_methods);
127        return(NO_PLAN) if $num_fixture_tests eq NO_PLAN;
128        my @test_methods = _get_methods($self, TEST);
129        my $num_tests = _total_num_tests($self, @test_methods);
130        return(NO_PLAN) if $num_tests eq NO_PLAN;
131        return($num_startup_shutdown_methods + $num_tests + @test_methods * $num_fixture_tests);
132};
133
134sub expected_tests {
135        my $total = 0;
136        foreach my $test (@_) {
137                if (UNIVERSAL::isa($test, __PACKAGE__)) {
138                        my $n = _num_expected_tests($test);
139                        return(NO_PLAN) if $n eq NO_PLAN;
140                        $total += $n;
141                } elsif ($test =~ m/^\d+$/) {
142                        # SHOULD ALSO ALLOW NO_PLAN
143                        $total += $test;
144                } else {
145                        $test = 'undef' unless defined($test);
146                        croak "$test is not a Test::Class or an integer";
147                };
148        };
149        return($total);
150};
151
152sub _total_num_tests {
153        my ($self, @methods) = @_;
154        my $class = ref($self) || $self;
155        my $total_num_tests = 0;
156        foreach my $method (@methods) {
157                foreach my $class (Class::ISA::self_and_super_path($class)) {
158                        my $info = _method_info($self, $class, $method);
159                        next unless $info;
160                        my $num_tests = $info->num_tests;
161                        return(NO_PLAN) if ($num_tests eq NO_PLAN);
162                        $total_num_tests += $num_tests;
163                        last unless $num_tests =~ m/^\+/
164                };
165        };
166        return($total_num_tests);
167};
168
169sub _all_ok_from {
170        my ($self, $start_test) = @_;
171        my $current_test = $Builder->current_test;
172        return(1) if $start_test == $current_test;
173        my @results = ($Builder->summary)[$start_test .. $current_test-1];
174        foreach my $result (@results) { return(0) unless $result };
175        return(1);
176};
177
178sub _exception_failure {
179        my ($self, $method, $exception, $tests) = @_;
180        local $Test::Builder::Level = 3;
181        my $message = $method;
182        $message .= " (for test method '$Current_method')"
183                        if $method ne $Current_method;
184        _show_header($self, @$tests) unless $Builder->has_plan;
185        $Builder->ok(0, "$message died ($exception)");
186};
187
188sub _run_method {
189        my ($self, $method, $tests) = @_;
190        my $original_ok = \&Test::Builder::ok;
191        {
192            no warnings;
193        *Test::Builder::ok = sub {
194            my ($builder, $test, $name) = @_;
195            local $Test::Builder::Level = $Test::Builder::Level+1;
196            unless ( defined($name) ) {
197                $name = $self->current_method;
198                $name =~ tr/_/ /;
199            };
200            $original_ok->($builder, $test, $name)
201        };
202        };
203        my $num_start = $Builder->current_test;
204        my $skip_reason = eval {$self->$method};
205        my $exception = $@;
206        chomp($exception) if $exception;
207        my $num_done = $Builder->current_test - $num_start;
208        my $num_expected = _total_num_tests($self, $method);
209        $num_expected = $num_done if $num_expected eq NO_PLAN;
210        if ($num_done == $num_expected) {
211                _exception_failure($self, $method, $exception, $tests)
212                                unless $exception eq '';
213        } elsif ($num_done > $num_expected) {
214                $Builder->diag("expected $num_expected test(s) in $method, $num_done completed\n");
215        } else {
216                until (($Builder->current_test - $num_start) >= $num_expected) {
217                        if ($exception ne '') {
218                                _exception_failure($self, $method, $exception, $tests);
219                                $skip_reason = "$method died";
220                                $exception = '';
221                        } else {
222                                $Builder->skip($skip_reason || $method);
223                        };
224                };
225        };
226        return(_all_ok_from($self, $num_start));
227};
228
229sub _show_header {
230        my ($self, @tests) = @_;
231        my $num_tests = Test::Class->expected_tests(@tests);
232        if ($num_tests eq NO_PLAN) {
233                $Builder->no_plan;
234        } else {
235                $Builder->expected_tests($num_tests);
236        };
237};
238
239sub runtests {
240        my @tests = @_;
241        if (@tests == 1 && !ref($tests[0])) {
242                my $base_class = shift @tests;
243                @tests = $base_class->run_all_classes;
244        };
245        my $all_passed = 1;
246        foreach my $t (@tests) {
247                # SHOULD ALSO ALLOW NO_PLAN
248                next if $t =~ m/^\d+$/;
249                croak "$t not Test::Class or integer"
250                                unless UNIVERSAL::isa($t, __PACKAGE__);
251                $t = $t->new unless ref($t);
252                my $class = ref($t);
253                my @setup = _get_methods($t, SETUP);
254                my @teardown = _get_methods($t, TEARDOWN);
255                foreach my $method (_get_methods($t, STARTUP)) {
256                    _show_header($t, @tests)
257                                        unless $Builder->has_plan
258                                        || _total_num_tests($t, $method) eq '0';
259                        my $method_passed = _run_method($t, $method, \@tests);
260                        $all_passed &&= $method_passed;
261                };
262                foreach my $test (_get_methods($t, TEST)) {
263                        local $Current_method = $test;
264                        $Builder->diag("\n$class->$test") if $ENV{TEST_VERBOSE};
265                        foreach my $method (@setup, $test, @teardown) {
266                                _show_header($t, @tests)
267                                                unless $Builder->has_plan
268                                                || _total_num_tests($t, $method) eq '0';
269                                my $method_passed = _run_method($t, $method, \@tests);
270                                $all_passed &&= $method_passed;
271                        };
272                };
273                foreach my $method (_get_methods($t, SHUTDOWN)) {
274                        _show_header($t, @tests)
275                                        unless $Builder->has_plan
276                                        || _total_num_tests($t, $method) eq '0';
277                        my $method_passed = _run_method($t, $method, \@tests);
278                        $all_passed &&= $method_passed;
279                };
280
281        };
282        return($all_passed);
283};
284
285my %AUTORUN = ();
286
287sub autorun {
288        my $class = shift;
289        $class = ref($class) if ref($class);
290        $AUTORUN{$class} = shift if @_;
291        return($AUTORUN{$class}) if defined($AUTORUN{$class});
292        foreach (Devel::Symdump->rnew->packages) {
293                return(0) if UNIVERSAL::isa($_, $class) && $class ne $_;
294        };
295        return(1);
296};
297
298sub run_all_classes {
299        my $class = shift;
300        grep {UNIVERSAL::isa($_, $class) && $_->autorun}
301                        Devel::Symdump->rnew->packages;
302};
303
304sub _find_calling_test_class {
305        my $level = 0;
306        while (my $class = caller(++$level)) {
307                next if $class eq __PACKAGE__;
308                return($class) if $class->isa(__PACKAGE__);
309        };
310        return(undef);
311};
312
313sub num_method_tests {
314        my ($self, $method, $n) = @_;
315        my $class = _find_calling_test_class( $self )
316            or croak "not called in a Test::Class";
317        my $info = _method_info($self, $class, $method)
318            or croak "$method is not a test method of class $class";
319        $info->num_tests($n) if defined($n);
320        return( $info->num_tests );
321};
322
323sub num_tests {
324    my $self = shift;
325        croak "num_tests need to be called within a test method"
326                        unless defined $Current_method;
327        return( $self->num_method_tests( $Current_method, @_ ) );
328};
329
330sub BAILOUT {
331        my ($self, $reason) = @_;
332        $Builder->BAILOUT($reason);
333};
334
335sub _last_test_if_exiting_immediately {
336    $Builder->expected_tests || $Builder->current_test+1
337};
338
339sub FAIL_ALL {
340        my ($self, $reason) = @_;
341        my $last_test = _last_test_if_exiting_immediately();
342        $Builder->expected_tests( $last_test ) unless $Builder->has_plan;
343        $Builder->ok(0, $reason) until $Builder->current_test >= $last_test;
344        my $num_failed = grep( !$_, $Builder->summary );
345        exit( $num_failed < 254 ? $num_failed : 254 );
346};
347
348sub SKIP_ALL { 
349        my ($self, $reason) = @_;
350        $Builder->skip_all( $reason ) unless $Builder->has_plan;
351        my $last_test = _last_test_if_exiting_immediately();
352        $Builder->skip( $reason )
353            until $Builder->current_test >= $last_test;
354        exit(0);
355}
356
3571;
Note: See TracBrowser for help on using the browser.