File Coverage

blib/lib/Test/Config/System.pm
Criterion Covered Total %
statement 138 140 98.5
branch 61 64 95.3
condition 26 41 63.4
subroutine 14 14 100.0
pod 6 6 100.0
total 245 265 92.4


line stmt bran cond sub pod time code
1             package Test::Config::System;
2              
3 8     8   34297 use warnings;
  8         16  
  8         233  
4 8     8   86 use strict;
  8         16  
  8         224  
5              
6 8     8   42 use Carp;
  8         25  
  8         682  
7              
8             my $CLASS = __PACKAGE__;
9 8     8   60 use base 'Test::Builder::Module';
  8         17  
  8         7923  
10              
11             =head1 NAME
12              
13             Test::Config::System - System configuration related unit tests
14              
15             =head1 VERSION
16              
17             Version 0.63
18              
19             =cut
20              
21             our $VERSION = '0.63';
22             our @EXPORT = qw(check_package check_any_package check_file_contents check_link check_file check_dir plan diag ok skip);
23             our $AUTOLOAD;
24              
25             sub AUTOLOAD {
26 6     6   6617 my $tb = Test::Config::System->builder;
27 6         106 my $name = $AUTOLOAD;
28 6         45 $name =~ s/.*://;
29              
30 6         59 $tb->$name(@_);
31             }
32              
33             =head1 SYNOPSIS
34              
35             use Test::Config::System tests => 3;
36            
37             check_package('less', 'package less');
38             check_package('emacs21', 'emacs uninstalled', 1);
39             check_link('/etc/alternatives/www-browser', '/usr/bin/w3m');
40             check_file_contents('Test/Config/System.pm', qr/do {local \$\//);
41              
42             =head1 DESCRIPTION
43              
44             Test::Config::System provides functions to help test system configuration,
45             such as installed packages or config files. It was built for use in a
46             cfengine staging environment. (L. cfengine is used
47             for automating and managing system configuration at large sites).
48              
49             Test::Config::System does not depend on, or interact with cfengine in any way,
50             however, and can be used on its own or with another configuration management
51             tool (such as puppet or bcfg2).
52              
53             This module is a subclass of Test::Builder::Module, and is used like any other
54             Test::* module. Instead of directly providing an ok() function, the functions
55             exported (described below), call ok() themselves, based on the outcome of the
56             check they preform.
57              
58             =head1 EXAMPLE
59              
60             use Test::Config::System tests => 3;
61             # check something we already know: :)
62             check_package('perl', 'perl is installed');
63             check_file_contents('/etc/apt/sources.list', qr/apt-proxy/,
64             'apt-proxy is not being used', 1); # the apt-proxy is not
65             # anywhere in sources.list
66             check_package('vim'); # Make sure we have the one true editor
67              
68              
69             =head1 EXPORT
70              
71             check_package
72             check_any_package
73             check_file_contents
74             check_link
75             check_dir
76             check_file
77              
78             From Test::Builder::Module:
79              
80             plan
81             diag
82             ok
83             skip
84              
85             =head1 FUNCTIONS
86              
87             Each of the functions below use Test::Builder::Module's ok() function to
88             report test success or failure. They return the result of ok() upon
89             normal completion, or a false value (undef in scalar context, an empty
90             list in list context) on error (eg a required parameter not provided, or
91             FS doesn't support symlinks).
92              
93             =head2 check_package( NAME, [DESC, INVERT, PACKAGE_MANAGER] )
94              
95             NOTE: check_package currently only supports dpkg and rpm.
96              
97             check_package tests whether or not a package is installed. If the package
98             is not installed or if the given package manager does not exist, the test
99             fails. Otherwise, the test will pass. (If the test is inverted, the outcome
100             is the opposite).
101              
102             - NAME: package name
103             - DESC: test name (optional, defaults to package name)
104             - INVERT: invert test (optional. Possible values are 0 (default) and 1.)
105             - PACKAGE_MANAGER: package manager (optional. Defaults to 'dpkg'.
106             Supported values are 'dpkg' and 'rpm'.
107             If the given package manager is not
108             supported, returns false)
109              
110             Examples:
111              
112             # Will fail if nethack is not installed
113             check_package('nethack-text');
114             # This will fail if x11-common is installed:
115             check_package('x11-common', 'x11-common is not installed', 1);
116              
117             =cut
118              
119             #TODO: version
120             #FIXME: this needs to be more portable :/
121             my %_pkgmgrs = ( 'dpkg' => "/usr/bin/dpkg -l %s 2>/dev/null|grep '^ii' > /dev/null",
122             'rpm' => "/bin/rpm -q %s >/dev/null",
123             );
124              
125             sub check_package {
126 5     5 1 3245 my ($pkg, $testname, $invert, $pkgmgr, $res);
127 5         13 $pkg = shift;
128 5 100       24 unless ($pkg) {
129 1         11 carp "Package name required";
130 1         491 return;
131             }
132 4   66     19 $testname = (shift || $pkg);
133 4   100     29 $invert = (shift || 0);
134 4   100     18 $pkgmgr = (shift || 'dpkg');
135              
136 4         12 $res = _installedp($pkg, $pkgmgr);
137 4 100       143 return $res if !defined($res); # installedp wants us to return false
138              
139 2         67 my $tb = Test::Config::System->builder;
140 2 100       111 $tb->ok($invert ? !$res : $res, $testname)
141             }
142              
143             =head2 check_any_package( LIST, [DESC, INVERT, PACKAGE_MANAGER] )
144              
145             NOTE: check_any_package currently only supports dpkg and rpm.
146              
147             check_any_package tests if any one in a list of packages is installed.
148             Much like Perl's "||" operator, check_any_package will short-circuit (if
149             the first package is installed, the test will immediately pass without
150             checking the others).
151              
152             - LIST: a list of packages to check
153             - DESC: see C
154             - INVERT: inverts test (A value of 1 will cause the test to fail
155             unless I of the given packages are installed)
156             - PACKAGE_MANAGER: see C
157              
158             Examples:
159              
160             # Will pass if either of the given packages are installed
161             check_any_package(['libtest-config-system-perl',
162             'cpan-libtest-config-system-perl'],
163             'Test::Config::System is installed');
164             # Will fail if either xserver-xorg or xserver-xfree86 is installed
165             check_any_package(['xserver-xorg', 'xserver-xfree86',
166             'No X11 server installed', 1);
167              
168             =cut
169              
170             sub check_any_package {
171 5     5 1 4382 my ($list, $testname, $invert, $pkgmgr, $res);
172 5         9 $list = shift;
173 5 100       56 unless (ref($list)) {
174 1         11 carp "Require a list of package names";
175 1         556 return;
176             }
177 4   33     17 $testname = (shift || $list->[0]);
178 4   100     32 $invert = (shift || 0);
179 4   50     17 $pkgmgr = (shift || 'dpkg');
180              
181 4         11 for my $pkg (@$list) {
182 7         41 $res = _installedp($pkg, $pkgmgr);
183 7 100       63 return $res if !defined($res); # installedp wants us to return false
184 6 100       94 last if $res;
185             }
186              
187 3         88 my $tb = Test::Config::System->builder;
188 3 100       175 $tb->ok($invert ? !$res : $res, $testname);
189             }
190              
191              
192             # used by check_.*package
193             sub _installedp {
194             ## Let's clean up %ENV a bit before we call a shell...
195 11     11   55 local %ENV;
196 11         806 $ENV{PATH} = '/bin:/usr/bin';
197 11 50       49 $ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
198 11         251 delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
199              
200              
201 11         26 my $res=0;
202 11         48 my ($pkg, $pkgmgr) = @_;
203              
204 11         47 my %cmds = ( 'dpkg' => '/usr/bin/dpkg -l %s 2>/dev/null|grep \'^ii\' > /dev/null',
205             'rpm' => '/bin/rpm -q %s >/dev/null',
206             );
207              
208             ## The debian policy is more restrictive. I can't find any policy
209             ## information for redhat, so I'm being a bit more lenient.
210 11 100       123 if ($pkg =~ /^([A-Za-z0-9-+._]+)/) {
211 10         75 $pkg = $1; # This untaints $pkg
212             } else {
213 1         13 carp "Invalid package name. If this is an error and the package is indeed valid, *please* file a bug.";
214 1         661 return;
215             }
216              
217 10 100       37 if (exists($_pkgmgrs{$pkgmgr})) { # Do we support this package manager?
218 8         286468 $res = system(sprintf($cmds{$pkgmgr}, $pkg));
219             ## get the actual return value
220 8         130 $res = $res >> 8;
221 8         187 $res = !$res; # shell does 0 on success...
222             } else {
223 2         24 carp "Package manager [$pkgmgr] is not supported";
224 2         1446 return;
225             }
226              
227 8         1645 return $res;
228             }
229              
230             =head2 check_file_contents( FILENAME, REGEX, [DESC, INVERT] )
231              
232             check_file_contents tests that a file's contents match a given regex. It
233             slurps the contents of the given filename, then matches it against the given
234             regex. If the regex matches the contents of the file, ok() is called with a
235             true value, causing the test to pass. If the regex does not match, the test
236             will fail (as in check_package, if the test is inverted, the results are
237             inverted as well).
238              
239             If the given file cannot be opened for reading, ok() will be called with a
240             false value (unless, as always, invert is true).
241              
242             Note that the entire file is slurped into memory at once. This has two
243             concequences. First, large files will eat up RAM. Second, ^ and $ do not
244             work line-by-line, as one might expect. "\n" (or your system's line-end
245             sequence) can be used in place of ^ or $ to match the start and end of a
246             line.
247              
248             - FILENAME: file to test
249             - REGEX: regex to match (qr//-style regex)
250             - DESC: test name (optional, defaults to filename)
251             - INVERT: invert test (optional. Possible values are 0 (default) and 1.)
252              
253             Examples:
254              
255             check_file_contents('/etc/fstab', qr|proc\s+/proc\s+proc\s+defaults\s+0\s+0|,
256             'proc is in fstab');
257             check_file_contents('/etc/passwd', qr|evilbob:x:\d+:\d+:|,
258             'evilbob is not in passwd', 1);
259              
260             =cut
261              
262             #TODO: should take, optionally, a hash of files/regexen or something of that
263             # nature
264             sub check_file_contents {
265 6     6 1 10260 my $tb = Test::Config::System->builder;
266 6         67 my ($filename, $regex, $testname, $invert, $res);
267 6         12 $filename = shift;
268 6         11 $regex = shift;
269 6 100       23 if (!$filename) {
270 1         19 carp "Filename required";
271 1         638 return;
272             }
273 5 100       26 if (ref($regex) ne 'Regexp') {
274 1         11 carp "qr// style regex required";
275 1         559 return;
276             }
277 4   33     17 $testname = (shift || $filename);
278 4   100     23 $invert = (shift || 0);
279              
280 4         13 $res = _match_file($filename,$regex);
281              
282 4 100       30 $tb->ok($invert ? !$res : $res, $testname);
283             }
284              
285              
286             sub _match_file {
287 4     4   9 my ($filename,$regex) = @_;
288 4         160 my $res = (open my $fh, '<', $filename);
289 4 100       21 return unless $res;
290              
291 2         4 my $text = do {local $/; <$fh>};
  2         10  
  2         132  
292 2         26 close($fh);
293              
294 2         326 return ($text =~ /$regex/);
295             }
296              
297              
298             =head2 check_link( FILENAME, [TARGET, DESC, INVERT] )
299              
300             check_link verifies that a symlink exists and, optionally, points to the
301             correct target.
302              
303             If no filename is passed, it will return a false value, otherwise a test
304             will be run and the result of ok() will be returned.
305              
306             If the filesystem does not support symlinks, the test will be skipped, and
307             check_link will return false.
308              
309             - FILENAME: filename (path to a symlink)
310             - TARGET: path the link should point to. optional. If not specified,
311             check_link will ignore the target, and just verify that the
312             link exists.
313             - DESC: test name (optional, defaults to filename)
314             - INVERT: invert test (optional. Possible values are 0 (default) and 1.)
315              
316             Examples:
317              
318             check_link('/etc/alternatives/rsh', '/usr/bin/ssh');
319             check_link('/etc/sudoers', '', 'sudoers is not a symlink', 1);
320              
321             =cut
322              
323             sub check_link {
324 4     4 1 2140 my ($src, $target, $testname, $invert, $res);
325 4         502 my $tb = Test::Config::System->builder;
326              
327 4 50       33 unless (eval { symlink("",""); 1 }) {
  4         36  
  4         16  
328 0         0 $tb->skip("symlinks are not supported on this platform");
329 0         0 return;
330             }
331              
332 4         7 $src = shift;
333 4 100       12 if (!$src) {
334 1         18 carp "Filename required";
335 1         578 return;
336             }
337              
338 3         3 $target = shift;
339 3   33     10 $testname = (shift || $src);
340 3   100     13 $invert = (shift || 0);
341              
342 3         48 my $link = readlink($src);
343 3 100       13 if (!$link) { # not a symlink or ENOENT, fail
    100          
344 1         3 $res = 0;
345             } elsif ($target) { # valid link and target is asked for
346 1 50       6 $res = (($link eq $target) ? 1 : 0);
347             } else { # valid link and don't care about target
348 1         19 $res = -l $src;
349             }
350              
351 3 100       16 $tb->ok($invert ? !$res : $res, $testname);
352             }
353              
354             =head2 check_file( PATH, [STAT, DESC, INVERT] )
355              
356             check_file verifies that a directory exists. Optionally, it can check
357             various attributes such as owner, group, or permissions.
358              
359             - PATH: path of the file to check
360             - STAT: a hashref of attributes and their desired values. Valid keys:
361             -uid owner uid
362             -gid owner gid
363             -mode file permissions
364             Each key is optional, as is the entire hash. Note that the type need
365             not be specified in -mode; it is added automatically (ie use 0700
366             instead of 010700).
367              
368             - DESC: test name (optional, defaults to PATH)
369             - INVERT: invert test (optional. Possible values are 0 (default) and 1.)
370              
371             Examples:
372              
373             check_file('/etc/sudoers', { '-uid' => 0, '-gid' => 0, '-mode' => 0440);
374              
375             =head2 check_dir( PATH, [STAT, DESC, INVERT] )
376              
377             check_dir is check_file for directories (they both call the same internal
378             sub). Arguments and calling are exactly the same, the only significant
379             difference is that check_dir verifies that PATH is a directory rather than
380             a file.
381              
382             Examples:
383              
384             check_dir('/home/ian', { '-uid' => scalar getpwnam('ian') } );
385             check_dir('/root/', { '-uid' => 0, '-gid' => 0, '-mode' => 0700 },
386             '/root/ has sane permissions');
387             check_dir('/home/evilbob', { }, 'Evilbobs homedir does not exist',1);
388              
389             =cut
390              
391             sub check_dir {
392 7     7 1 5942 my ($path, $stat, $testname, $invert,$res);
393 7         15 $path = shift;
394 7 100       22 if (!$path) {
395 1         11 carp "Directory name required";
396 1         452 return;
397             }
398              
399 6   50     19 $stat = (shift || { });
400 6   33     18 $testname = (shift || $path);
401 6   100     27 $invert = (shift || 0);
402              
403 6         34 my $tb = Test::Config::System->builder;
404 6 100       167 unless (-d $path) {
405 2 100       13 return $tb->ok($invert ? 1 : 0, $testname);
406             }
407              
408 4         12 $res = _pathp(oct(40000), $path, $stat);
409 4 100       48 $tb->ok($invert ? !$res : $res, $testname);
410             }
411              
412             sub check_file {
413 7     7 1 5283 my ($path, $stat, $testname, $invert,$res);
414 7         20 $path = shift;
415 7 100       35 if (!$path) {
416 1         10 carp "Filename required";
417 1         458 return;
418             }
419 6   50     18 $stat = (shift || { });
420 6   33     19 $testname = (shift || $path);
421 6   100     29 $invert = (shift || 0);
422              
423 6         23 my $tb = Test::Config::System->builder;
424 6 100       161 unless (-f $path) {
425 2 100       14 return $tb->ok($invert ? 1 : 0, $testname);
426             }
427              
428 4         14 $res = _pathp(oct(100000), $path, $stat);
429 4 100       22 $tb->ok($invert ? !$res : $res, $testname);
430              
431             }
432              
433             sub _pathp {
434 8     8   33 my %tbl = ( '-uid' => 4,
435             '-gid' => 5,
436             '-mode' => 2,
437             );
438             ## Internal function, we can assume args are checked by the caller.
439 8         17 my ($type, $path, $stat) = @_;
440 8         29 my $tb = Test::Config::System->builder;
441              
442 8         64 my $res = 1;
443              
444 8 100       25 if (ref($stat) eq 'HASH') {
445 7         155 my @attrs = stat($path);
446 7         17 $attrs[2] -= $type; # Already know the type, only matching perms
447             ## Loop through each pair or until $res is false
448 7   66     62 while ((my ($key, $val) = each(%$stat)) && $res) {
449             ## Look up the key's position in the list returned from stat(),
450             ## and compare the return from stat() to the value given.
451 8 100       23 if (exists($tbl{$key})) {
452 7 100       25 $res = ($attrs[$tbl{$key}] == $val) ? 1 : 0;
453             }
454 8 100       63 $tb->diag("required: $key->$val, actual: $key->"
455             . $attrs[$tbl{$key}]) unless $res;
456             }
457             }
458 8         621 return $res; # caller will do the ok()
459             }
460              
461             =head1 AUTHOR
462              
463             Ian Kilgore, C<< >>
464              
465             =head1 BUGS
466              
467             =over 4
468              
469             =item * Never call check_file_contents with untrusted input.
470              
471             check_file_contents runs a given regexp, which can contain arbitrary
472             perl code.
473              
474             =item * check_package currently only supports dpkg and rpm
475              
476             =back
477              
478             Please report any bugs or feature requests to
479             C, or through the web interface at
480             L.
481             I will be notified, and then you'll automatically be notified of progress on
482             your bug as I make changes.
483              
484             =head1 SUPPORT
485              
486             You can find documentation for this module with the perldoc command.
487              
488             perldoc Test::Config::System
489              
490             You can also look for information at:
491              
492             =over 4
493              
494             =item * AnnoCPAN: Annotated CPAN documentation
495              
496             L
497              
498             =item * CPAN Ratings
499              
500             L
501              
502             =item * RT: CPAN's request tracker
503              
504             L
505              
506             =item * Search CPAN
507              
508             L
509              
510             =back
511              
512             =head1 ACKNOWLEDGEMENTS
513              
514             Many thanks to [naikonta] at perlmonks, for reviewing the module before
515             the release of 0.01.
516              
517             =head1 COPYRIGHT & LICENSE
518              
519             Copyright 2007 Ian Kilgore, all rights reserved.
520              
521             This program is free software; you can redistribute it and/or modify it
522             under the same terms as Perl itself.
523              
524             =head1 SEE ALSO
525              
526             L
527              
528             =cut
529              
530             1; # End of Test::Config::System