4 # Helper Functions for testing Nagios Plugins
9 @EXPORT = qw(getTestParameter checkCmd skipMissingCmd);
10 @EXPORT_OK = qw(DetermineTestHarnessDirectory TestsFrom SetCacheFilename);
23 use vars
qw($VERSION);
24 $VERSION = do { my @r = (q$Revision$ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker
28 NPTest - Simplify the testing of Nagios Plugins
32 This modules provides convenience functions to assist in the testing
33 of Nagios Plugins, making the testing code easier to read and write;
34 hopefully encouraging the development of a more complete test suite for
35 the Nagios 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
41 This module defines three public functions, C<getTestParameter(...)>,
42 C<checkCmd(...)> and C<skipMissingCmd(...)>. These are exported by
43 default via the C<use NPTest;> statement.
47 =item getTestParameter( "ENV_VARIABLE", $brief_description, $default )
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), this routine will die causing the test to
60 Responses are stored in an external, file-based
61 cache so subsequent test runs will use these values. The user is able
62 to change the values by amending the values in the file /var/tmp/NPTest.pm,
63 or by setting 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 Nagios 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
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
113 HASHREF(integer,string)
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(...)>
135 Many Nagios 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.
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?"
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.
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
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.
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
192 The rest of the code, as I have only commented on the major public
193 functions that test harness writers will use, not all the code present
194 in this helper module.
198 Copyright (c) 2005 Peter Bray. All rights reserved.
200 This package is free software and is provided "as is" without express
201 or implied warranty. It may be used, redistributed and/or modified
202 under the same terms as the Nagios Plugins release.
207 # Package Scope Variables
212 # I'm not really sure wether to house a site-specific cache inside
213 # or outside of the extracted source / build tree - lets default to outside
214 my( $CACHEFILENAME ) = ( exists( $ENV{'NPTEST_CACHE'} ) && $ENV{'NPTEST_CACHE'} )
215 ? $ENV{'NPTEST_CACHE'} : "/var/tmp/NPTest.cache"; # "../Cache.pdd";
223 my( $command, $desiredExitStatus, $desiredOutput, %exceptions ) = @_;
225 my $result = NPTest->testCmd($command);
227 my $output = $result->output;
228 my $exitStatus = $result->return_code;
230 $output = "" unless defined( $output );
235 my $testOutput = "continue";
237 if ( defined( $desiredExitStatus ) )
239 if ( ref $desiredExitStatus eq "ARRAY" )
241 if ( scalar( grep { $_ == $exitStatus } @{$desiredExitStatus} ) )
243 $desiredExitStatus = $exitStatus;
247 $desiredExitStatus = -1;
250 elsif ( ref $desiredExitStatus eq "HASH" )
252 if ( exists( ${$desiredExitStatus}{$exitStatus} ) )
254 if ( defined( ${$desiredExitStatus}{$exitStatus} ) )
256 $testOutput = ${$desiredExitStatus}{$exitStatus};
258 $desiredExitStatus = $exitStatus;
262 $desiredExitStatus = -1;
266 if ( %exceptions && exists( $exceptions{$exitStatus} ) )
268 $testStatus += skip( $exceptions{$exitStatus}, $exitStatus, $desiredExitStatus );
269 $testOutput = "skip";
273 $testStatus += ok( $exitStatus, $desiredExitStatus );
277 if ( defined( $desiredOutput ) )
279 if ( $testOutput ne "skip" )
281 $testStatus += ok( $output, $desiredOutput );
285 $testStatus += skip( "Skipping output test as requested", $output, $desiredOutput );
295 my( $command, $count ) = @_;
301 $testStatus += skip( "Missing ${command} - tests skipped", 1 );
309 my( $param, $envvar, $default, $brief, $scoped );
311 if (scalar @_ <= 3) {
312 ($param, $brief, $default) = @_;
316 ( $param, $envvar, $default, $brief, $scoped ) = @_;
320 # Apply default values for optional arguments
321 $scoped = ( defined( $scoped ) && $scoped );
323 my $testharness = basename( (caller(0))[1], ".t" ); # used for scoping
325 if ( defined( $envvar ) && exists( $ENV{$envvar} ) && $ENV{$envvar} )
327 return $ENV{$envvar};
330 my $cachedValue = SearchCache( $param, $testharness );
331 if ( defined( $cachedValue ) )
333 # This save required to convert to new style because the key required is
334 # changing to the environment variable
335 if ($new_style == 0) {
336 SetCacheParameter( $envvar, undef, $cachedValue );
341 my $defaultValid = ( defined( $default ) && $default );
342 my $autoAcceptDefault = ( exists( $ENV{'NPTEST_ACCEPTDEFAULT'} ) && $ENV{'NPTEST_ACCEPTDEFAULT'} );
344 if ( $autoAcceptDefault && $defaultValid )
349 die "Need to manually enter test parameter $param" unless (-t STDERR);
351 my $userResponse = "";
353 while ( $userResponse eq "" )
356 print STDERR "Test Harness : $testharness\n";
357 print STDERR "Test Parameter : $param\n";
358 print STDERR "Environment Variable : $envvar\n" if ($param ne $envvar);
359 print STDERR "Brief Description : $brief\n";
360 print STDERR "Enter value (or 'none') ", ($defaultValid ? "[${default}]" : "[]"), " => ";
361 $userResponse = <STDIN>;
362 $userResponse = "" if ! defined( $userResponse ); # Handle EOF
363 chomp( $userResponse );
364 if ( $defaultValid && $userResponse eq "" )
366 $userResponse = $default;
372 if ($userResponse =~ /^(na|none)$/) {
376 # define all user responses at global scope
377 SetCacheParameter( $param, ( $scoped ? $testharness : undef ), $userResponse );
379 return $userResponse;
383 # Internal Cache Management Functions
388 my( $param, $scope ) = @_;
392 if ( exists( $CACHE{$scope} ) && exists( $CACHE{$scope}{$param} ) )
394 return $CACHE{$scope}{$param};
397 if ( exists( $CACHE{$param} ) )
399 return $CACHE{$param};
401 return undef; # Need this to say "nothing found"
404 sub SetCacheParameter
406 my( $param, $scope, $value ) = @_;
408 if ( defined( $scope ) )
410 $CACHE{$scope}{$param} = $value;
414 $CACHE{$param} = $value;
422 return if exists( $CACHE{'_cache_loaded_'} );
424 if ( -f $CACHEFILENAME )
426 my( $fileHandle ) = new IO::File;
428 if ( ! $fileHandle->open( "< ${CACHEFILENAME}" ) )
430 print STDERR "NPTest::LoadCache() : Problem opening ${CACHEFILENAME} : $!\n";
434 my( $fileContents ) = join( "\n", <$fileHandle> );
436 $fileHandle->close();
438 my( $contentsRef ) = eval $fileContents;
439 %CACHE = %{$contentsRef};
443 $CACHE{'_cache_loaded_'} = 1;
449 delete $CACHE{'_cache_loaded_'};
451 my( $fileHandle ) = new IO::File;
453 if ( ! $fileHandle->open( "> ${CACHEFILENAME}" ) )
455 print STDERR "NPTest::LoadCache() : Problem saving ${CACHEFILENAME} : $!\n";
459 my( $dataDumper ) = new Data::Dumper( [ \%CACHE ] );
461 $dataDumper->Terse(1);
463 print $fileHandle $dataDumper->Dump();
465 $fileHandle->close();
467 $CACHE{'_cache_loaded_'} = 1;
471 # (Questionable) Public Cache Management Functions
476 my( $filename ) = @_;
478 # Unfortunately we can not validate the filename
479 # in any meaningful way, as it may not yet exist
480 $CACHEFILENAME = $filename;
485 # Test Harness Wrapper Functions
488 sub DetermineTestHarnessDirectory
490 my( $userSupplied ) = @_;
493 if ( defined( $userSupplied ) && $userSupplied )
495 if ( -d $userSupplied )
497 return $userSupplied;
501 return undef; # userSupplied is invalid -> FAIL
505 # Simple Case : "t" is a subdirectory of the current directory
511 # To be honest I don't understand which case satisfies the
512 # original code in test.pl : when $tstdir == `pwd` w.r.t.
513 # $tstdir =~ s|^(.*)/([^/]+)/?$|$1/$2|; and if (-d "../../$2/t")
514 # Assuming pwd is "/a/b/c/d/e" then we are testing for "/a/b/c/e/t"
515 # if I understand the code correctly (a big assumption)
517 # Simple Case : the current directory is "t"
520 if ( $pwd =~ m|/t$| )
524 # The alternate that might work better is
527 # As the current test harnesses assume the application
528 # to be tested is in the current directory (ie "./check_disk ....")
536 my( $directory, $excludeIfAppMissing ) = @_;
538 $excludeIfAppMissing = 0 unless defined( $excludeIfAppMissing );
540 if ( ! opendir( DIR, $directory ) )
542 print STDERR "NPTest::TestsFrom() - Failed to open ${directory} : $!\n";
551 while ( $filename = readdir( DIR ) )
553 if ( $filename =~ m/\.t$/ )
555 if ( $excludeIfAppMissing )
557 $application = basename( $filename, ".t" );
558 if ( ! -e $application )
560 print STDERR "No application (${application}) found for test harness (${filename})\n";
564 push @tests, "${directory}/${filename}";
573 # All the new object oriented stuff below
578 return bless $self, $type;
585 return $self->{return_code} = shift;
587 return $self->{return_code};
593 return $self->{output} = shift;
595 return $self->{output};
601 $_ = $self->{output};
608 my $command = shift or die "No command passed to testCmd";
609 my $object = $class->new;
611 my $output = `$command`;
612 $object->return_code($? >> 8);
615 die "Got signal $_ for command $command";
618 $object->output($output);
620 if ($ENV{'NPTEST_DEBUG'}) {
621 my ($pkg, $file, $line) = caller(0);
622 print "testCmd: Called from line $line in $file", $/;
623 print "Testing: $command", $/;
624 print "Output: ", $object->output, $/;
625 print "Return code: ", $object->return_code, $/;