After realizing the sad state of code coverage on our unit tests at work I am trying to create a utility that will scan our code base and flag files that don't have 100%. I found two approaches that get all of the methods:
Access symbol table directly:
for my $classname ( @ARGV ) {
eval "require $classname";
die "Can't load $classname $EVAL_ERROR"
if $EVAL_ERROR;
no strict 'refs';
METHODS:
for my $sym ( keys %{ "${classname}::" } ) {
next METHODS unless defined &{"${classname}::${sym}"};
print "$sym\n";
}
}
Use the Class::Inspector
module from CPAN:
for my $classname ( @ARGV ) {
my @methods = Class::Inspector->methods($classname, 'public');
print Dumper \@methods;
}
these two approaches produce similar results; The problem with these is that they show all of the methods available to the entire module, not just the methods defined inside of that module.
Is there some way to distinguish between methods accessible to a module and methods defined explicitly inside of a module?
Note: I am not attempting to create a full code coverage test, for my use case I just want to test that all of the methods have been called at least once. Complete coverage tests like Devel::Cover
are overkill for us.
Each sub (or more specifically, each CV), remembers which package it was originally declared in. Test case:
Foo.pm
:
package Foo;
sub import {
*{caller . "::foo"} = sub{};
}
1;
Bar.pm
:
package Bar;
use Foo;
our $bar; # introduces *Bar::bar which does not have a CODE slot
sub baz {}
1;
Accessing the symbol table now gives both foo
and baz
. By the way, I'd write that code like this (for reasons that will become clear in a moment):
my $classname = 'Bar';
for my $glob (values %{ "${classname}::" }) {
my $sub = *$glob{CODE} or next;
say *$glob{NAME};
}
Next, we have to look into the B
module to introspect the underlying C data structure. We do this with the B::svref_2object
function. This will produce a B::CV
object which has the convenient STASH
field (which returns a B::HV
object which has a NAME
field):
use B ();
my $classname = 'Bar';
for my $glob (values %{ "${classname}::" }) {
my $sub = *$glob{CODE} or next;
my $cv = B::svref_2object($sub);
$cv->STASH->NAME eq $classname or next;
say *$glob{NAME};
}
Add a few sanity checks, and this should work quite well.
Dynamic class/module loading should not be done via string eval
. Instead I recommend Module::Runtime
:
Module::Runtime::require_module($classname);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With