4 # Helper Functions for testing Monitoring Plugins
9 @EXPORT = qw(getTestParameter checkCmd skipMissingCmd skipMsg);
10 @EXPORT_OK = qw(DetermineTestHarnessDirectory TestsFrom SetCacheFilename);
23 use vars
qw($VERSION);
24 $VERSION = "1556."; # must be all one line, for MakeMaker
28 NPTest - Simplify the testing of Monitoring Plugins
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
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.
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), 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
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
102 HASHREF(integer,string)
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(...)>
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.
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?"
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.
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
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.
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
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
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.
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.
205 # Package Scope Variables
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";
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 );
233 my $testOutput = "continue";
235 if ( defined( $desiredExitStatus ) )
237 if ( ref $desiredExitStatus eq "ARRAY" )
239 if ( scalar( grep { $_ == $exitStatus } @{$desiredExitStatus} ) )
241 $desiredExitStatus = $exitStatus;
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;
260 $desiredExitStatus = -1;
264 if ( %exceptions && exists( $exceptions{$exitStatus} ) )
266 $testStatus += skip( $exceptions{$exitStatus}, $exitStatus, $desiredExitStatus );
267 $testOutput = "skip";
271 $testStatus += ok( $exitStatus, $desiredExitStatus );
275 if ( defined( $desiredOutput ) )
277 if ( $testOutput ne "skip" )
279 $testStatus += ok( $output, $desiredOutput );
283 $testStatus += skip( "Skipping output test as requested", $output, $desiredOutput );
293 my( $command, $count ) = @_;
299 $testStatus += skip( "Missing ${command} - tests skipped", 1 );
307 my( $msg, $count ) = @_;
313 $testStatus += skip( $msg, 1 );
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) {
333 if($ENV{'NPTEST_ACCEPTDEFAULT'}) {
334 return $default if $default;
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 "") {
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;
358 if($userResponse =~ /^(na|none)$/) {
362 # store user responses
363 SetCacheParameter($param, $userResponse);
365 return $userResponse;
369 # Internal Cache Management Functions
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;
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";
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;
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;
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";
442 print $fileHandle $data;
443 $fileHandle->close();
446 $CACHE{'_cache_loaded_'} = 1;
447 $CACHE{'_original_cache'} = $data;
451 # (Questionable) Public Cache Management Functions
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 ) = @_;
474 if ( @userSupplied > 0 )
476 for my $u ( @userSupplied )
485 # Simple Cases: "t" and tests are subdirectories of the current directory
488 push ( @dirs, "./t");
492 push ( @dirs, "./tests");
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"
509 if ( $pwd =~ m|/t$| )
511 push ( @dirs, $pwd );
513 # The alternate that might work better is
516 # As the current test harnesses assume the application
517 # to be tested is in the current directory (ie "./check_disk ....")
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";
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";
553 push @tests, "${directory}/${filename}";
562 # All the new object oriented stuff below
567 return bless $self, $type;
574 return $self->{return_code} = shift;
576 return $self->{return_code};
582 return $self->{output} = shift;
584 return $self->{output};
590 $_ = $self->{output};
597 $_ = $self->{output};
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);
615 die "Got signal $_ for command $command";
618 $object->output($output);
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, $/;
635 # assume ipv6 if a ping6 to labs.consol.de works
636 `ping6 -c 1 2a03:3680:0:2::21 2>&1`;