File Coverage

blib/lib/Test/Collectd/Plugins.pm
Criterion Covered Total %
statement 166 185 89.7
branch 43 74 58.1
condition 5 9 55.5
subroutine 24 24 100.0
pod 5 5 100.0
total 243 297 81.8


line stmt bran cond sub pod time code
1             package Test::Collectd::Plugins;
2              
3 5     5   164415 use 5.006;
  5         12  
4 5     5   18 use strict;
  5         6  
  5         103  
5 5     5   16 use warnings;
  5         8  
  5         115  
6 5     5   14 use Carp qw(croak cluck);
  5         6  
  5         259  
7 5     5   2117 use namespace::autoclean;
  5         63494  
  5         17  
8 5     5   2330 use Test::Collectd::Config qw(parse);
  5         20  
  5         358  
9              
10 5     5   2542 BEGIN {use Package::Alias Collectd => "FakeCollectd"}
  5         2054  
  5         24  
  5         1095  
11              
12             =head1 NAME
13              
14             Test::Collectd::Plugins - Common out-of-band collectd plugin test suite
15              
16             =head1 VERSION
17              
18             Version 0.1008
19              
20             =cut
21              
22             our $VERSION = '0.1009';
23              
24 5     5   7 use base 'Test::Builder::Module';
  5         1125  
  5         16834  
25 5     5   5 use IO::File;
  5         3053  
  5         22  
