File Coverage

blib/lib/Test/ClassAPI.pm
Criterion Covered Total %
statement 123 138 89.1
branch 24 42 57.1
condition 4 7 57.1
subroutine 14 14 100.0
pod 1 2 50.0
total 166 203 81.7


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