NPTest.pm 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. package NPTest;
  2. #
  3. # Helper Functions for testing Monitoring Plugins
  4. #
  5. require Exporter;
  6. @ISA = qw(Exporter);
  7. @EXPORT = qw(getTestParameter checkCmd skipMissingCmd skipMsg);
  8. @EXPORT_OK = qw(DetermineTestHarnessDirectory TestsFrom SetCacheFilename);
  9. use strict;
  10. use warnings;
  11. use Cwd;
  12. use File::Basename;
  13. use IO::File;
  14. use Data::Dumper;
  15. use Test;
  16. use vars qw($VERSION);
  17. $VERSION = "1556."; # must be all one line, for MakeMaker
  18. =head1 NAME
  19. NPTest - Simplify the testing of Monitoring Plugins
  20. =head1 DESCRIPTION
  21. This modules provides convenience functions to assist in the testing
  22. of Monitoring Plugins, making the testing code easier to read and write;
  23. hopefully encouraging the development of a more complete test suite for
  24. the Monitoring Plugins. It is based on the patterns of testing seen in the
  25. 1.4.0 release, and continues to use the L<Test> module as the basis of
  26. testing.
  27. =head1 FUNCTIONS
  28. This module defines four public functions, C<getTestParameter(...)>,
  29. C<checkCmd(...)>, C<skipMissingCmd(...)> and C<skipMsg(...)>. These are exported by
  30. default via the C<use NPTest;> statement.
  31. =over
  32. =item getTestParameter( "ENV_VARIABLE", $brief_description, $default )
  33. $default is optional.
  34. This function allows the test harness
  35. developer to interactively request test parameter information from the
  36. user. The user can accept the developer's default value or reply "none"
  37. which will then be returned as "" for the test to skip if appropriate.
  38. If a parameter needs to be entered and the test is run without a tty
  39. attached (such as a cronjob), the parameter will be assigned as if it
  40. was "none". Tests can check for the parameter and skip if not set.
  41. Responses are stored in an external, file-based cache so subsequent test
  42. runs will use these values. The user is able to change the values by
  43. amending the values in the file /var/tmp/NPTest.cache, or by setting
  44. the appropriate environment variable before running the test.
  45. The option exists to store parameters in a scoped means, allowing a
  46. test harness to a localise a parameter should the need arise. This
  47. allows a parameter of the same name to exist in a test harness
  48. specific scope, while not affecting the globally scoped parameter. The
  49. scoping identifier is the name of the test harness sans the trailing
  50. ".t". All cache searches first look to a scoped parameter before
  51. looking for the parameter at global scope. Thus for a test harness
  52. called "check_disk.t" requesting the parameter "mountpoint_valid", the
  53. cache is first searched for "check_disk"/"mountpoint_valid", if this
  54. fails, then a search is conducted for "mountpoint_valid".
  55. To facilitate quick testing setup, it is possible to accept all the
  56. developer provided defaults by setting the environment variable
  57. "NPTEST_ACCEPTDEFAULT" to "1" (or any other perl truth value). Note
  58. that, such defaults are not stored in the cache, as there is currently
  59. no mechanism to edit existing cache entries, save the use of text
  60. editor or removing the cache file completely.
  61. =item C<testCmd($command)>
  62. Call with NPTest->testCmd("./check_disk ...."). This returns a NPTest object
  63. which you can then run $object->return_code or $object->output against.
  64. Testing of results would be done in your test script, not in this module.
  65. =item C<checkCmd(...)>
  66. This function is obsolete. Use C<testCmd()> instead.
  67. This function attempts to encompass the majority of test styles used
  68. in testing Monitoring Plugins. As each plug-in is a separate command, the
  69. typical tests we wish to perform are against the exit status of the
  70. command and the output (if any) it generated. Simplifying these tests
  71. into a single function call, makes the test harness easier to read and
  72. maintain and allows additional functionality (such as debugging) to be
  73. provided without additional effort on the part of the test harness
  74. developer.
  75. It is possible to enable debugging via the environment variable
  76. C<NPTEST_DEBUG>. If this environment variable exists and its value in PERL's
  77. boolean context evaluates to true, debugging is enabled.
  78. The function prototype can be expressed as follows:
  79. Parameter 1 : command => DEFINED SCALAR(string)
  80. Parameter 2 : desiredExitStatus => ONE OF
  81. SCALAR(integer)
  82. ARRAYREF(integer)
  83. HASHREF(integer,string)
  84. UNDEFINED
  85. Parameter 3 : desiredOutput => SCALAR(string) OR UNDEFINED
  86. Parameter 4 : exceptions => HASH(integer,string) OR UNDEFINED
  87. Returns : SCALAR(integer) as defined by Test::ok(...)
  88. The function treats the first parameter C<$command> as a command line
  89. to execute as part of the test, it is executed only once and its exit
  90. status (C<$?E<gt>E<gt>8>) and output are captured.
  91. At this point if debugging is enabled the command, its exit status and
  92. output are displayed to the tester.
  93. C<checkCmd(...)> allows the testing of either the exit status or the
  94. generated output or both, not testing either will result in neither
  95. the C<Test::ok(...)> or C<Test::skip(...)> functions being called,
  96. something you probably don't want. Note that each defined test
  97. (C<$desiredExitStatus> and C<$desiredOutput>) results in a invocation
  98. of either C<Test::ok(...)> or C<Test::skip(...)>, so remember this
  99. when counting the number of tests to place in the C<Test::plan(...)>
  100. call.
  101. Many Monitoring Plugins test network services, some of which may not be
  102. present on all systems. To cater for this, C<checkCmd(...)> allows the
  103. tester to define exceptions based on the command's exit status. These
  104. exceptions are provided to skip tests if the test case developer
  105. believes the service is not being provided. For example, if a site
  106. does not have a POP3 server, the test harness could map the
  107. appropriate exit status to a useful message the person running the
  108. tests, telling the reason the test is being skipped.
  109. Example:
  110. my %exceptions = ( 2 =E<gt> "No POP Server present?" );
  111. $t += checkCmd( "./check_pop I<some args>", 0, undef, %exceptions );
  112. Thus, in the above example, an exit status of 2 does not result in a
  113. failed test case (as the exit status is not the desired value of 0),
  114. but a skipped test case with the message "No POP Server present?"
  115. given as the reason.
  116. Sometimes the exit status of a command should be tested against a set
  117. of possible values, rather than a single value, this could especially
  118. be the case in failure testing. C<checkCmd(...)> support two methods
  119. of testing against a set of desired exit status values.
  120. =over
  121. =item *
  122. Firstly, if C<$desiredExitStatus> is a reference to an array of exit
  123. stati, if the actual exit status of the command is present in the
  124. array, it is used in the call to C<Test::ok(...)> when testing the
  125. exit status.
  126. =item *
  127. Alternatively, if C<$desiredExitStatus> is a reference to a hash of
  128. exit stati (mapped to the strings "continue" or "skip"), similar
  129. processing to the above occurs with the side affect of determining if
  130. any generated output testing should proceed. Note: only the string
  131. "skip" will result in generated output testing being skipped.
  132. =back
  133. =item C<skipMissingCmd(...)>
  134. If a command is missing and the test harness must C<Test::skip()> some
  135. or all of the tests in a given test harness this function provides a
  136. simple iterator to issue an appropriate message the requested number
  137. of times.
  138. =back
  139. =item C<skipMsg(...)>
  140. If for any reason the test harness must C<Test::skip()> some
  141. or all of the tests in a given test harness this function provides a
  142. simple iterator to issue an appropriate message the requested number
  143. of times.
  144. =back
  145. =head1 SEE ALSO
  146. L<Test>
  147. The rest of the code, as I have only commented on the major public
  148. functions that test harness writers will use, not all the code present
  149. in this helper module.
  150. =head1 AUTHOR
  151. Copyright (c) 2005 Peter Bray. All rights reserved.
  152. This package is free software and is provided "as is" without express
  153. or implied warranty. It may be used, redistributed and/or modified
  154. under the same terms as the Monitoring Plugins release.
  155. =cut
  156. #
  157. # Package Scope Variables
  158. #
  159. my( %CACHE ) = ();
  160. # I'm not really sure wether to house a site-specific cache inside
  161. # or outside of the extracted source / build tree - lets default to outside
  162. my( $CACHEFILENAME ) = ( exists( $ENV{'NPTEST_CACHE'} ) && $ENV{'NPTEST_CACHE'} )
  163. ? $ENV{'NPTEST_CACHE'} : "/var/tmp/NPTest.cache"; # "../Cache.pdd";
  164. #
  165. # Testing Functions
  166. #
  167. sub checkCmd
  168. {
  169. my( $command, $desiredExitStatus, $desiredOutput, %exceptions ) = @_;
  170. my $result = NPTest->testCmd($command);
  171. my $output = $result->output;
  172. my $exitStatus = $result->return_code;
  173. $output = "" unless defined( $output );
  174. chomp( $output );
  175. my $testStatus;
  176. my $testOutput = "continue";
  177. if ( defined( $desiredExitStatus ) )
  178. {
  179. if ( ref $desiredExitStatus eq "ARRAY" )
  180. {
  181. if ( scalar( grep { $_ == $exitStatus } @{$desiredExitStatus} ) )
  182. {
  183. $desiredExitStatus = $exitStatus;
  184. }
  185. else
  186. {
  187. $desiredExitStatus = -1;
  188. }
  189. }
  190. elsif ( ref $desiredExitStatus eq "HASH" )
  191. {
  192. if ( exists( ${$desiredExitStatus}{$exitStatus} ) )
  193. {
  194. if ( defined( ${$desiredExitStatus}{$exitStatus} ) )
  195. {
  196. $testOutput = ${$desiredExitStatus}{$exitStatus};
  197. }
  198. $desiredExitStatus = $exitStatus;
  199. }
  200. else
  201. {
  202. $desiredExitStatus = -1;
  203. }
  204. }
  205. if ( %exceptions && exists( $exceptions{$exitStatus} ) )
  206. {
  207. $testStatus += skip( $exceptions{$exitStatus}, $exitStatus, $desiredExitStatus );
  208. $testOutput = "skip";
  209. }
  210. else
  211. {
  212. $testStatus += ok( $exitStatus, $desiredExitStatus );
  213. }
  214. }
  215. if ( defined( $desiredOutput ) )
  216. {
  217. if ( $testOutput ne "skip" )
  218. {
  219. $testStatus += ok( $output, $desiredOutput );
  220. }
  221. else
  222. {
  223. $testStatus += skip( "Skipping output test as requested", $output, $desiredOutput );
  224. }
  225. }
  226. return $testStatus;
  227. }
  228. sub skipMissingCmd
  229. {
  230. my( $command, $count ) = @_;
  231. my $testStatus;
  232. for ( 1 .. $count )
  233. {
  234. $testStatus += skip( "Missing ${command} - tests skipped", 1 );
  235. }
  236. return $testStatus;
  237. }
  238. sub skipMsg
  239. {
  240. my( $msg, $count ) = @_;
  241. my $testStatus;
  242. for ( 1 .. $count )
  243. {
  244. $testStatus += skip( $msg, 1 );
  245. }
  246. return $testStatus;
  247. }
  248. sub getTestParameter
  249. {
  250. my( $param, $envvar, $default, $brief, $scoped );
  251. my $new_style;
  252. if (scalar @_ <= 3) {
  253. ($param, $brief, $default) = @_;
  254. $envvar = $param;
  255. $new_style = 1;
  256. } else {
  257. ( $param, $envvar, $default, $brief, $scoped ) = @_;
  258. $new_style = 0;
  259. }
  260. # Apply default values for optional arguments
  261. $scoped = ( defined( $scoped ) && $scoped );
  262. my $testharness = basename( (caller(0))[1], ".t" ); # used for scoping
  263. if ( defined( $envvar ) && exists( $ENV{$envvar} ) && $ENV{$envvar} )
  264. {
  265. return $ENV{$envvar};
  266. }
  267. my $cachedValue = SearchCache( $param, $testharness );
  268. if ( defined( $cachedValue ) )
  269. {
  270. # This save required to convert to new style because the key required is
  271. # changing to the environment variable
  272. if ($new_style == 0) {
  273. SetCacheParameter( $envvar, undef, $cachedValue );
  274. }
  275. return $cachedValue;
  276. }
  277. my $defaultValid = ( defined( $default ) && $default );
  278. my $autoAcceptDefault = ( exists( $ENV{'NPTEST_ACCEPTDEFAULT'} ) && $ENV{'NPTEST_ACCEPTDEFAULT'} );
  279. if ( $autoAcceptDefault && $defaultValid )
  280. {
  281. return $default;
  282. }
  283. # Set "none" if no terminal attached (eg, tinderbox build servers when new variables set)
  284. return "" unless (-t STDIN);
  285. my $userResponse = "";
  286. while ( $userResponse eq "" )
  287. {
  288. print STDERR "\n";
  289. print STDERR "Test Harness : $testharness\n";
  290. print STDERR "Test Parameter : $param\n";
  291. print STDERR "Environment Variable : $envvar\n" if ($param ne $envvar);
  292. print STDERR "Brief Description : $brief\n";
  293. print STDERR "Enter value (or 'none') ", ($defaultValid ? "[${default}]" : "[]"), " => ";
  294. $userResponse = <STDIN>;
  295. $userResponse = "" if ! defined( $userResponse ); # Handle EOF
  296. chomp( $userResponse );
  297. if ( $defaultValid && $userResponse eq "" )
  298. {
  299. $userResponse = $default;
  300. }
  301. }
  302. print STDERR "\n";
  303. if ($userResponse =~ /^(na|none)$/) {
  304. $userResponse = "";
  305. }
  306. # define all user responses at global scope
  307. SetCacheParameter( $param, ( $scoped ? $testharness : undef ), $userResponse );
  308. return $userResponse;
  309. }
  310. #
  311. # Internal Cache Management Functions
  312. #
  313. sub SearchCache
  314. {
  315. my( $param, $scope ) = @_;
  316. LoadCache();
  317. if ( exists( $CACHE{$scope} ) && exists( $CACHE{$scope}{$param} ) )
  318. {
  319. return $CACHE{$scope}{$param};
  320. }
  321. if ( exists( $CACHE{$param} ) )
  322. {
  323. return $CACHE{$param};
  324. }
  325. return undef; # Need this to say "nothing found"
  326. }
  327. sub SetCacheParameter
  328. {
  329. my( $param, $scope, $value ) = @_;
  330. if ( defined( $scope ) )
  331. {
  332. $CACHE{$scope}{$param} = $value;
  333. }
  334. else
  335. {
  336. $CACHE{$param} = $value;
  337. }
  338. SaveCache();
  339. }
  340. sub LoadCache
  341. {
  342. return if exists( $CACHE{'_cache_loaded_'} );
  343. my $fileContents = "";
  344. if ( -f $CACHEFILENAME )
  345. {
  346. my( $fileHandle ) = new IO::File;
  347. if ( ! $fileHandle->open( "< ${CACHEFILENAME}" ) )
  348. {
  349. print STDERR "NPTest::LoadCache() : Problem opening ${CACHEFILENAME} : $!\n";
  350. return;
  351. }
  352. $fileContents = join("", <$fileHandle>);
  353. $fileHandle->close();
  354. chomp($fileContents);
  355. my( $contentsRef ) = eval $fileContents;
  356. %CACHE = %{$contentsRef} if (defined($contentsRef));
  357. }
  358. $CACHE{'_cache_loaded_'} = 1;
  359. $CACHE{'_original_cache'} = $fileContents;
  360. }
  361. sub SaveCache
  362. {
  363. delete $CACHE{'_cache_loaded_'};
  364. my $oldFileContents = delete $CACHE{'_original_cache'};
  365. my($dataDumper) = new Data::Dumper([\%CACHE]);
  366. $dataDumper->Terse(1);
  367. $dataDumper->Sortkeys(1);
  368. my $data = $dataDumper->Dump();
  369. $data =~ s/^\s+/ /gmx; # make sure all systems use same amount of whitespace
  370. $data =~ s/^\s+}/}/gmx;
  371. chomp($data);
  372. if($oldFileContents ne $data) {
  373. my($fileHandle) = new IO::File;
  374. if (!$fileHandle->open( "> ${CACHEFILENAME}")) {
  375. print STDERR "NPTest::LoadCache() : Problem saving ${CACHEFILENAME} : $!\n";
  376. return;
  377. }
  378. print $fileHandle $data;
  379. $fileHandle->close();
  380. }
  381. $CACHE{'_cache_loaded_'} = 1;
  382. $CACHE{'_original_cache'} = $data;
  383. }
  384. #
  385. # (Questionable) Public Cache Management Functions
  386. #
  387. sub SetCacheFilename
  388. {
  389. my( $filename ) = @_;
  390. # Unfortunately we can not validate the filename
  391. # in any meaningful way, as it may not yet exist
  392. $CACHEFILENAME = $filename;
  393. }
  394. #
  395. # Test Harness Wrapper Functions
  396. #
  397. sub DetermineTestHarnessDirectory
  398. {
  399. my( @userSupplied ) = @_;
  400. my @dirs;
  401. # User Supplied
  402. if ( @userSupplied > 0 )
  403. {
  404. for my $u ( @userSupplied )
  405. {
  406. if ( -d $u )
  407. {
  408. push ( @dirs, $u );
  409. }
  410. }
  411. }
  412. # Simple Cases: "t" and tests are subdirectories of the current directory
  413. if ( -d "./t" )
  414. {
  415. push ( @dirs, "./t");
  416. }
  417. if ( -d "./tests" )
  418. {
  419. push ( @dirs, "./tests");
  420. }
  421. if ( @dirs > 0 )
  422. {
  423. return @dirs;
  424. }
  425. # To be honest I don't understand which case satisfies the
  426. # original code in test.pl : when $tstdir == `pwd` w.r.t.
  427. # $tstdir =~ s|^(.*)/([^/]+)/?$|$1/$2|; and if (-d "../../$2/t")
  428. # Assuming pwd is "/a/b/c/d/e" then we are testing for "/a/b/c/e/t"
  429. # if I understand the code correctly (a big assumption)
  430. # Simple Case : the current directory is "t"
  431. my $pwd = cwd();
  432. if ( $pwd =~ m|/t$| )
  433. {
  434. push ( @dirs, $pwd );
  435. # The alternate that might work better is
  436. # chdir( ".." );
  437. # return "./t";
  438. # As the current test harnesses assume the application
  439. # to be tested is in the current directory (ie "./check_disk ....")
  440. }
  441. return @dirs;
  442. }
  443. sub TestsFrom
  444. {
  445. my( $directory, $excludeIfAppMissing ) = @_;
  446. $excludeIfAppMissing = 0 unless defined( $excludeIfAppMissing );
  447. if ( ! opendir( DIR, $directory ) )
  448. {
  449. print STDERR "NPTest::TestsFrom() - Failed to open ${directory} : $!\n";
  450. return ();
  451. }
  452. my( @tests ) = ();
  453. my $filename;
  454. my $application;
  455. while ( $filename = readdir( DIR ) )
  456. {
  457. if ( $filename =~ m/\.t$/ )
  458. {
  459. if ( $excludeIfAppMissing )
  460. {
  461. $application = basename( $filename, ".t" );
  462. if ( ! -e $application and ! -e $application.'.pm' )
  463. {
  464. print STDERR "No application (${application}) found for test harness (${filename})\n";
  465. next;
  466. }
  467. }
  468. push @tests, "${directory}/${filename}";
  469. }
  470. }
  471. closedir( DIR );
  472. return sort @tests;
  473. }
  474. # All the new object oriented stuff below
  475. sub new {
  476. my $type = shift;
  477. my $self = {};
  478. return bless $self, $type;
  479. }
  480. # Accessors
  481. sub return_code {
  482. my $self = shift;
  483. if (@_) {
  484. return $self->{return_code} = shift;
  485. } else {
  486. return $self->{return_code};
  487. }
  488. }
  489. sub output {
  490. my $self = shift;
  491. if (@_) {
  492. return $self->{output} = shift;
  493. } else {
  494. return $self->{output};
  495. }
  496. }
  497. sub perf_output {
  498. my $self = shift;
  499. $_ = $self->{output};
  500. /\|(.*)$/;
  501. return $1 || "";
  502. }
  503. sub only_output {
  504. my $self = shift;
  505. $_ = $self->{output};
  506. /(.*?)\|/;
  507. return $1 || "";
  508. }
  509. sub testCmd {
  510. my $class = shift;
  511. my $command = shift or die "No command passed to testCmd";
  512. my $timeout = shift || 120;
  513. my $object = $class->new;
  514. local $SIG{'ALRM'} = sub { die("timeout in command: $command"); };
  515. alarm($timeout); # no test should take longer than 120 seconds
  516. my $output = `$command`;
  517. $object->return_code($? >> 8);
  518. $_ = $? & 127;
  519. if ($_) {
  520. die "Got signal $_ for command $command";
  521. }
  522. chomp $output;
  523. $object->output($output);
  524. alarm(0);
  525. my ($pkg, $file, $line) = caller(0);
  526. print "Testing: $command", $/;
  527. if ($ENV{'NPTEST_DEBUG'}) {
  528. print "testCmd: Called from line $line in $file", $/;
  529. print "Output: ", $object->output, $/;
  530. print "Return code: ", $object->return_code, $/;
  531. }
  532. return $object;
  533. }
  534. # do we have ipv6
  535. sub has_ipv6 {
  536. # assume ipv6 if a ping6 to labs.consol.de works
  537. `ping6 -c 1 2a03:3680:0:2::21 2>&1`;
  538. if($? == 0) {
  539. return 1;
  540. }
  541. return;
  542. }
  543. 1;
  544. #
  545. # End of File
  546. #