configure.ac: Change RADIUS library preferences
[monitoring-plugins.git] / NPTest.pm
blobf72ed2df3e5b22d83b5f9fdbc4b2ddd049d9f672
1 package NPTest;
4 # Helper Functions for testing Monitoring Plugins
7 require Exporter;
8 @ISA = qw(Exporter);
9 @EXPORT = qw(getTestParameter checkCmd skipMissingCmd skipMsg);
10 @EXPORT_OK = qw(DetermineTestHarnessDirectory TestsFrom SetCacheFilename);
12 use strict;
13 use warnings;
15 use Cwd;
16 use File::Basename;
18 use IO::File;
19 use Data::Dumper;
21 use Test;
23 use vars qw($VERSION);
24 $VERSION = "1556."; # must be all one line, for MakeMaker
26 =head1 NAME
28 NPTest - Simplify the testing of Monitoring Plugins
30 =head1 DESCRIPTION
32 This modules provides convenience functions to assist in the testing
33 of Monitoring Plugins, making the testing code easier to read and write;
34 hopefully encouraging the development of a more complete test suite for
35 the Monitoring Plugins. It is based on the patterns of testing seen in the
36 1.4.0 release, and continues to use the L<Test> module as the basis of
37 testing.
39 =head1 FUNCTIONS
41 This module defines four public functions, C<getTestParameter(...)>,
42 C<checkCmd(...)>, C<skipMissingCmd(...)> and C<skipMsg(...)>. These are exported by
43 default via the C<use NPTest;> statement.
45 =over
47 =item getTestParameter( "ENV_VARIABLE", $brief_description, $default )
49 $default is optional.
51 This function allows the test harness
52 developer to interactively request test parameter information from the
53 user. The user can accept the developer's default value or reply "none"
54 which will then be returned as "" for the test to skip if appropriate.
56 If a parameter needs to be entered and the test is run without a tty
57 attached (such as a cronjob), the parameter will be assigned as if it
58 was "none". Tests can check for the parameter and skip if not set.
60 Responses are stored in an external, file-based cache so subsequent test
61 runs will use these values. The user is able to change the values by
62 amending the values in the file /var/tmp/NPTest.cache, or by setting
63 the appropriate environment variable before running the test.
65 The option exists to store parameters in a scoped means, allowing a
66 test harness to a localise a parameter should the need arise. This
67 allows a parameter of the same name to exist in a test harness
68 specific scope, while not affecting the globally scoped parameter. The
69 scoping identifier is the name of the test harness sans the trailing
70 ".t". All cache searches first look to a scoped parameter before
71 looking for the parameter at global scope. Thus for a test harness
72 called "check_disk.t" requesting the parameter "mountpoint_valid", the
73 cache is first searched for "check_disk"/"mountpoint_valid", if this
74 fails, then a search is conducted for "mountpoint_valid".
76 To facilitate quick testing setup, it is possible to accept all the
77 developer provided defaults by setting the environment variable
78 "NPTEST_ACCEPTDEFAULT" to "1" (or any other perl truth value). Note
79 that, such defaults are not stored in the cache, as there is currently
80 no mechanism to edit existing cache entries, save the use of text
81 editor or removing the cache file completely.
83 =item C<testCmd($command)>
85 Call with NPTest->testCmd("./check_disk ...."). This returns a NPTest object
86 which you can then run $object->return_code or $object->output against.
88 Testing of results would be done in your test script, not in this module.
90 =item C<checkCmd(...)>
92 This function is obsolete. Use C<testCmd()> instead.
94 This function attempts to encompass the majority of test styles used
95 in testing Monitoring Plugins. As each plug-in is a separate command, the
96 typical tests we wish to perform are against the exit status of the
97 command and the output (if any) it generated. Simplifying these tests
98 into a single function call, makes the test harness easier to read and
99 maintain and allows additional functionality (such as debugging) to be
100 provided without additional effort on the part of the test harness
101 developer.
103 It is possible to enable debugging via the environment variable
104 C<NPTEST_DEBUG>. If this environment variable exists and its value in PERL's
105 boolean context evaluates to true, debugging is enabled.
107 The function prototype can be expressed as follows:
109 Parameter 1 : command => DEFINED SCALAR(string)
110 Parameter 2 : desiredExitStatus => ONE OF
111 SCALAR(integer)
112 ARRAYREF(integer)
113 HASHREF(integer,string)
114 UNDEFINED
115 Parameter 3 : desiredOutput => SCALAR(string) OR UNDEFINED
116 Parameter 4 : exceptions => HASH(integer,string) OR UNDEFINED
117 Returns : SCALAR(integer) as defined by Test::ok(...)
119 The function treats the first parameter C<$command> as a command line
120 to execute as part of the test, it is executed only once and its exit
121 status (C<$?E<gt>E<gt>8>) and output are captured.
123 At this point if debugging is enabled the command, its exit status and
124 output are displayed to the tester.
126 C<checkCmd(...)> allows the testing of either the exit status or the
127 generated output or both, not testing either will result in neither
128 the C<Test::ok(...)> or C<Test::skip(...)> functions being called,
129 something you probably don't want. Note that each defined test
130 (C<$desiredExitStatus> and C<$desiredOutput>) results in a invocation
131 of either C<Test::ok(...)> or C<Test::skip(...)>, so remember this
132 when counting the number of tests to place in the C<Test::plan(...)>
133 call.
135 Many Monitoring Plugins test network services, some of which may not be
136 present on all systems. To cater for this, C<checkCmd(...)> allows the
137 tester to define exceptions based on the command's exit status. These
138 exceptions are provided to skip tests if the test case developer
139 believes the service is not being provided. For example, if a site
140 does not have a POP3 server, the test harness could map the
141 appropriate exit status to a useful message the person running the
142 tests, telling the reason the test is being skipped.
144 Example:
146 my %exceptions = ( 2 =E<gt> "No POP Server present?" );
148 $t += checkCmd( "./check_pop I<some args>", 0, undef, %exceptions );
150 Thus, in the above example, an exit status of 2 does not result in a
151 failed test case (as the exit status is not the desired value of 0),
152 but a skipped test case with the message "No POP Server present?"
153 given as the reason.
155 Sometimes the exit status of a command should be tested against a set
156 of possible values, rather than a single value, this could especially
157 be the case in failure testing. C<checkCmd(...)> support two methods
158 of testing against a set of desired exit status values.
160 =over
162 =item *
164 Firstly, if C<$desiredExitStatus> is a reference to an array of exit
165 stati, if the actual exit status of the command is present in the
166 array, it is used in the call to C<Test::ok(...)> when testing the
167 exit status.
169 =item *
171 Alternatively, if C<$desiredExitStatus> is a reference to a hash of
172 exit stati (mapped to the strings "continue" or "skip"), similar
173 processing to the above occurs with the side affect of determining if
174 any generated output testing should proceed. Note: only the string
175 "skip" will result in generated output testing being skipped.
177 =back
179 =item C<skipMissingCmd(...)>
181 If a command is missing and the test harness must C<Test::skip()> some
182 or all of the tests in a given test harness this function provides a
183 simple iterator to issue an appropriate message the requested number
184 of times.
186 =back
188 =item C<skipMsg(...)>
190 If for any reason the test harness must C<Test::skip()> some
191 or all of the tests in a given test harness this function provides a
192 simple iterator to issue an appropriate message the requested number
193 of times.
195 =back
197 =head1 SEE ALSO
199 L<Test>
201 The rest of the code, as I have only commented on the major public
202 functions that test harness writers will use, not all the code present
203 in this helper module.
205 =head1 AUTHOR
207 Copyright (c) 2005 Peter Bray. All rights reserved.
209 This package is free software and is provided "as is" without express
210 or implied warranty. It may be used, redistributed and/or modified
211 under the same terms as the Monitoring Plugins release.
213 =cut
216 # Package Scope Variables
219 my( %CACHE ) = ();
221 # I'm not really sure wether to house a site-specific cache inside
222 # or outside of the extracted source / build tree - lets default to outside
223 my( $CACHEFILENAME ) = ( exists( $ENV{'NPTEST_CACHE'} ) && $ENV{'NPTEST_CACHE'} )
224 ? $ENV{'NPTEST_CACHE'} : "/var/tmp/NPTest.cache"; # "../Cache.pdd";
227 # Testing Functions
230 sub checkCmd
232 my( $command, $desiredExitStatus, $desiredOutput, %exceptions ) = @_;
234 my $result = NPTest->testCmd($command);
236 my $output = $result->output;
237 my $exitStatus = $result->return_code;
239 $output = "" unless defined( $output );
240 chomp( $output );
242 my $testStatus;
244 my $testOutput = "continue";
246 if ( defined( $desiredExitStatus ) )
248 if ( ref $desiredExitStatus eq "ARRAY" )
250 if ( scalar( grep { $_ == $exitStatus } @{$desiredExitStatus} ) )
252 $desiredExitStatus = $exitStatus;
254 else
256 $desiredExitStatus = -1;
259 elsif ( ref $desiredExitStatus eq "HASH" )
261 if ( exists( ${$desiredExitStatus}{$exitStatus} ) )
263 if ( defined( ${$desiredExitStatus}{$exitStatus} ) )
265 $testOutput = ${$desiredExitStatus}{$exitStatus};
267 $desiredExitStatus = $exitStatus;
269 else
271 $desiredExitStatus = -1;
275 if ( %exceptions && exists( $exceptions{$exitStatus} ) )
277 $testStatus += skip( $exceptions{$exitStatus}, $exitStatus, $desiredExitStatus );
278 $testOutput = "skip";
280 else
282 $testStatus += ok( $exitStatus, $desiredExitStatus );
286 if ( defined( $desiredOutput ) )
288 if ( $testOutput ne "skip" )
290 $testStatus += ok( $output, $desiredOutput );
292 else
294 $testStatus += skip( "Skipping output test as requested", $output, $desiredOutput );
298 return $testStatus;
302 sub skipMissingCmd
304 my( $command, $count ) = @_;
306 my $testStatus;
308 for ( 1 .. $count )
310 $testStatus += skip( "Missing ${command} - tests skipped", 1 );
313 return $testStatus;
316 sub skipMsg
318 my( $msg, $count ) = @_;
320 my $testStatus;
322 for ( 1 .. $count )
324 $testStatus += skip( $msg, 1 );
327 return $testStatus;
330 sub getTestParameter
332 my( $param, $envvar, $default, $brief, $scoped );
333 my $new_style;
334 if (scalar @_ <= 3) {
335 ($param, $brief, $default) = @_;
336 $envvar = $param;
337 $new_style = 1;
338 } else {
339 ( $param, $envvar, $default, $brief, $scoped ) = @_;
340 $new_style = 0;
343 # Apply default values for optional arguments
344 $scoped = ( defined( $scoped ) && $scoped );
346 my $testharness = basename( (caller(0))[1], ".t" ); # used for scoping
348 if ( defined( $envvar ) && exists( $ENV{$envvar} ) && $ENV{$envvar} )
350 return $ENV{$envvar};
353 my $cachedValue = SearchCache( $param, $testharness );
354 if ( defined( $cachedValue ) )
356 # This save required to convert to new style because the key required is
357 # changing to the environment variable
358 if ($new_style == 0) {
359 SetCacheParameter( $envvar, undef, $cachedValue );
361 return $cachedValue;
364 my $defaultValid = ( defined( $default ) && $default );
365 my $autoAcceptDefault = ( exists( $ENV{'NPTEST_ACCEPTDEFAULT'} ) && $ENV{'NPTEST_ACCEPTDEFAULT'} );
367 if ( $autoAcceptDefault && $defaultValid )
369 return $default;
372 # Set "none" if no terminal attached (eg, tinderbox build servers when new variables set)
373 return "" unless (-t STDIN);
375 my $userResponse = "";
377 while ( $userResponse eq "" )
379 print STDERR "\n";
380 print STDERR "Test Harness : $testharness\n";
381 print STDERR "Test Parameter : $param\n";
382 print STDERR "Environment Variable : $envvar\n" if ($param ne $envvar);
383 print STDERR "Brief Description : $brief\n";
384 print STDERR "Enter value (or 'none') ", ($defaultValid ? "[${default}]" : "[]"), " => ";
385 $userResponse = <STDIN>;
386 $userResponse = "" if ! defined( $userResponse ); # Handle EOF
387 chomp( $userResponse );
388 if ( $defaultValid && $userResponse eq "" )
390 $userResponse = $default;
394 print STDERR "\n";
396 if ($userResponse =~ /^(na|none)$/) {
397 $userResponse = "";
400 # define all user responses at global scope
401 SetCacheParameter( $param, ( $scoped ? $testharness : undef ), $userResponse );
403 return $userResponse;
407 # Internal Cache Management Functions
410 sub SearchCache
412 my( $param, $scope ) = @_;
414 LoadCache();
416 if ( exists( $CACHE{$scope} ) && exists( $CACHE{$scope}{$param} ) )
418 return $CACHE{$scope}{$param};
421 if ( exists( $CACHE{$param} ) )
423 return $CACHE{$param};
425 return undef; # Need this to say "nothing found"
428 sub SetCacheParameter
430 my( $param, $scope, $value ) = @_;
432 if ( defined( $scope ) )
434 $CACHE{$scope}{$param} = $value;
436 else
438 $CACHE{$param} = $value;
441 SaveCache();
444 sub LoadCache
446 return if exists( $CACHE{'_cache_loaded_'} );
448 my $fileContents = "";
449 if ( -f $CACHEFILENAME )
451 my( $fileHandle ) = new IO::File;
453 if ( ! $fileHandle->open( "< ${CACHEFILENAME}" ) )
455 print STDERR "NPTest::LoadCache() : Problem opening ${CACHEFILENAME} : $!\n";
456 return;
459 $fileContents = join("", <$fileHandle>);
460 $fileHandle->close();
462 chomp($fileContents);
463 my( $contentsRef ) = eval $fileContents;
464 %CACHE = %{$contentsRef} if (defined($contentsRef));
468 $CACHE{'_cache_loaded_'} = 1;
469 $CACHE{'_original_cache'} = $fileContents;
473 sub SaveCache
475 delete $CACHE{'_cache_loaded_'};
476 my $oldFileContents = delete $CACHE{'_original_cache'};
478 my($dataDumper) = new Data::Dumper([\%CACHE]);
479 $dataDumper->Terse(1);
480 $dataDumper->Sortkeys(1);
481 my $data = $dataDumper->Dump();
482 $data =~ s/^\s+/ /gmx; # make sure all systems use same amount of whitespace
483 $data =~ s/^\s+}/}/gmx;
484 chomp($data);
486 if($oldFileContents ne $data) {
487 my($fileHandle) = new IO::File;
488 if (!$fileHandle->open( "> ${CACHEFILENAME}")) {
489 print STDERR "NPTest::LoadCache() : Problem saving ${CACHEFILENAME} : $!\n";
490 return;
492 print $fileHandle $data;
493 $fileHandle->close();
496 $CACHE{'_cache_loaded_'} = 1;
497 $CACHE{'_original_cache'} = $data;
501 # (Questionable) Public Cache Management Functions
504 sub SetCacheFilename
506 my( $filename ) = @_;
508 # Unfortunately we can not validate the filename
509 # in any meaningful way, as it may not yet exist
510 $CACHEFILENAME = $filename;
515 # Test Harness Wrapper Functions
518 sub DetermineTestHarnessDirectory
520 my( @userSupplied ) = @_;
521 my @dirs;
523 # User Supplied
524 if ( @userSupplied > 0 )
526 for my $u ( @userSupplied )
528 if ( -d $u )
530 push ( @dirs, $u );
535 # Simple Cases: "t" and tests are subdirectories of the current directory
536 if ( -d "./t" )
538 push ( @dirs, "./t");
540 if ( -d "./tests" )
542 push ( @dirs, "./tests");
545 if ( @dirs > 0 )
547 return @dirs;
550 # To be honest I don't understand which case satisfies the
551 # original code in test.pl : when $tstdir == `pwd` w.r.t.
552 # $tstdir =~ s|^(.*)/([^/]+)/?$|$1/$2|; and if (-d "../../$2/t")
553 # Assuming pwd is "/a/b/c/d/e" then we are testing for "/a/b/c/e/t"
554 # if I understand the code correctly (a big assumption)
556 # Simple Case : the current directory is "t"
557 my $pwd = cwd();
559 if ( $pwd =~ m|/t$| )
561 push ( @dirs, $pwd );
563 # The alternate that might work better is
564 # chdir( ".." );
565 # return "./t";
566 # As the current test harnesses assume the application
567 # to be tested is in the current directory (ie "./check_disk ....")
570 return @dirs;
573 sub TestsFrom
575 my( $directory, $excludeIfAppMissing ) = @_;
577 $excludeIfAppMissing = 0 unless defined( $excludeIfAppMissing );
579 if ( ! opendir( DIR, $directory ) )
581 print STDERR "NPTest::TestsFrom() - Failed to open ${directory} : $!\n";
582 return ();
585 my( @tests ) = ();
587 my $filename;
588 my $application;
590 while ( $filename = readdir( DIR ) )
592 if ( $filename =~ m/\.t$/ )
594 if ( $excludeIfAppMissing )
596 $application = basename( $filename, ".t" );
597 if ( ! -e $application and ! -e $application.'.pm' )
599 print STDERR "No application (${application}) found for test harness (${filename})\n";
600 next;
603 push @tests, "${directory}/${filename}";
607 closedir( DIR );
609 return sort @tests;
612 # All the new object oriented stuff below
614 sub new {
615 my $type = shift;
616 my $self = {};
617 return bless $self, $type;
620 # Accessors
621 sub return_code {
622 my $self = shift;
623 if (@_) {
624 return $self->{return_code} = shift;
625 } else {
626 return $self->{return_code};
629 sub output {
630 my $self = shift;
631 if (@_) {
632 return $self->{output} = shift;
633 } else {
634 return $self->{output};
638 sub perf_output {
639 my $self = shift;
640 $_ = $self->{output};
641 /\|(.*)$/;
642 return $1 || "";
645 sub only_output {
646 my $self = shift;
647 $_ = $self->{output};
648 /(.*?)\|/;
649 return $1 || "";
652 sub testCmd {
653 my $class = shift;
654 my $command = shift or die "No command passed to testCmd";
655 my $timeout = shift || 120;
656 my $object = $class->new;
658 local $SIG{'ALRM'} = sub { die("timeout in command: $command"); };
659 alarm($timeout); # no test should take longer than 120 seconds
661 my $output = `$command`;
662 $object->return_code($? >> 8);
663 $_ = $? & 127;
664 if ($_) {
665 die "Got signal $_ for command $command";
667 chomp $output;
668 $object->output($output);
670 alarm(0);
672 my ($pkg, $file, $line) = caller(0);
673 print "Testing: $command", $/;
674 if ($ENV{'NPTEST_DEBUG'}) {
675 print "testCmd: Called from line $line in $file", $/;
676 print "Output: ", $object->output, $/;
677 print "Return code: ", $object->return_code, $/;
680 return $object;
683 # do we have ipv6
684 sub has_ipv6 {
685 # assume ipv6 if a ping6 to labs.consol.de works
686 `ping6 -c 1 2a03:3680:0:2::21 2>&1`;
687 if($? == 0) {
688 return 1;
690 return;
695 # End of File