Code

Create a permanent link to HEAD snapshot
[nagiosplug.git] / NPTest.pm
1 package NPTest;
3 #
4 # Helper Functions for testing Nagios Plugins
5 #
7 require Exporter;
8 @ISA       = qw(Exporter);
9 @EXPORT    = qw(getTestParameter checkCmd skipMissingCmd);
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 = do { my @r = (q$Revision$ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker
26 =head1 NAME
28 NPTest - Simplify the testing of Nagios Plugins
30 =head1 DESCRIPTION
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
37 testing.
39 =head1 FUNCTIONS
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.
45 =over
47 =item C<getTestParameter(...)>
49 A flexible and user override-able method of collecting, storing and
50 retrieving test parameters. This function allows the test harness
51 developer to interactively request test parameter information from the
52 user, when the no means of obtaining the information automatically has
53 been successful. The user is provided with the option of accepting
54 test harness developer's default value for the parameter, if a suggested
55 default is provided.
57 User supplied responses are stored in an external (file-based)
58 cache. These values are retrieved on subsequent runs alleviating the
59 user of reconfirming the previous entered responses. The user is able
60 to override the value of a parameter on any given run by setting the
61 associated environment variable. These environment variable based
62 overrides are not stored in the cache, allowing one-time and what-if
63 based tests on the command line without polluting the cache.
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 The 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
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 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.
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 =head1 SEE ALSO
190 L<Test>
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.
196 =head1 AUTHOR
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.
204 =cut
207 # Package Scope Variables
210 my( %CACHE ) = ();
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";
218 # Testing Functions
221 sub checkCmd
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 );
231   chomp( $output );
233   my $testStatus;
235   my $testOutput = "continue";
237   if ( defined( $desiredExitStatus ) )
238   {
239     if ( ref $desiredExitStatus eq "ARRAY" )
240     {
241       if ( scalar( grep { $_ == $exitStatus } @{$desiredExitStatus} ) )
242       {
243         $desiredExitStatus = $exitStatus;
244       }
245       else
246       {
247         $desiredExitStatus = -1;
248       }
249     }
250     elsif ( ref $desiredExitStatus eq "HASH" )
251     {
252       if ( exists( ${$desiredExitStatus}{$exitStatus} ) )
253       {
254         if ( defined( ${$desiredExitStatus}{$exitStatus} ) )
255         {
256           $testOutput = ${$desiredExitStatus}{$exitStatus};
257         }
258         $desiredExitStatus = $exitStatus;
259       }
260       else
261       {
262         $desiredExitStatus = -1;
263       }
264     }
266     if ( %exceptions && exists( $exceptions{$exitStatus} ) )
267     {
268       $testStatus += skip( $exceptions{$exitStatus}, $exitStatus, $desiredExitStatus );
269       $testOutput = "skip";
270     }
271     else
272     {
273       $testStatus += ok( $exitStatus, $desiredExitStatus );
274     }
275   }
277   if ( defined( $desiredOutput ) )
278   {
279     if ( $testOutput ne "skip" )
280     {
281       $testStatus += ok( $output, $desiredOutput );
282     }
283     else
284     {
285       $testStatus += skip( "Skipping output test as requested", $output, $desiredOutput );
286     }
287   }
289   return $testStatus;
293 sub skipMissingCmd
295   my( $command, $count ) = @_;
297   my $testStatus;
299   for ( 1 .. $count )
300   {
301     $testStatus += skip( "Missing ${command} - tests skipped", 1 );
302   }
304   return $testStatus;
307 sub getTestParameter
309   my( $param, $envvar, $default, $brief, $scoped ) = @_;
311   # Apply default values for optional arguments
312   $scoped = ( defined( $scoped ) && $scoped );
314   my $testharness = basename( (caller(0))[1], ".t" ); # used for scoping
316   if ( defined( $envvar ) &&  exists( $ENV{$envvar} ) && $ENV{$envvar} )
317   {
318     return $ENV{$envvar};
319   }
321   my $cachedValue = SearchCache( $param, $testharness );
322   if ( defined( $cachedValue ) && $cachedValue )
323   {
324     return $cachedValue;
325   }
327   my $defaultValid      = ( defined( $default ) && $default );
328   my $autoAcceptDefault = ( exists( $ENV{'NPTEST_ACCEPTDEFAULT'} ) && $ENV{'NPTEST_ACCEPTDEFAULT'} );
330   if ( $autoAcceptDefault && $defaultValid )
331   {
332     return $default;
333   }
335   my $userResponse = "";
337   while ( $userResponse eq "" )
338   {
339     print STDERR "\n";
340     print STDERR "Test Harness         : $testharness\n";
341     print STDERR "Test Parameter       : $param\n";
342     print STDERR "Environment Variable : $envvar\n";
343     print STDERR "Brief Description    : $brief\n";
344     print STDERR "Enter value ", ($defaultValid ? "[${default}]" : "[]"), " => ";
345     $userResponse = <STDIN>;
346     $userResponse = "" if ! defined( $userResponse ); # Handle EOF
347     chomp( $userResponse );
348     if ( $defaultValid && $userResponse eq "" )
349     {
350       $userResponse = $default;
351     }
352   }
354   print STDERR "\n";
356   # define all user responses at global scope
357   SetCacheParameter( $param, ( $scoped ? $testharness : undef ), $userResponse );
359   return $userResponse;
363 # Internal Cache Management Functions
366 sub SearchCache
368   my( $param, $scope ) = @_;
370   LoadCache();
372   if ( exists( $CACHE{$scope} ) && exists( $CACHE{$scope}{$param} ) )
373   {
374     return $CACHE{$scope}{$param};
375   }
377   if ( exists( $CACHE{$param} ) )
378   {
379     return $CACHE{$param};
380   }
383 sub SetCacheParameter
385   my( $param, $scope, $value ) = @_;
387   if ( defined( $scope ) )
388   {
389     $CACHE{$scope}{$param} = $value;
390   }
391   else
392   {
393     $CACHE{$param} = $value;
394   }
396   SaveCache();
399 sub LoadCache
401   return if exists( $CACHE{'_cache_loaded_'} );
403   if ( -f $CACHEFILENAME )
404   {
405     my( $fileHandle ) = new IO::File;
407     if ( ! $fileHandle->open( "< ${CACHEFILENAME}" ) )
408     {
409       print STDERR "NPTest::LoadCache() : Problem opening ${CACHEFILENAME} : $!\n";
410       return;
411     }
413     my( $fileContents ) = join( "\n", <$fileHandle> );
415     $fileHandle->close();
417     my( $contentsRef ) = eval $fileContents;
418     %CACHE = %{$contentsRef};
420   }
422   $CACHE{'_cache_loaded_'} = 1;
426 sub SaveCache
428   delete $CACHE{'_cache_loaded_'};
430   my( $fileHandle ) = new IO::File;
432   if ( ! $fileHandle->open( "> ${CACHEFILENAME}" ) )
433   {
434     print STDERR "NPTest::LoadCache() : Problem saving ${CACHEFILENAME} : $!\n";
435     return;
436   }
438   my( $dataDumper ) = new Data::Dumper( [ \%CACHE ] );
440   $dataDumper->Terse(1);
442   print $fileHandle $dataDumper->Dump();
444   $fileHandle->close();
446   $CACHE{'_cache_loaded_'} = 1;
450 # (Questionable) Public Cache Management Functions
453 sub SetCacheFilename
455   my( $filename ) = @_;
457   # Unfortunately we can not validate the filename
458   # in any meaningful way, as it may not yet exist
459   $CACHEFILENAME = $filename;
464 # Test Harness Wrapper Functions
467 sub DetermineTestHarnessDirectory
469   my( $userSupplied ) = @_;
471   # User Supplied
472   if ( defined( $userSupplied ) && $userSupplied )
473   {
474     if ( -d $userSupplied )
475     {
476       return $userSupplied;
477     }
478     else
479     {
480       return undef; # userSupplied is invalid -> FAIL
481     }
482   }
484   # Simple Case : "t" is a subdirectory of the current directory
485   if ( -d "./t" )
486   {
487     return "./t";
488   }
490   # To be honest I don't understand which case satisfies the
491   # original code in test.pl : when $tstdir == `pwd` w.r.t.
492   # $tstdir =~ s|^(.*)/([^/]+)/?$|$1/$2|; and if (-d "../../$2/t")
493   # Assuming pwd is "/a/b/c/d/e" then we are testing for "/a/b/c/e/t"
494   # if I understand the code correctly (a big assumption)
496   # Simple Case : the current directory is "t"
497   my $pwd = cwd();
499   if ( $pwd =~ m|/t$| )
500   {
501     return $pwd;
503     # The alternate that might work better is
504     # chdir( ".." );
505     # return "./t";
506     # As the current test harnesses assume the application
507     # to be tested is in the current directory (ie "./check_disk ....")
508   }
510   return undef;
513 sub TestsFrom
515   my( $directory, $excludeIfAppMissing ) = @_;
517   $excludeIfAppMissing = 0 unless defined( $excludeIfAppMissing );
519   if ( ! opendir( DIR, $directory ) )
520   {
521     print STDERR "NPTest::TestsFrom() - Failed to open ${directory} : $!\n";
522     return ();
523   }
525   my( @tests ) = ();
527   my $filename;
528   my $application;
530   while ( $filename = readdir( DIR ) )
531   {
532     if ( $filename =~ m/\.t$/ )
533     {
534       if ( $excludeIfAppMissing )
535       {
536         $application = basename( $filename, ".t" );
537         if ( ! -e $application )
538         {
539           print STDERR "No application (${application}) found for test harness (${filename})\n";
540           next;
541         }
542       }
543       push @tests, "${directory}/${filename}";
544     }
545   }
547   closedir( DIR );
549   return @tests;
552 # All the new object oriented stuff below
554 sub new { 
555         my $type = shift;
556         my $self = {};
557         return bless $self, $type;
560 # Accessors
561 sub return_code {
562         my $self = shift;
563         if (@_) {
564                 return $self->{return_code} = shift;
565         } else {
566                 return $self->{return_code};
567         }
569 sub output {
570         my $self = shift;
571         if (@_) {
572                 return $self->{output} = shift;
573         } else {
574                 return $self->{output};
575         }
578 sub testCmd {
579         my $class = shift;
580         my $command = shift or die "No command passed to testCmd";
581         my $object = $class->new;
582         
583         my $output = `$command`;
584         chomp $output;
585         
586         $object->output($output);
587         $object->return_code($? >> 8);
589         if ($ENV{'NPTEST_DEBUG'}) {
590                 my ($pkg, $file, $line) = caller(0);
591                 print "testCmd: Called from line $line in $file", $/;
592                 print "Testing: $command", $/;
593                 print "Output:  ", $object->output, $/;
594                 print "Return code: ", $object->return_code, $/;
595         }
597         return $object;
600 1;
602 # End of File