Merge pull request #1563 from jacobbaungard/ipv6_check_icmp
[monitoring-plugins.git] / NPTest.pm
blob4b2de39bd746333d90673f0a5ad9c098a83a00fe
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 To facilitate quick testing setup, it is possible to accept all the
66 developer provided defaults by setting the environment variable
67 "NPTEST_ACCEPTDEFAULT" to "1" (or any other perl truth value). Note
68 that, such defaults are not stored in the cache, as there is currently
69 no mechanism to edit existing cache entries, save the use of text
70 editor or removing the cache file completely.
72 =item C<testCmd($command)>
74 Call with NPTest->testCmd("./check_disk ...."). This returns a NPTest object
75 which you can then run $object->return_code or $object->output against.
77 Testing of results would be done in your test script, not in this module.
79 =item C<checkCmd(...)>
81 This function is obsolete. Use C<testCmd()> instead.
83 This function attempts to encompass the majority of test styles used
84 in testing Monitoring Plugins. As each plug-in is a separate command, the
85 typical tests we wish to perform are against the exit status of the
86 command and the output (if any) it generated. Simplifying these tests
87 into a single function call, makes the test harness easier to read and
88 maintain and allows additional functionality (such as debugging) to be
89 provided without additional effort on the part of the test harness
90 developer.
92 It is possible to enable debugging via the environment variable
93 C<NPTEST_DEBUG>. If this environment variable exists and its value in PERL's
94 boolean context evaluates to true, debugging is enabled.
96 The function prototype can be expressed as follows:
98 Parameter 1 : command => DEFINED SCALAR(string)
99 Parameter 2 : desiredExitStatus => ONE OF
100 SCALAR(integer)
101 ARRAYREF(integer)
102 HASHREF(integer,string)
103 UNDEFINED
104 Parameter 3 : desiredOutput => SCALAR(string) OR UNDEFINED
105 Parameter 4 : exceptions => HASH(integer,string) OR UNDEFINED
106 Returns : SCALAR(integer) as defined by Test::ok(...)
108 The function treats the first parameter C<$command> as a command line
109 to execute as part of the test, it is executed only once and its exit
110 status (C<$?E<gt>E<gt>8>) and output are captured.
112 At this point if debugging is enabled the command, its exit status and
113 output are displayed to the tester.
115 C<checkCmd(...)> allows the testing of either the exit status or the
116 generated output or both, not testing either will result in neither
117 the C<Test::ok(...)> or C<Test::skip(...)> functions being called,
118 something you probably don't want. Note that each defined test
119 (C<$desiredExitStatus> and C<$desiredOutput>) results in a invocation
120 of either C<Test::ok(...)> or C<Test::skip(...)>, so remember this
121 when counting the number of tests to place in the C<Test::plan(...)>
122 call.
124 Many Monitoring Plugins test network services, some of which may not be
125 present on all systems. To cater for this, C<checkCmd(...)> allows the
126 tester to define exceptions based on the command's exit status. These
127 exceptions are provided to skip tests if the test case developer
128 believes the service is not being provided. For example, if a site
129 does not have a POP3 server, the test harness could map the
130 appropriate exit status to a useful message the person running the
131 tests, telling the reason the test is being skipped.
133 Example:
135 my %exceptions = ( 2 =E<gt> "No POP Server present?" );
137 $t += checkCmd( "./check_pop I<some args>", 0, undef, %exceptions );
139 Thus, in the above example, an exit status of 2 does not result in a
140 failed test case (as the exit status is not the desired value of 0),
141 but a skipped test case with the message "No POP Server present?"
142 given as the reason.
144 Sometimes the exit status of a command should be tested against a set
145 of possible values, rather than a single value, this could especially
146 be the case in failure testing. C<checkCmd(...)> support two methods
147 of testing against a set of desired exit status values.
149 =over
151 =item *
153 Firstly, if C<$desiredExitStatus> is a reference to an array of exit
154 stati, if the actual exit status of the command is present in the
155 array, it is used in the call to C<Test::ok(...)> when testing the
156 exit status.
158 =item *
160 Alternatively, if C<$desiredExitStatus> is a reference to a hash of
161 exit stati (mapped to the strings "continue" or "skip"), similar
162 processing to the above occurs with the side affect of determining if
163 any generated output testing should proceed. Note: only the string
164 "skip" will result in generated output testing being skipped.
166 =back
168 =item C<skipMissingCmd(...)>
170 If a command is missing and the test harness must C<Test::skip()> some
171 or all of the tests in a given test harness this function provides a
172 simple iterator to issue an appropriate message the requested number
173 of times.
175 =back
177 =item C<skipMsg(...)>
179 If for any reason the test harness must C<Test::skip()> some
180 or all of the tests in a given test harness this function provides a
181 simple iterator to issue an appropriate message the requested number
182 of times.
184 =back
186 =head1 SEE ALSO
188 L<Test>
190 The rest of the code, as I have only commented on the major public
191 functions that test harness writers will use, not all the code present
192 in this helper module.
194 =head1 AUTHOR
196 Copyright (c) 2005 Peter Bray. All rights reserved.
198 This package is free software and is provided "as is" without express
199 or implied warranty. It may be used, redistributed and/or modified
200 under the same terms as the Monitoring Plugins release.
202 =cut
205 # Package Scope Variables
208 my( %CACHE ) = ();
210 # I'm not really sure wether to house a site-specific cache inside
211 # or outside of the extracted source / build tree - lets default to outside
212 my( $CACHEFILENAME ) = ( exists( $ENV{'NPTEST_CACHE'} ) && $ENV{'NPTEST_CACHE'} )
213 ? $ENV{'NPTEST_CACHE'} : "/var/tmp/NPTest.cache"; # "../Cache.pdd";
216 # Testing Functions
219 sub checkCmd
221 my( $command, $desiredExitStatus, $desiredOutput, %exceptions ) = @_;
223 my $result = NPTest->testCmd($command);
225 my $output = $result->output;
226 my $exitStatus = $result->return_code;
228 $output = "" unless defined( $output );
229 chomp( $output );
231 my $testStatus;
233 my $testOutput = "continue";
235 if ( defined( $desiredExitStatus ) )
237 if ( ref $desiredExitStatus eq "ARRAY" )
239 if ( scalar( grep { $_ == $exitStatus } @{$desiredExitStatus} ) )
241 $desiredExitStatus = $exitStatus;
243 else
245 $desiredExitStatus = -1;
248 elsif ( ref $desiredExitStatus eq "HASH" )
250 if ( exists( ${$desiredExitStatus}{$exitStatus} ) )
252 if ( defined( ${$desiredExitStatus}{$exitStatus} ) )
254 $testOutput = ${$desiredExitStatus}{$exitStatus};
256 $desiredExitStatus = $exitStatus;
258 else
260 $desiredExitStatus = -1;
264 if ( %exceptions && exists( $exceptions{$exitStatus} ) )
266 $testStatus += skip( $exceptions{$exitStatus}, $exitStatus, $desiredExitStatus );
267 $testOutput = "skip";
269 else
271 $testStatus += ok( $exitStatus, $desiredExitStatus );
275 if ( defined( $desiredOutput ) )
277 if ( $testOutput ne "skip" )
279 $testStatus += ok( $output, $desiredOutput );
281 else
283 $testStatus += skip( "Skipping output test as requested", $output, $desiredOutput );
287 return $testStatus;
291 sub skipMissingCmd
293 my( $command, $count ) = @_;
295 my $testStatus;
297 for ( 1 .. $count )
299 $testStatus += skip( "Missing ${command} - tests skipped", 1 );
302 return $testStatus;
305 sub skipMsg
307 my( $msg, $count ) = @_;
309 my $testStatus;
311 for ( 1 .. $count )
313 $testStatus += skip( $msg, 1 );
316 return $testStatus;
319 sub getTestParameter {
320 my($param, $description, $default) = @_;
322 if($param !~ m/^NP_[A-Z0-9_]+$/mx) {
323 die("parameter should be all uppercase and start with NP_ (requested from ".(caller(0))[1].")");
326 return $ENV{$param} if $ENV{$param};
328 my $cachedValue = SearchCache($param);
329 if(defined $cachedValue) {
330 return $cachedValue;
333 if($ENV{'NPTEST_ACCEPTDEFAULT'}) {
334 return $default if $default;
335 return "";
338 # Set "none" if no terminal attached (eg, tinderbox build servers when new variables set)
339 return "" unless (-t STDIN);
341 my $userResponse = "";
342 while($userResponse eq "") {
343 print STDERR "\n";
344 print STDERR "Test File : ".(caller(0))[1]."\n";
345 print STDERR "Test Parameter : $param\n";
346 print STDERR "Description : $description\n";
347 print STDERR "Enter value (or 'none') ", ($default ? "[${default}]" : "[]"), " => ";
348 $userResponse = <STDIN>;
349 $userResponse = "" if ! defined( $userResponse ); # Handle EOF
350 chomp($userResponse);
351 if($default && $userResponse eq "") {
352 $userResponse = $default;
356 print STDERR "\n";
358 if($userResponse =~ /^(na|none)$/) {
359 $userResponse = "";
362 # store user responses
363 SetCacheParameter($param, $userResponse);
365 return $userResponse;
369 # Internal Cache Management Functions
372 sub SearchCache {
373 my($param) = @_;
375 LoadCache();
377 if(exists $CACHE{$param}) {
378 return $CACHE{$param};
380 return undef; # Need this to say "nothing found"
383 sub SetCacheParameter {
384 my($param, $value) = @_;
385 $CACHE{$param} = $value;
386 SaveCache();
389 sub LoadCache
391 return if exists( $CACHE{'_cache_loaded_'} );
393 my $fileContents = "";
394 if ( -f $CACHEFILENAME )
396 my( $fileHandle ) = new IO::File;
398 if ( ! $fileHandle->open( "< ${CACHEFILENAME}" ) )
400 print STDERR "NPTest::LoadCache() : Problem opening ${CACHEFILENAME} : $!\n";
401 return;
404 $fileContents = join("", <$fileHandle>);
405 $fileHandle->close();
407 chomp($fileContents);
408 my( $contentsRef ) = eval $fileContents;
409 %CACHE = %{$contentsRef} if (defined($contentsRef));
413 $CACHE{'_cache_loaded_'} = 1;
414 $CACHE{'_original_cache'} = $fileContents;
418 sub SaveCache
420 delete $CACHE{'_cache_loaded_'};
421 my $oldFileContents = delete $CACHE{'_original_cache'};
423 # clean up old style params
424 for my $key (keys %CACHE) {
425 delete $CACHE{$key} if $key !~ m/^NP_[A-Z0-9_]+$/mx;
428 my($dataDumper) = new Data::Dumper([\%CACHE]);
429 $dataDumper->Terse(1);
430 $dataDumper->Sortkeys(1);
431 my $data = $dataDumper->Dump();
432 $data =~ s/^\s+/ /gmx; # make sure all systems use same amount of whitespace
433 $data =~ s/^\s+}/}/gmx;
434 chomp($data);
436 if($oldFileContents ne $data) {
437 my($fileHandle) = new IO::File;
438 if (!$fileHandle->open( "> ${CACHEFILENAME}")) {
439 print STDERR "NPTest::SaveCache() : Problem saving ${CACHEFILENAME} : $!\n";
440 return;
442 print $fileHandle $data;
443 $fileHandle->close();
446 $CACHE{'_cache_loaded_'} = 1;
447 $CACHE{'_original_cache'} = $data;
451 # (Questionable) Public Cache Management Functions
454 sub SetCacheFilename
456 my( $filename ) = @_;
458 # Unfortunately we can not validate the filename
459 # in any meaningful way, as it may not yet exist
460 $CACHEFILENAME = $filename;
465 # Test Harness Wrapper Functions
468 sub DetermineTestHarnessDirectory
470 my( @userSupplied ) = @_;
471 my @dirs;
473 # User Supplied
474 if ( @userSupplied > 0 )
476 for my $u ( @userSupplied )
478 if ( -d $u )
480 push ( @dirs, $u );
485 # Simple Cases: "t" and tests are subdirectories of the current directory
486 if ( -d "./t" )
488 push ( @dirs, "./t");
490 if ( -d "./tests" )
492 push ( @dirs, "./tests");
495 if ( @dirs > 0 )
497 return @dirs;
500 # To be honest I don't understand which case satisfies the
501 # original code in test.pl : when $tstdir == `pwd` w.r.t.
502 # $tstdir =~ s|^(.*)/([^/]+)/?$|$1/$2|; and if (-d "../../$2/t")
503 # Assuming pwd is "/a/b/c/d/e" then we are testing for "/a/b/c/e/t"
504 # if I understand the code correctly (a big assumption)
506 # Simple Case : the current directory is "t"
507 my $pwd = cwd();
509 if ( $pwd =~ m|/t$| )
511 push ( @dirs, $pwd );
513 # The alternate that might work better is
514 # chdir( ".." );
515 # return "./t";
516 # As the current test harnesses assume the application
517 # to be tested is in the current directory (ie "./check_disk ....")
520 return @dirs;
523 sub TestsFrom
525 my( $directory, $excludeIfAppMissing ) = @_;
527 $excludeIfAppMissing = 0 unless defined( $excludeIfAppMissing );
529 if ( ! opendir( DIR, $directory ) )
531 print STDERR "NPTest::TestsFrom() - Failed to open ${directory} : $!\n";
532 return ();
535 my( @tests ) = ();
537 my $filename;
538 my $application;
540 while ( $filename = readdir( DIR ) )
542 if ( $filename =~ m/\.t$/ )
544 if ( $excludeIfAppMissing )
546 $application = basename( $filename, ".t" );
547 if ( ! -e $application and ! -e $application.'.pm' )
549 print STDERR "No application (${application}) found for test harness (${filename})\n";
550 next;
553 push @tests, "${directory}/${filename}";
557 closedir( DIR );
559 return sort @tests;
562 # All the new object oriented stuff below
564 sub new {
565 my $type = shift;
566 my $self = {};
567 return bless $self, $type;
570 # Accessors
571 sub return_code {
572 my $self = shift;
573 if (@_) {
574 return $self->{return_code} = shift;
575 } else {
576 return $self->{return_code};
579 sub output {
580 my $self = shift;
581 if (@_) {
582 return $self->{output} = shift;
583 } else {
584 return $self->{output};
588 sub perf_output {
589 my $self = shift;
590 $_ = $self->{output};
591 /\|(.*)$/;
592 return $1 || "";
595 sub only_output {
596 my $self = shift;
597 $_ = $self->{output};
598 /(.*?)\|/;
599 return $1 || "";
602 sub testCmd {
603 my $class = shift;
604 my $command = shift or die "No command passed to testCmd";
605 my $timeout = shift || 120;
606 my $object = $class->new;
608 local $SIG{'ALRM'} = sub { die("timeout in command: $command"); };
609 alarm($timeout); # no test should take longer than 120 seconds
611 my $output = `$command`;
612 $object->return_code($? >> 8);
613 $_ = $? & 127;
614 if ($_) {
615 die "Got signal $_ for command $command";
617 chomp $output;
618 $object->output($output);
620 alarm(0);
622 my ($pkg, $file, $line) = caller(0);
623 print "Testing: $command", $/;
624 if ($ENV{'NPTEST_DEBUG'}) {
625 print "testCmd: Called from line $line in $file", $/;
626 print "Output: ", $object->output, $/;
627 print "Return code: ", $object->return_code, $/;
630 return $object;
633 # do we have ipv6
634 sub has_ipv6 {
635 # assume ipv6 if a ping6 to labs.consol.de works
636 `ping6 -c 1 2a03:3680:0:2::21 2>&1`;
637 if($? == 0) {
638 return 1;
640 return;
645 # End of File