File Coverage

inc/Test/Class.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1 3     3   2003 #line 1
  3         5  
  3         71  
2 3     3   13 use strict;
  3         4  
  3         56  
3 3     3   57 use warnings;
  3         7  
  3         114  
4             use 5.006;
5              
6             package Test::Class;
7 3     3   2589  
  3         18027  
  3         24  
8 3     3   160 use Attribute::Handlers;
  3         6  
  3         279  
9 3     3   2283 use Carp;
  0            
  0            
10             use Class::ISA;
11             use Devel::Symdump;
12             use Storable qw(dclone);
13             use Test::Builder;
14             use Test::Class::MethodInfo;
15              
16             our $VERSION = '0.31';
17              
18             my $Check_block_has_run;
19             {
20             no warnings 'void';
21             CHECK { $Check_block_has_run = 1 };
22             }
23              
24             use constant NO_PLAN => "no_plan";
25             use constant SETUP => "setup";
26             use constant TEST => "test";
27             use constant TEARDOWN => "teardown";
28             use constant STARTUP => "startup";
29             use constant SHUTDOWN => "shutdown";
30              
31              
32             our $Current_method = undef;
33             sub current_method { $Current_method };
34              
35              
36             my $Builder = Test::Builder->new;
37             sub builder { $Builder };
38              
39              
40             my $Tests = {};
41              
42              
43             my %_Test; # inside-out object field indexed on $self
44              
45             sub DESTROY {
46             my $self = shift;
47             delete $_Test{ $self };
48             };
49              
50             sub _test_info {
51             my $self = shift;
52             return ref($self) ? $_Test{$self} : $Tests;
53             };
54              
55             sub _method_info {
56             my ($self, $class, $method) = @_;
57             return( _test_info($self)->{$class}->{$method} );
58             };
59              
60             sub _methods_of_class {
61             my ( $self, $class ) = @_;
62             my $test_info = _test_info($self)
63             or die "Test::Class internals seem confused. Did you override "
64             . "new() in a sub-class or via multiple inheritence?\n";
65             return values %{ $test_info->{$class} };
66             };
67              
68             sub _parse_attribute_args {
69             my $args = shift || '';
70             my $num_tests;
71             my $type;
72             $args =~ s/\s+//sg;
73             foreach my $arg (split /=>/, $args) {
74             if (Test::Class::MethodInfo->is_num_tests($arg)) {
75             $num_tests = $arg;
76             } elsif (Test::Class::MethodInfo->is_method_type($arg)) {
77             $type = $arg;
78             } else {
79             die 'bad attribute args';
80             };
81             };
82             return( $type, $num_tests );
83             };
84              
85             sub _is_public_method {
86             my ($class, $name) = @_;
87             foreach my $parent_class ( Class::ISA::super_path( $class ) ) {
88             return unless $parent_class->can( $name );
89             return if _method_info( $class, $parent_class, $name );
90             }
91             return 1;
92             }
93              
94             sub Test : ATTR(CODE,RAWDATA) {
95             my ($class, $symbol, $code_ref, $attr, $args) = @_;
96             if ($symbol eq "ANON") {
97             warn "cannot test anonymous subs - you probably loaded a Test::Class too late (after the CHECK block was run). See 'A NOTE ON LOADING TEST CLASSES' in perldoc Test::Class for more details\n";
98             } else {
99             my $name = *{$symbol}{NAME};
100             warn "overriding public method $name with a test method in $class\n"
101             if _is_public_method( $class, $name );
102             eval {
103             my ($type, $num_tests) = _parse_attribute_args($args);
104             $Tests->{$class}->{$name} = Test::Class::MethodInfo->new(
105             name => $name,
106             num_tests => $num_tests,
107             type => $type,
108             );
109             } || warn "bad test definition '$args' in $class->$name\n";
110             };
111             };
112              
113             sub Tests : ATTR(CODE,RAWDATA) {
114             my ($class, $symbol, $code_ref, $attr, $args) = @_;
115             $args ||= 'no_plan';
116             Test( $class, $symbol, $code_ref, $attr, $args );
117             };
118              
119             sub _class_of {
120             my $self = shift;
121             return ref $self ? ref $self : $self;
122             }
123              
124             sub new {
125             my $proto = shift;
126             my $class = _class_of( $proto );
127             $proto = {} unless ref($proto);
128             my $self = bless {%$proto, @_}, $class;
129             $_Test{$self} = dclone($Tests);
130             return($self);
131             };
132              
133             sub _get_methods {
134             my ( $self, @types ) = @_;
135             my $test_class = _class_of( $self );
136            
137             my $test_method_regexp = $ENV{ TEST_METHOD } || '.*';
138             my $method_regexp = eval { qr/\A$test_method_regexp\z/ };
139             die "TEST_METHOD ($test_method_regexp) is not a valid regexp: $@" if $@;
140            
141             my %methods = ();
142             foreach my $class ( Class::ISA::self_and_super_path( $test_class ) ) {
143             foreach my $info ( _methods_of_class( $self, $class ) ) {
144             my $name = $info->name;
145             foreach my $type ( @types ) {
146             if ( $info->is_type( $type ) ) {
147             $methods{ $name } = 1
148             unless $type eq TEST && $name !~ $method_regexp;
149             }
150             };
151             };
152             };
153              
154             return sort keys %methods;
155             };
156              
157             sub _num_expected_tests {
158             my $self = shift;
159             if (my $reason = $self->SKIP_CLASS ) {
160             return $reason eq "1" ? 0 : 1;
161             };
162             my @startup_shutdown_methods =
163             _get_methods($self, STARTUP, SHUTDOWN);
164             my $num_startup_shutdown_methods =
165             _total_num_tests($self, @startup_shutdown_methods);
166             return(NO_PLAN) if $num_startup_shutdown_methods eq NO_PLAN;
167             my @fixture_methods = _get_methods($self, SETUP, TEARDOWN);
168             my $num_fixture_tests = _total_num_tests($self, @fixture_methods);
169             return(NO_PLAN) if $num_fixture_tests eq NO_PLAN;
170             my @test_methods = _get_methods($self, TEST);
171             my $num_tests = _total_num_tests($self, @test_methods);
172             return(NO_PLAN) if $num_tests eq NO_PLAN;
173             return($num_startup_shutdown_methods + $num_tests + @test_methods * $num_fixture_tests);
174             };
175              
176             sub expected_tests {
177             my $total = 0;
178             foreach my $test (@_) {
179             if ( _isa_class( __PACKAGE__, $test ) ) {
180             my $n = _num_expected_tests($test);
181             return NO_PLAN if $n eq NO_PLAN;
182             $total += $n;
183             } elsif ( defined $test && $test =~ m/^\d+$/ ) {
184             $total += $test;
185             } else {
186             $test = 'undef' unless defined $test;
187             croak "$test is not a Test::Class or an integer";
188             };
189             };
190             return $total;
191             };
192              
193             sub _total_num_tests {
194             my ($self, @methods) = @_;
195             my $class = _class_of( $self );
196             my $total_num_tests = 0;
197             foreach my $method (@methods) {
198             foreach my $class (Class::ISA::self_and_super_path($class)) {
199             my $info = _method_info($self, $class, $method);
200             next unless $info;
201             my $num_tests = $info->num_tests;
202             return(NO_PLAN) if ($num_tests eq NO_PLAN);
203             $total_num_tests += $num_tests;
204             last unless $num_tests =~ m/^\+/
205             };
206             };
207             return($total_num_tests);
208             };
209              
210             sub _has_no_tests {
211             my ( $self, $method ) = @_;
212             return _total_num_tests( $self, $method ) eq '0';
213             }
214              
215             sub _all_ok_from {
216             my ($self, $start_test) = @_;
217             my $current_test = $Builder->current_test;
218             return(1) if $start_test == $current_test;
219             my @results = ($Builder->summary)[$start_test .. $current_test-1];
220             foreach my $result (@results) { return(0) unless $result };
221             return(1);
222             };
223              
224             sub _exception_failure {
225             my ($self, $method, $exception, $tests) = @_;
226             local $Test::Builder::Level = 3;
227             my $message = $method;
228             $message .= " (for test method '$Current_method')"
229             if defined $Current_method && $method ne $Current_method;
230             _show_header($self, @$tests);
231             $Builder->ok(0, "$message died ($exception)");
232             };
233              
234             sub _run_method {
235             my ($self, $method, $tests) = @_;
236             my $num_start = $Builder->current_test;
237             my $skip_reason;
238             my $original_ok = \&Test::Builder::ok;
239             no warnings;
240             local *Test::Builder::ok = sub {
241             my ($builder, $test, $description) = @_;
242             local $Test::Builder::Level = $Test::Builder::Level+1;
243             unless ( defined($description) ) {
244             $description = $self->current_method;
245             $description =~ tr/_/ /;
246             };
247             my $is_ok = $original_ok->($builder, $test, $description);
248             unless ( $is_ok ) {
249             my $class = ref $self;
250             $Builder->diag( " (in $class->$method)" );
251             };
252             return $is_ok;
253             };
254             $skip_reason = eval {$self->$method};
255             $skip_reason = $method unless $skip_reason;
256             my $exception = $@;
257             chomp($exception) if $exception;
258             my $num_done = $Builder->current_test - $num_start;
259             my $num_expected = _total_num_tests($self, $method);
260             $num_expected = $num_done if $num_expected eq NO_PLAN;
261             if ($num_done == $num_expected) {
262             _exception_failure($self, $method, $exception, $tests)
263             unless $exception eq '';
264             } elsif ($num_done > $num_expected) {
265             $Builder->diag("expected $num_expected test(s) in $method, $num_done completed\n");
266             } else {
267             until (($Builder->current_test - $num_start) >= $num_expected) {
268             if ($exception ne '') {
269             _exception_failure($self, $method, $exception, $tests);
270             $skip_reason = "$method died";
271             $exception = '';
272             } else {
273             $Builder->skip( $skip_reason );
274             };
275             };
276             };
277             return(_all_ok_from($self, $num_start));
278             };
279              
280             sub _show_header {
281             my ($self, @tests) = @_;
282             return if $Builder->has_plan;
283             my $num_tests = Test::Class->expected_tests(@tests);
284             if ($num_tests eq NO_PLAN) {
285             $Builder->no_plan;
286             } else {
287             $Builder->expected_tests($num_tests);
288             };
289             };
290              
291             my %SKIP_THIS_CLASS = ();
292              
293             sub SKIP_CLASS {
294             my $class = shift;
295             $SKIP_THIS_CLASS{ $class } = shift if @_;
296             return $SKIP_THIS_CLASS{ $class };
297             };
298              
299             sub _isa_class {
300             my ( $class, $object_or_class ) = @_;
301             return unless defined $object_or_class;
302             return if $object_or_class eq 'Contextual::Return::Value';
303             return eval {
304             $object_or_class->isa( $class ) and $object_or_class->can( 'runtests' )
305             };
306             }
307              
308             sub _test_classes {
309             my $class = shift;
310             return grep { _isa_class( $class, $_ ) } Devel::Symdump->rnew->packages;
311             };
312              
313             sub runtests {
314             die "Test::Class was loaded too late (after the CHECK block was run). See 'A NOTE ON LOADING TEST CLASSES' in perldoc Test::Class for more details\n"
315             unless $Check_block_has_run;
316             my @tests = @_;
317             if (@tests == 1 && !ref($tests[0])) {
318             my $base_class = shift @tests;
319             @tests = _test_classes( $base_class );
320             };
321             my $all_passed = 1;
322             TEST_OBJECT: foreach my $t (@tests) {
323             # SHOULD ALSO ALLOW NO_PLAN
324             next if $t =~ m/^\d+$/;
325             croak "$t is not Test::Class or integer"
326             unless _isa_class( __PACKAGE__, $t );
327             if (my $reason = $t->SKIP_CLASS) {
328             _show_header($t, @tests);
329             $Builder->skip( $reason ) unless $reason eq "1";
330             } else {
331             $t = $t->new unless ref($t);
332             foreach my $method (_get_methods($t, STARTUP)) {
333             _show_header($t, @tests) unless _has_no_tests($t, $method);
334             my $method_passed = _run_method($t, $method, \@tests);
335             $all_passed = 0 unless $method_passed;
336             next TEST_OBJECT unless $method_passed;
337             };
338             my $class = ref($t);
339             my @setup = _get_methods($t, SETUP);
340             my @teardown = _get_methods($t, TEARDOWN);
341             foreach my $test (_get_methods($t, TEST)) {
342             local $Current_method = $test;
343             $Builder->diag("\n$class->$test") if $ENV{TEST_VERBOSE};
344             foreach my $method (@setup, $test, @teardown) {
345             _show_header($t, @tests) unless _has_no_tests($t, $method);
346             $all_passed = 0 unless _run_method($t, $method, \@tests);
347             };
348             };
349             foreach my $method (_get_methods($t, SHUTDOWN)) {
350             _show_header($t, @tests) unless _has_no_tests($t, $method);
351             $all_passed = 0 unless _run_method($t, $method, \@tests);
352             }
353             }
354             }
355             return($all_passed);
356             };
357              
358             sub _find_calling_test_class {
359             my $level = 0;
360             while (my $class = caller(++$level)) {
361             next if $class eq __PACKAGE__;
362             return $class if _isa_class( __PACKAGE__, $class );
363             };
364             return(undef);
365             };
366              
367             sub num_method_tests {
368             my ($self, $method, $n) = @_;
369             my $class = _find_calling_test_class( $self )
370             or croak "not called in a Test::Class";
371             my $info = _method_info($self, $class, $method)
372             or croak "$method is not a test method of class $class";
373             $info->num_tests($n) if defined($n);
374             return( $info->num_tests );
375             };
376              
377             sub num_tests {
378             my $self = shift;
379             croak "num_tests need to be called within a test method"
380             unless defined $Current_method;
381             return( $self->num_method_tests( $Current_method, @_ ) );
382             };
383              
384             sub BAILOUT {
385             my ($self, $reason) = @_;
386             $Builder->BAILOUT($reason);
387             };
388              
389             sub _last_test_if_exiting_immediately {
390             $Builder->expected_tests || $Builder->current_test+1
391             };
392              
393             sub FAIL_ALL {
394             my ($self, $reason) = @_;
395             my $last_test = _last_test_if_exiting_immediately();
396             $Builder->expected_tests( $last_test ) unless $Builder->has_plan;
397             $Builder->ok(0, $reason) until $Builder->current_test >= $last_test;
398             my $num_failed = grep( !$_, $Builder->summary );
399             exit( $num_failed < 254 ? $num_failed : 254 );
400             };
401              
402             sub SKIP_ALL {
403             my ($self, $reason) = @_;
404             $Builder->skip_all( $reason ) unless $Builder->has_plan;
405             my $last_test = _last_test_if_exiting_immediately();
406             $Builder->skip( $reason )
407             until $Builder->current_test >= $last_test;
408             exit(0);
409             }
410              
411             1;
412              
413             __END__