File Coverage

inc/Test/ClassAPI.pm
Criterion Covered Total %
statement 112 135 82.9
branch 19 42 45.2
condition 4 7 57.1
subroutine 13 13 100.0
pod 1 2 50.0
total 149 199 74.8


line stmt bran cond sub pod time code
1             #line 1
2             package Test::ClassAPI;
3              
4             # Allows us to test class APIs in a simplified manner.
5             # Implemented as a wrapper around Test::More, Class::Inspector and Config::Tiny.
6 1     1   22523  
  1         3  
  1         43  
7 1     1   6 use 5.005;
  1         2  
  1         59  
8 1     1   6 use strict;
  1         2  
  1         20  
9 1     1   855 use Test::More ();
  1         847  
  1         16  
10 1     1   705 use Config::Tiny ();
  1         3399  
  1         26  
11 1     1   9 use Class::Inspector ();
  1         3  
  1         65  
12             use Params::Util '_INSTANCE';
13 1     1   6  
  1         2  
  1         101  
14             use vars qw{$VERSION $CONFIG $SCHEDULE $EXECUTED %IGNORE *DATA};
15 1     1   1 BEGIN {
16             $VERSION = '1.04';
17              
18 1         2 # Config starts empty
19 1         1 $CONFIG = undef;
20             $SCHEDULE = undef;
21              
22 1         1 # We only execute once
23             $EXECUTED = '';
24              
25             # When looking for method that arn't described in the class
26 1         2 # description, we ignore anything from UNIVERSAL.
  2         54  
27             %IGNORE = map { $_, 1 } qw{isa can};
28             }
29              
30             # Get the super path ( not including UNIVERSAL )
31             # Rather than using Class::ISA, we'll use an inlined version
32             # that implements the same basic algorithm, but faster.
33 5     5   8 sub _super_path($) {
34 5         8 my $class = shift;
35 5         7 my @path = ();
36 5         60 my @queue = ( $class );
37 5         15 my %seen = ( $class => 1 );
38 1     1   5 while ( my $cl = shift @queue ) {
  1         2  
  1         902  
39 9         11 no strict 'refs';
40 4         20 push @path, $cl;
  4         8  
41 4         6 unshift @queue, grep { ! $seen{$_}++ }
  4         7  
  9         48  
42 9         8 map { s/^::/main::/; s/\'/::/g; $_ }
43             ( @{"${cl}::ISA"} );
44             }
45 5         12  
46             @path;
47             }
48              
49              
50              
51              
52              
53             #####################################################################
54             # Main Methods
55              
56             # Initialise the Configuration
57 1     1 0 2 sub init {
58             my $class = shift;
59              
60 1 50       8 # Use the script's DATA handle or one passed
61             *DATA = ref($_[0]) eq 'GLOB' ? shift : *main::DATA;
62            
63 1         5 # Read in all the data, and create the config object
64 1 50       48 local $/ = undef;
65             $CONFIG = Config::Tiny->read_string( )
66             or die 'Failed to load test configuration: '
67 1 50       462 . Config::Tiny->errstr;
68             $SCHEDULE = delete $CONFIG->{_}
69             or die 'Config does not have a schedule defined';
70              
71 1         6 # Add implied schedule entries
72 6   100     84 foreach my $tclass ( keys %$CONFIG ) {
73 6         6 $SCHEDULE->{$tclass} ||= 'class';
  6         17  
74 29 50       61 foreach my $test ( keys %{$CONFIG->{$tclass}} ) {
75 0   0     0 next unless $CONFIG->{$tclass}->{$test} eq 'implements';
76             $SCHEDULE->{$test} ||= 'interface';
77             }
78             }
79            
80              
81 1         3 # Check the schedule information
82 6         8 foreach my $tclass ( keys %$SCHEDULE ) {
83 6 50       21 my $value = $SCHEDULE->{$tclass};
84 0         0 unless ( $value =~ /^(?:class|abstract|interface)$/ ) {
85             die "Invalid schedule option '$value' for class '$tclass'";
86 6 50       14 }
87 0         0 unless ( $CONFIG->{$tclass} ) {
88             die "No section '[$tclass]' defined for schedule class";
89             }
90             }
91 1         5  
92             1;
93             }
94              
95             # Find and execute the tests
96 1     1 1 14 sub execute {
97 1 50       4 my $class = shift;
98 0         0 if ( $EXECUTED ) {
99             die 'You can only execute once, use another test script';
100 1 50       8 }
101             $class->init unless $CONFIG;
102              
103 1         2 # Handle options
  2         8  
104 1         3 my @options = map { lc $_ } @_;
  2         5  
105 1         2 my $CHECK_UNKNOWN_METHODS = !! grep { $_ eq 'complete' } @options;
  2         4  
106             my $CHECK_FUNCTION_COLLISIONS = !! grep { $_ eq 'collisions' } @options;
107              
108 1 50       14 # Set the plan of no plan if we don't have a plan
109 0         0 unless ( Test::More->builder->has_plan ) {
110             Test::More::plan( 'no_plan' );
111             }
112              
113 1         28 # Determine the list of classes to test
114 1         3 my @classes = sort keys %$SCHEDULE;
  6         12  
115             @classes = grep { $SCHEDULE->{$_} ne 'interface' } @classes;
116              
117 1         2 # Check that all the classes/abstracts are loaded
118 6         1376 foreach my $class ( @classes ) {
119             Test::More::ok( Class::Inspector->loaded( $class ), "Class '$class' is loaded" );
120             }
121              
122 1         241 # Check that all the full classes match all the required interfaces
  6         13  
123 1         3 @classes = grep { $SCHEDULE->{$_} eq 'class' } @classes;
124             foreach my $class ( @classes ) {
125 5         853 # Find all testable parents
  9         22  
126             my @path = grep { $SCHEDULE->{$_} } _super_path($class);
127              
128 5         6 # Iterate over the testable entries
129 5         6 my %known_methods = ();
130 5         7 my @implements = ();
131 9         10 foreach my $parent ( @path ) {
  9         123  
132 42         82 foreach my $test ( sort keys %{$CONFIG->{$parent}} ) {
133             my $type = $CONFIG->{$parent}->{$test};
134              
135 42 100       80 # Does the class have a named method
136 38         54 if ( $type eq 'method' ) {
137 38         78 $known_methods{$test}++;
138 38         11262 Test::More::can_ok( $class, $test );
139             next;
140             }
141              
142 4 50       12 # Does the class inherit from a named parent
143 4         35 if ( $type eq 'isa' ) {
144 4         893 Test::More::ok( $class->isa($test), "$class isa $test" );
145             next;
146             }
147 0 0       0  
148 0         0 unless ( $type eq 'implements' ) {
149 0         0 print "# Warning: Unknown test type '$type'";
150             next;
151             }
152            
153             # When we 'implement' a class or interface,
154             # we need to check the 'method' tests within
155             # it, but not anything else. So we will add
156             # the class name to a seperate queue to be
157             # processed afterwards, ONLY if it is not
158             # already in the normal @path, or already
159 0 0       0 # on the seperate queue.
  0         0  
160 0 0       0 next if grep { $_ eq $test } @path;
  0         0  
161 0         0 next if grep { $_ eq $test } @implements;
162             push @implements, $test;
163             }
164             }
165              
166             # Now, if it had any, go through and check the classes added
167 5         9 # because of any 'implements' tests
168 0         0 foreach my $parent ( @implements ) {
  0         0  
169 0         0 foreach my $test ( keys %{$CONFIG->{$parent}} ) {
170 0 0       0 my $type = $CONFIG->{$parent}->{$test};
171             if ( $type eq 'method' ) {
172 0         0 # Does the class have a method
173 0         0 $known_methods{$test}++;
174             Test::More::can_ok( $class, $test );
175             }
176             }
177             }
178 5 50       12  
179             if ( $CHECK_UNKNOWN_METHODS ) {
180 5 50       19 # Check for unknown public methods
181             my $methods = Class::Inspector->methods( $class, 'public', 'expanded' )
182 1         10 or die "Failed to find public methods for class '$class'";
  1         3  
183 37   66     90 @$methods = grep { $_->[2] !~ /^[A-Z_]+$/ } # Internals stuff
184 5         1022 grep { $_->[1] ne 'Exporter' } # Ignore Exporter methods we don't overload
185 5 50       12 grep { ! ($known_methods{$_->[2]} or $IGNORE{$_->[2]}) } @$methods;
186 0         0 if ( @$methods ) {
  0         0  
187             print STDERR join '', map { "# Found undocumented method '$_->[2]' defined at '$_->[0]'\n" } @$methods;
188 5         20 }
189             Test::More::is( scalar(@$methods), 0, "No unknown public methods in '$class'" );
190             }
191 5 50       1188  
192             if ( $CHECK_FUNCTION_COLLISIONS ) {
193             # Check for methods collisions.
194             # A method collision is where
195             #
196             # Foo::Bar->method
197             #
198             # is actually interpreted as
199             #
200             # &Foo::Bar()->method
201 1     1   4 #
  1         3  
  1         222  
202 5         9 no strict 'refs';
203 5         4 my @collisions = ();
  5         45  
204 78 100       136 foreach my $symbol ( sort keys %{"${class}::"} ) {
205 5 50       5 next unless $symbol =~ s/::$//;
  5         34  
206 0         0 next unless defined *{"${class}::${symbol}"}{CODE};
207 0         0 print STDERR "Found function collision: ${class}->${symbol} clashes with ${class}::${symbol}\n";
208             push @collisions, $symbol;
209 5         26 }
210             Test::More::is( scalar(@collisions), 0, "No function/class collisions in '$class'" );
211             }
212             }
213 1         271  
214             1;
215             }
216              
217             1;
218              
219             __END__