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