File Coverage

blib/lib/Test/ClassAPI.pm
Criterion Covered Total %
statement 121 136 88.9
branch 24 42 57.1
condition 4 7 57.1
subroutine 14 14 100.0
pod 1 2 50.0
total 164 201 81.5


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