477 lines
13 KiB
Perl
477 lines
13 KiB
Perl
# My::Test::Class
|
|
#
|
|
# Copyright (c) 2018 Eric Miller
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
# * Neither the name of the University of California, Santa Cruz nor the
|
|
# names of its contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
# LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
package My::Test::Class;
|
|
|
|
use Test::Class::Most attributes => [qw/class mock_session todo_methods/];
|
|
use Test::MockObject::Extends;
|
|
use File::Find 'find';
|
|
use Path::Class 'dir';
|
|
use File::Slurper 'read_lines';
|
|
use Class::Inspector;
|
|
|
|
use base qw<Test::Class Class::Data::Inheritable>;
|
|
|
|
# Don't run the base tests defined in this class, run them in subclasses only
|
|
My::Test::Class->SKIP_CLASS(1);
|
|
|
|
# add a test which runs "use Test::Class::Load qw<xt/lib>;" and uncomment:
|
|
# INIT { Test::Class->runtests }
|
|
|
|
my $EMPTY = q{};
|
|
|
|
sub startup : Tests( startup => 1 ) {
|
|
my $test = shift;
|
|
(my $class = ref $test) =~ s/^Test:://x;
|
|
return ok 1, "$class loaded" if $class eq __PACKAGE__;
|
|
use_ok $class or die;
|
|
$test->class($class);
|
|
$test->mock_session(create_mock_session());
|
|
$test->todo_methods(0);
|
|
return;
|
|
}
|
|
|
|
sub shutdown : Tests(shutdown) { }
|
|
|
|
sub setup : Tests(setup) {
|
|
my $test = shift;
|
|
my $class = $test->class;
|
|
my $sess = $test->mock_session;
|
|
|
|
$test->{info} = $class->new(
|
|
'AutoSpecify' => 0,
|
|
'BulkWalk' => 0,
|
|
'UseEnums' => 1,
|
|
'RetryNoSuch' => 1,
|
|
'DestHost' => '127.0.0.1',
|
|
'Community' => 'public',
|
|
'Version' => 2,
|
|
'Session' => $sess,
|
|
);
|
|
}
|
|
|
|
sub teardown : Tests(teardown) {
|
|
my $test = shift;
|
|
my $sess = $test->mock_session;
|
|
|
|
# Make sure we start clear object and any mocked session data after each test
|
|
$test->{info} = undef;
|
|
$sess->{Data} = {};
|
|
}
|
|
|
|
sub constructor : Tests(8) {
|
|
my $test = shift;
|
|
my $class = $test->class;
|
|
|
|
can_ok $class, 'new';
|
|
isa_ok $test->{info}, $class, '... and the object it returns';
|
|
|
|
is(defined $test->{info}{init}, 1, 'MIBs initialized');
|
|
ok(exists $test->{info}{mibs}, 'MIBs subclass data structure initialized');
|
|
ok(exists $test->{info}{globals},
|
|
'Globals subclass data structure initialized');
|
|
ok(exists $test->{info}{funcs}, 'Funcs subclass data structure initialized');
|
|
ok(exists $test->{info}{munge}, 'Munge subclass data structure initialized');
|
|
ok(exists $test->{info}{store}, 'Store initialized');
|
|
}
|
|
|
|
sub device_type : Tests(2) {
|
|
my $test = shift;
|
|
my $class = $test->class;
|
|
|
|
can_ok($test->{info}, 'device_type');
|
|
|
|
SKIP: {
|
|
skip "Device type not applicable to $class", 1
|
|
if $class !~ /Layer\d::\w+$/x;
|
|
|
|
# This depends on cache or mocked session data being provided.
|
|
# Recommendation is to extend the existing setup method in the
|
|
# subclass to provide the common data.
|
|
is($test->{info}->device_type(), $class, qq(Device type is $class));
|
|
}
|
|
}
|
|
|
|
sub globals : Tests(2) {
|
|
my $test = shift;
|
|
|
|
can_ok($test->{info}, 'globals');
|
|
|
|
subtest 'Globals validate subtest' => sub {
|
|
|
|
my $test_globals = $test->{info}->globals;
|
|
|
|
if (scalar keys %$test_globals) {
|
|
foreach my $key (sort (keys %$test_globals)) {
|
|
|
|
# Note: was going to use can_ok() as test method, but can() will insert
|
|
# AUTOLOAD methods such as the Globals and Funcs into the symbol table.
|
|
# This causes conflicts with the library inheritance scheme and
|
|
# manifests itself seemingly random test failures
|
|
ok($test->{info}->_validate_autoload_method($key),
|
|
qq('$key' validates));
|
|
}
|
|
}
|
|
else {
|
|
$test->builder->skip("No globals to test");
|
|
}
|
|
};
|
|
}
|
|
|
|
sub funcs : Tests(2) {
|
|
my $test = shift;
|
|
|
|
can_ok($test->{info}, 'funcs');
|
|
|
|
subtest 'Funcs validate subtest' => sub {
|
|
|
|
my $test_funcs = $test->{info}->funcs;
|
|
|
|
if (scalar keys %$test_funcs) {
|
|
foreach my $key (sort (keys %$test_funcs)) {
|
|
|
|
# Note: was going to use can_ok() as test method, but can() will insert
|
|
# AUTOLOAD methods such as the Globals and Funcs into the symbol table.
|
|
# This causes conflicts with the library inheritance scheme and
|
|
# manifests itself seemingly random test failures
|
|
ok($test->{info}->_validate_autoload_method($key),
|
|
qq('$key' validates));
|
|
}
|
|
}
|
|
else {
|
|
$test->builder->skip("No funcs to test");
|
|
}
|
|
};
|
|
}
|
|
|
|
sub mibs : Tests(2) {
|
|
my $test = shift;
|
|
|
|
can_ok($test->{info}, 'mibs');
|
|
|
|
subtest 'MIBs loaded subtest' => sub {
|
|
|
|
my $mibs = $test->{info}->mibs();
|
|
|
|
if (scalar keys %$mibs) {
|
|
foreach my $key (sort(keys %$mibs)) {
|
|
my $qual_name = "$key" . '::' . "$mibs->{$key}";
|
|
ok(defined $SNMP::MIB{$mibs->{$key}}, "$qual_name defined");
|
|
like(SNMP::translateObj($qual_name),
|
|
qr/^(\.\d+)+$/, "$qual_name translates to a OID");
|
|
}
|
|
}
|
|
else {
|
|
$test->builder->skip("No MIBs to test");
|
|
}
|
|
};
|
|
}
|
|
|
|
sub munge : Tests(2) {
|
|
my $test = shift;
|
|
|
|
can_ok($test->{info}, 'munge');
|
|
|
|
subtest 'Munges subtest' => sub {
|
|
|
|
my $test_munges = $test->{info}->munge();
|
|
|
|
if (scalar keys %$test_munges) {
|
|
foreach my $key (sort (keys %$test_munges)) {
|
|
isa_ok($test_munges->{$key}, 'CODE', "$key munge");
|
|
}
|
|
}
|
|
else {
|
|
$test->builder->skip("No munge to test");
|
|
}
|
|
};
|
|
}
|
|
|
|
sub method_coverage : Tests(1) {
|
|
my $test = shift;
|
|
my $class = $test->class;
|
|
my $class_regex = $class . '::';
|
|
my $test_class = "Test::$class";
|
|
my $test_class_regex = $test_class . '::';
|
|
|
|
TODO: {
|
|
# SNMP::Info is returning unexpected methods, investigate at some point
|
|
# SNMP::Info has methods which must be renamed in test classes, i.e.
|
|
# AUTOLOAD, DESTROY, can, etc.
|
|
todo_skip 'Base class', 1 if ($class eq 'SNMP::Info');
|
|
|
|
# Skip if we have marked TODO, methods that need coverage will be
|
|
# identified with verbose output
|
|
local $TODO = "$class method coverage not complete" if $test->todo_methods;
|
|
|
|
my $methods = Class::Inspector->methods($class, 'full', 'public');
|
|
my @local_methods = grep { $_ =~ /^$class/ } @$methods;
|
|
s/$class_regex//x for @local_methods;
|
|
|
|
my $test_methods = Class::Inspector->methods($test_class, 'full', 'public');
|
|
my @local_test_methods = grep { $_ =~ /^$test_class/x } @$test_methods;
|
|
s/$test_class_regex//x for @local_test_methods;
|
|
|
|
cmp_deeply(
|
|
\@local_methods,
|
|
subsetof(@local_test_methods),
|
|
qq(All public $class methods have coverage)
|
|
);
|
|
}
|
|
}
|
|
|
|
#
|
|
# Utility methods / functions
|
|
#
|
|
|
|
sub create_mock_session {
|
|
|
|
my $home = dir($ENV{HOME}, 'netdisco-mibs');
|
|
|
|
local $ENV{'SNMPCONFPATH'} = $EMPTY;
|
|
local $ENV{'MIBDIRS'} = $EMPTY;
|
|
local $ENV{'MIBS'} = $EMPTY;
|
|
local $ENV{'SNMP_PERSISTENT_DIR'} = $home;
|
|
|
|
SNMP::initMib();
|
|
|
|
my @mibdirs = _build_mibdirs();
|
|
|
|
foreach my $d (@mibdirs) {
|
|
next unless -d $d;
|
|
SNMP::addMibDirs($d);
|
|
}
|
|
|
|
my $session = SNMP::Session->new(
|
|
UseEnums => 1,
|
|
RetryNoSuch => 1,
|
|
DestHost => '127.0.0.1',
|
|
Community => 'public',
|
|
Version => 2,
|
|
|
|
# Hold simulated data for mock sessions
|
|
Data => {},
|
|
);
|
|
|
|
my $mock_session = Test::MockObject::Extends->new($session);
|
|
|
|
mock_get($mock_session);
|
|
mock_getnext($mock_session);
|
|
mock_set($mock_session);
|
|
|
|
return $mock_session;
|
|
}
|
|
|
|
sub _build_mibdirs {
|
|
my $home = dir($ENV{HOME}, 'netdisco-mibs');
|
|
return map { dir($home, $_)->stringify } @{_get_mibdirs_content($home)};
|
|
}
|
|
|
|
sub _get_mibdirs_content {
|
|
my $home = shift;
|
|
my @list
|
|
= map { s|$home/||; $_ } grep {m/[a-z0-9]/} grep {-d} glob("$home/*");
|
|
return \@list;
|
|
}
|
|
|
|
sub mock_get {
|
|
my $mock_session = shift;
|
|
|
|
$mock_session->mock(
|
|
'get',
|
|
sub {
|
|
my $self = shift;
|
|
my $vars = shift;
|
|
my ($leaf, $iid, $oid, $oid_name);
|
|
my $c_data = $self->{Data};
|
|
|
|
# From SNMP::Info get will only be passed either an OID or
|
|
# SNMP::Varbind with a fully qualified leaf and potentially
|
|
# a partial
|
|
if (ref($vars) =~ /SNMP::Varbind/x) {
|
|
($leaf, $iid) = @{$vars};
|
|
}
|
|
else {
|
|
$oid = $vars;
|
|
$oid_name = SNMP::translateObj($oid, 0, 1) || $EMPTY;
|
|
($leaf, $iid) = $oid_name =~ /^(\S+::\w+)[.]?(\S+)*$/x;
|
|
}
|
|
|
|
# This is a lot of indirection, but we need the base OID, it may be
|
|
# passed with a zero for non table leaf
|
|
my $oid_base = SNMP::translateObj($leaf);
|
|
|
|
$iid ||= 0;
|
|
my $new_iid = $iid;
|
|
my $val = $EMPTY;
|
|
my $data = $c_data->{$leaf} || $c_data->{$oid_base} || {};
|
|
my $count = scalar keys %{$data} || 0;
|
|
if ($count > 1) {
|
|
my $found = 0;
|
|
foreach my $d_iid (sort keys %{$data}) {
|
|
if ($d_iid eq $iid) {
|
|
$val = $data->{$d_iid};
|
|
$found = 1;
|
|
next;
|
|
}
|
|
elsif ($found == 1) {
|
|
$new_iid = $d_iid;
|
|
last;
|
|
}
|
|
}
|
|
if ($found && ($new_iid eq $iid)) {
|
|
$leaf = 'unknown';
|
|
}
|
|
}
|
|
else {
|
|
$val = $data->{$iid};
|
|
$leaf = 'unknown';
|
|
}
|
|
|
|
if (ref $vars =~ /SNMP::Varbind/x) {
|
|
$vars->[0] = $leaf;
|
|
$vars->[1] = $new_iid;
|
|
$vars->[2] = $val;
|
|
}
|
|
return (wantarray() ? $vars : $val);
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
sub mock_getnext {
|
|
my $mock_session = shift;
|
|
|
|
$mock_session->mock(
|
|
'getnext',
|
|
sub {
|
|
my $self = shift;
|
|
my $vars = shift;
|
|
my ($leaf, $iid, $oid, $oid_name);
|
|
my $c_data = $self->{Data};
|
|
|
|
# From SNMP::Info getnext will only be passed a SNMP::Varbind
|
|
# with a fully qualified leaf and potentially a partial
|
|
($leaf, $iid) = @{$vars};
|
|
|
|
# If we captured data using OIDs printed numerically -On option,
|
|
# we need to convert the leaf to an OID for match
|
|
my $leaf_oid = SNMP::translateObj($leaf, 0, 1) || '';
|
|
|
|
unless (defined $iid) {
|
|
$iid = -1;
|
|
}
|
|
my $new_iid = $iid;
|
|
my $val = $EMPTY;
|
|
my $data = $c_data->{$leaf} || $c_data->{$leaf_oid};
|
|
my $count = scalar keys %{$data} || 0;
|
|
if ($count) {
|
|
my $found = 0;
|
|
foreach my $d_iid (sort keys %{$data}) {
|
|
if ($d_iid gt $iid && !$found) {
|
|
$val = $data->{$d_iid};
|
|
$new_iid = $d_iid;
|
|
$found = 1;
|
|
next;
|
|
}
|
|
elsif ($found == 1) {
|
|
last;
|
|
}
|
|
}
|
|
if ($found && ($new_iid eq $iid)) {
|
|
$leaf = 'unknown';
|
|
}
|
|
}
|
|
else {
|
|
$val = $data->{$iid};
|
|
$leaf = 'unknown';
|
|
}
|
|
|
|
$vars->[0] = $leaf;
|
|
$vars->[1] = $new_iid;
|
|
$vars->[2] = $val;
|
|
return (wantarray() ? $vars : $val);
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
# For testing purposes assume sets worked
|
|
sub mock_set {
|
|
my $mock_session = shift;
|
|
|
|
$mock_session->mock(
|
|
'set',
|
|
sub {
|
|
return 1;
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
# Utility to load snmpwalk from a file to use for mock sessions
|
|
sub load_snmpdata {
|
|
my $test = shift;
|
|
my $data_file = shift;
|
|
|
|
my @lines = read_lines($data_file);
|
|
|
|
my $snmp_data = {};
|
|
foreach my $line (@lines) {
|
|
next if !$line;
|
|
next if ($line =~ /^#/);
|
|
if ($line =~ /^(\S+::\w+)[.]?(\S+)*\s=\s(.*)$/x) {
|
|
my ($leaf, $iid, $val) = ($1, $2, $3);
|
|
next if !$leaf;
|
|
$iid ||= 0;
|
|
$val =~ s/\"//g;
|
|
$snmp_data->{$leaf}->{$iid} = $val;
|
|
}
|
|
}
|
|
return $snmp_data;
|
|
}
|
|
|
|
# Returns 1 if the method is defined in the symbol table 0 otherwise, used for
|
|
# verification that dynamic methods via AUTOLOAD and can() have been inserted
|
|
# into the symbol table
|
|
sub symbol_test {
|
|
my $test = shift;
|
|
my $method = shift;
|
|
|
|
my $class = $test->class;
|
|
my %symbols = ();
|
|
{
|
|
no strict 'refs'; ## no critic (ProhibitNoStrict)
|
|
%symbols = %{$class . '::'};
|
|
}
|
|
return (defined($symbols{$method}) ? 1 : 0);
|
|
}
|
|
|
|
1;
|