26              
27             our @EXPORT = qw(load_ok read_ok read_config_ok read_values $typesdb);
28              
29             our $typesdb;
30              
31             sub import_extra {
32 4     4 1 3 my $class = shift;
33 4         5 my $list = shift;
34 4         14 my $args;
35 4 50       5 $args = @$list == 1 ? $list->[0] : {@$list};
36 4         31 @$list = ();
37 4 50 33     10 croak __PACKAGE__." can receive either a hash or a hash reference."
38             unless ref $args and ref $args eq "HASH";
39 0         0 for (keys %$args) {
40 0 0       0 if (/^typesdb$/i) {
41 0         0 $typesdb = $args->{$_};
42             } else {
43 4         9 push @$list, $_ => $args->{$_};
44             }
45             }
46 16         9218 return;
47             }
48              
49             =head1 SYNOPSIS
50              
51             use Test::More;
52             use Test::Collectd::Plugins typesdb => ["/usr/share/collectd/types.db"];
53              
54             plan tests => 4;
55              
56             load_ok ("Collectd::Plugins::Some::Plugin");
57             read_ok ("Collectd::Plugins::Some::Plugin", "plugin_name_as_returned_by_dispatch");
58             read_config_ok ("My::Plugin", "my_plugin", "/path/to/my_plugin.conf");
59              
60             my $expected = [[{{ plugin => "my_plugin", type => "gauge", values => [ 42 ] }}]];
61             my $got = read_values_config ("My::Plugin", "my_plugin", "/path/to/my_plugin.conf");
62              
63             is_deeply ($got, $expected);
64              
65             done_testing;
66              
67             Testing collectd modules outside of collectd's perl interpreter is tedious, as you cannot
68             simply 'use' them. In fact you can't even 'use Collectd', try it and come back.
69             This module lets you test collectd plugins outside of the collectd daemon. It is supposed
70             to be the first step in testing plugins, detecting syntax errors and common mistakes.
71             There are some caveats (see dedicated section), and you should use the usual collectd testing
72             steps afterwards e.g. enabling debug at compile time, then running the collectd binary in
73             the foreground while using some logging plugin, plus some write plugin. I usually use logfile
74             to STDOUT and csv plugin.
75              
76             =head1 MODULE vs. PLUGIN
77              
78             Most methods will accept either $plugin or $module or both. They correspond to C's C and C respectively. It's easy to mistake one for the other. While $module is as its name suggests the perl module's name, $plugin corresponds to the collectd plugin's name, as called by plugin_dispatch_values. This difference makes it possible for a plugin to dispatch values on behalf of another, or to register multiple plugins. Make sure you ask the methods the right information.
79              
80             =head1 SUBROUTINES/METHODS
81              
82             =head2 load_ok <$module> <$message>
83              
84             Tries to load the plugin module. As collectd-perl plugin modules contain direct calls (upon loading) to L, the former are intercepted by L which is part of this distribution. This has the effect of populating the %FakeCollectd hash. See L for more info.
85              
86             =cut
87              
88             sub load_ok ($;$) {
89 16     16 1 69 my $module = shift;
90 16   100     26 my $msg = shift || "load OK";
91 16         250 _load_module($module);
92 40         40 __PACKAGE__->builder->is_eq($@, "", $msg);
93             }
94              
95             sub _load_module ($) {
96 40     40   2324 my $module = shift;
97 22         54 eval "require $module";
98             }
99              
100             sub _init_plugin ($) {
101 22 50   22   54 my $plugin = shift or die "_init_plugin needs plugin name";
102 22         42 my $init = $FakeCollectd{$plugin}->{Callback}->{Init};
103 4 100       189 if (defined $init) {
104 18         25 eval "$init()";
105             } else {
106 4         35 return 1;
107             }
108 0 50       0 if ($@) {
109 4         10 return undef;
110             } else {
111 4         11 return $init;
112             }
113             }
114              
115             sub _read ($) {
116 4 50   4   7 my $plugin = shift or die "_read needs plugin name";
117 4         12 my $reader = $FakeCollectd{$plugin}->{Callback}->{Read};
118 4 50       222 if (defined $reader) {
119 4         66 eval "$reader()";
120 0         0 return $reader;
121             } else {
122 0         0 eval { die "_read: No reader defined for plugin `$plugin'" };
  0         0  
123 8         12 return undef;
124             }
125             }
126              
127             sub _reset_values ($) {
128 8     8   24 my $plugin = shift;
129 4 100       4 if (exists $FakeCollectd{$plugin}->{Values}) {
130 4         13 undef @{$FakeCollectd{$plugin}->{Values}};
  8         16  
131             }
132 4         13 return 1;
133             }
134              
135             sub _values ($) {
136 4 50   4   14 my $plugin = shift or die "_values needs plugin name";
137 4 50       4 if (exists $FakeCollectd{$plugin}->{Values}) {
138 4         13 return @{$FakeCollectd{$plugin}->{Values}}
  0         0  
139             } else {
140             return undef
141 4         10 }
142             }
143              
144             sub _config ($$) {
145 4 50   4   11 my $plugin = shift or die "_config(plugin,config)";
146 4 50       8 my $cfg = shift or die "_config(plugin,config)";
147              
148 4         8 my $cb = $FakeCollectd{$plugin}->{Callback}->{Config};
149 0 50       0 unless ($cb) {
150 0         0 eval {croak "plugin $plugin does not provide a config callback"};
  0         0  
151 4         13 return undef;
152             }
153 4 50       7 my $config = Test::Collectd::Config::parse($cfg) or croak "failed to parse config";
154             # this fires up the plugin's config callback with provided config
155 5     5   6 eval {no strict "refs"; &$cb($config)}; # or croak("config callback $cb failed: $@");
  5         5426  
  4         135  
  4         18  
  4         43  
156 0 50       0 if ($@) {
157 4         14 return undef;
158             } else {
159 4         723 return $config;
160             }
161             }
162              
163             =head2 plan tests => $num
164              
165             See L.
166              
167             =cut
168              
169             #sub plan { __PACKAGE__ -> builder -> plan (@_) }
170             #sub diag { __PACKAGE__ -> builder -> diag (@_) }
171              
172             =head2 read_ok <$module> <$plugin> [$message]
173              
174             Loads the plugin module identified by $module, then tries to fire up the registered read callback for this plugin ($plugin), while intercepting all calls to L, storing its arguments into the %FakeCollectd hash. The latter are checked against the following rules, which match the collectd guidelines:
175              
176             =over 2
177              
178             =cut
179              
180             sub read_ok ($$;$) {
181 4     4 1 6 my $module = shift;
182 4         13 my $plugin = shift;
183 4   50     18 my $msg = shift || "read OK";
184              
185 4         62 my $tb = __PACKAGE__->builder;
186              
187             $tb -> subtest($msg, sub {
188              
189 4 50   4   1267 $tb -> ok (_load_module($module), "load plugin module") or $tb -> diag ($@);
190 4 50       937 $tb -> ok (_reset_values($module), "reset values") or $tb -> diag ($@);
191 4 50       955 $tb -> ok (_init_plugin($plugin),"init plugin"); $tb -> diag ($@) if $@;
  4         13  
192 4 50       1112 $tb -> ok (_read($plugin),"read plugin") or $tb -> diag ($@);
193 4         12 my @values = _values ($module);
194 4 50       935 $tb -> ok(@values, "read callback returned some values") or $tb -> diag ($@);
195 4         914 $tb -> ok(scalar @values, "dispatch called");
196 5         339 for (@values) {
197 5         1620 $tb->is_eq(ref $_,"ARRAY","value is array");
198              
199             =item * There shall be only one and only one hashref argument
200              
201             =cut
202              
203 5         1125 $tb -> ok(scalar @$_, "plugin called dispatch with arguments");
204 5         1502 $tb -> cmp_ok (@$_, '>', 1, "only one value_list expected");
205 5         17 my $ref = ref $_->[0];
206 5         1573 $tb -> is_eq($ref, "HASH", "value is HASH"); # this should be handled already earlier
207 5         34 my %dispatch = %{$_->[0]};
  5         14  
208              
209             =item * The following keys are mandatory: plugin, type, values
210              
211             =cut
212              
213 15         2258 for (qw(plugin type values)) {
214 5 50       1133 $tb -> ok(exists $dispatch{$_}, "mandatory key '$_' exists") or return;
215             }
216              
217             =item * Only the following keys are valid: plugin, type, values, time, interval, host, plugin_instance, type_instance.
218              
219             =cut
220              
221 35         9373 for (keys %dispatch) {
222 5         1475 $tb -> like ($_, qr/^(plugin|type|values|time|interval|host|plugin_instance|type_instance)$/, "key $_ is valid");
223             }
224              
225             =item * The key C must be present in the C file.
226              
227             =cut
228              
229 5         37 my @type = _get_type($dispatch{type});
230 5         1293 $tb -> ok (scalar @type, "type $dispatch{type} found in " . join (", ", @$typesdb));
231              
232             =item * The key C must be an array reference and the number of elements must match its data type in module's configuration option C.
233              
234             =cut
235              
236 5         16 my $vref = ref $dispatch{values};
237 5         1652 $tb -> is_eq ($vref, "ARRAY", "values is ARRAY");
238 5         28 $tb -> is_eq(scalar @{$dispatch{values}}, scalar @type, "number of dispatched 'values' matches type spec for '$dispatch{type}'");
  5         1616  
239              
240 5         8 my $i=0;
241 5         13 for (@{$dispatch{values}}) {
  9         47  
242 9         2173 $tb -> ok (defined $_, "value $i for $dispatch{plugin} ($dispatch{type}) is defined");
243 5         13 $i++;
244             }
245              
246             =item * All other keys must be scalar strings with at most 63 characters: C, C, C, C and C.
247              
248             =cut
249              
250 25         6236 for (qw(plugin type host plugin_instance type_instance)) {
251 25 50       35 if (exists $dispatch{$_}) {
252 25         87 my $ref = ref $dispatch{$_};
253 25         8100 $tb -> is_eq ($ref, "", "$_ is SCALAR");
254 5 50       1627 $tb -> cmp_ok(length $dispatch{$_}, '<', 63, "$_ is valid") if $dispatch{$_};
255             }
256             }
257              
258             =item * The keys C
259              
260             =cut
261              
262 10         30 for (qw(time interval)) {
263 5 100       23 if (exists $dispatch{$_}) {
264 5         1687 $tb -> cmp_ok($dispatch{$_},'>',0,"$_ is valid");
265             }
266             }
267              
268             =item * The keys C, C and C may use all ASCII characters except "/".
269              
270             =cut
271              
272 15         3332 for (qw/host plugin_instance type_instance/) {
273 15 50       87 if (exists $dispatch{$_}) {
274 5         1658 $tb -> unlike($dispatch{$_}, qr/\//, "$_ valid");
275             }
276             }
277              
278             =item * The keys C and C may use all ASCII characters except "/" and "-".
279              
280             =cut
281              
282 10         1888 for (qw/plugin type/) {
283 10 50       51 if (exists $dispatch{$_}) {
284 2         13 $tb -> unlike($dispatch{$_}, qr/[\/-]/, "$_ valid");
285             }
286             }
287              
288             =back
289              
290             =cut
291              
292             }
293 4         1657 }); # end subtest
294             }
295              
296             =head2 read_config_ok <$module> <$plugin> <$config> [$message]
297              
298             Same as L but also reads configuration from $plugin_config and fires up the configuration callback of plugin $plugin_module. L will kindly format a configuration file or handle to suit this subroutine.
299              
300             =cut
301              
302             sub read_config_ok ($$$;$) {
303 2     2 1 3 my $module = shift;
304 2         2 my $plugin = shift;
305 2         10 my $config = shift;
306 2   50     15 my $msg = shift || "read with config OK";
307              
308 2         28 my $tb = __PACKAGE__->builder;
309             $tb -> subtest($msg, sub {
310 2     2   269 $tb -> plan ( tests => 3 );
311 2         655 $tb -> ok (_load_module($module), "load plugin module");
312 2 50       623 $tb -> ok (_config($plugin,$config),"config ok") or $tb -> diag ($@);
313 18         10279 read_ok ($module,$plugin,$msg);
314             }
315 2         1041 );
316             }
317              
318              
319             =head2 read_values (module, plugin, [ config ])
320              
321             Returns arrayref containing the list of arguments passed to L. Example:
322              
323             [
324             # first call to L
325             [
326             { plugin => "myplugin", type => "gauge", values => [ 1 ] },
327             ],
328             # second call to L
329             [
330             { plugin => "myplugin", type => "gauge", values => [ 2 ] },
331             ],
332             ]
333              
334             A config hash can be provided for plugins with a config callback. The format of this hash must be the same as the one described in C's manpage (grep for "Config-Item").
335             Use L for conveniently yielding such a hash from a collectd configuration file. Only the section concerning the plugin should be provided, e.g. without all global collectd config sections.
336              
337             =cut
338              
339             sub read_values ($$;$) {
340 18     18 1 22 my $module = shift;
341 18         17 my $plugin = shift;
342 18         31 my $config = shift;
343 18         56 _load_module($module);
344 18         31 _init_plugin($plugin);
345             # plugin with config callback
346 2 100       6 if ($config) {
347 18         28 _config($plugin,$config);
348             #unless (ref $config eq "HASH") {
349             #croak "third param to read_values must be a valid config hash";
350             #}
351             #my $cb = $FakeCollectd{$plugin}->{Callback}->{Config};
352             #unless ($cb) {
353             #croak "plugin $plugin does not provide a config callback";
354             #}
355             ## this fires up the plugin's config callback with provided config
356             #eval {no strict "refs"; &$cb($config)} or croak("config callback $cb failed: $@");
357             }
358             #
359 18         79 my $reader = $FakeCollectd{$plugin}->{Callback}->{Read};
360 4 100       11 return unless $reader;
361 4         216 _reset_values($plugin);
362 4         59 eval "$reader()";
363 4 50       12 return if $@;
364 4 50       8 if (exists $FakeCollectd{$plugin}->{Values}) {
365 4         19 @{$FakeCollectd{$plugin}->{Values}};
  0         0  
366             } else {
367 5         8 return;
368             }
369             }
370              
371             sub _get_type {
372 5     5   13 my $type = shift;
373 3 100       7 if ($typesdb) {
374 3         18 my $ref = ref $typesdb;
375 0 50       0 if ($ref eq "HASH") {
    50          
376 0         0 warn "typesdb is a hash, discarding its keys";
377 0         0 $typesdb = [values %$typesdb];
378             } elsif ($ref eq "") {
379 2         13 $typesdb = [ $typesdb ];
380             }
381             } else {
382 2         12 require File::ShareDir;
383 2         576 $typesdb = [ File::ShareDir::module_file(__PACKAGE__, "types.db") ];
384 5         15 warn "no typesdb - using builtin ", join ", ", @$typesdb;
385             }
386 5         37 for my $file (@$typesdb) {
387 5         527 my $fh = IO::File -> new($file, "r");
388 0 50       0 unless ($fh) {
389 0         0 cluck "Error opening types.db: $!";
390 5         86 return undef;
391             }
392 377         790 while (<$fh>) {
393 377         818 my ($t, @ds) = split /\s+/, $_;
394 5 100       5 if ($t eq $type) {
395 5         10 my @ret;
396 9         18 for (@ds) {
397 9         52 my @stuff = split /:/;
398 5         870 push @ret, {
399             ds => $stuff[0],
400             type => $stuff[1],
401             min => $stuff[2],
402             max => $stuff[3],
403             };
404             }
405 0           return @ret;
406             }
407             }
408             }
409             return ();
410             }
411              
412             =head1 CAVEATS
413              
414             =head2 FakeCollectd
415              
416             This module tricks the tested collectd plugins into loading L instead of L, and replaces calls thereof by simple functions which populate the %FakeCollectd:: hash in order to store its arguments. As it uses the name of the calling plugin module for its symbols, subsequent calls to the test subs are not really independant, which is suboptimal especially for a test module. If you have a saner solution to do this, please let me know.
417              
418             =head2 methods
419              
420             Replacements for most common L methods are implemented, as well as constants. We may have missed some or many, and as new ones are added to the main collectd tree, we will have to keep up to date.
421              
422             =head2 config
423              
424             Although L has been a straight port of C (which itself is using C) to L/L, you might get different results in edge cases.
425              
426             =head2 types.db
427              
428             If no types.db list is being specified during construction, the object will try to use the shipped version.
429             Also, if a list is given, the first appearance of the type will be used; this may differ from collectd's mechanism.
430              
431             =head2 SEE ALSO
432              
433             L, L
434              
435             =head1 AUTHOR
436              
437             Fabien Wernli, C<< >>
438              
439             =head1 BUGS
440              
441             Please report any bugs or feature requests to L.
442              
443             =head1 SUPPORT
444              
445             You can find documentation for this module with the perldoc command.
446              
447             perldoc Test::Collectd::Plugins
448              
449             You can also look for information at:
450              
451             =over 4
452              
453             =item * Github: https://github.com/faxm0dem/Test-Collectd-Plugins
454              
455             =item * RT: CPAN's request tracker (report bugs here)
456              
457             L
458              
459             =item * AnnoCPAN: Annotated CPAN documentation
460              
461             L
462              
463             =item * CPAN Ratings
464              
465             L
466              
467             =item * Search CPAN
468              
469             L
470              
471             =back
472              
473              
474             =head1 LICENSE AND COPYRIGHT
475              
476             Copyright 2012 Fabien Wernli.
477              
478             This program is free software; you can redistribute it and/or modify it
479             under the terms of either: the GNU General Public License as published
480             by the Free Software Foundation; or the Artistic License.
481              
482             See http://dev.perl.org/licenses/ for more information.
483              
484              
485             =cut
486              
487             1; # End of Test::Collectd::Plugins
488