line
stmt
bran
cond
sub
pod
time
code
1
package Test::Collectd::Plugins;
2
3
5
5
313349
use 5.006;
5
19
5
316
4
5
5
28
use strict;
5
7
5
9561
5
5
5
52
use warnings;
5
21
5
210
6
5
5
30
use Carp qw(croak cluck);
5
9
5
420
7
5
5
5007
use POSIX qw/isdigit/;
5
66691
5
44
8
5
5
23944
use namespace::autoclean;
5
195462
5
35
9
5
5
3765
use Test::Collectd::Config qw(parse);
5
23
5
432
10
11
5
5
7945
BEGIN {use Package::Alias Collectd => "FakeCollectd"}
5
5
3481
5
36
5
1670
12
13
=head1 NAME
14
15
Test::Collectd::Plugins - Common out-of-band collectd plugin test suite
16
17
=head1 VERSION
18
19
Version 0.1005
20
21
=cut
22
23
our $VERSION = '0.1006';
24
25
5
5
29
use base 'Test::Builder::Module';
5
9
5
2204
26
5
5
28221
use IO::File;
5
10
5
5448
27
28
our @EXPORT = qw(load_ok read_ok read_config_ok read_values $typesdb);
29
30
our $typesdb;
31
32
sub import_extra {
33
4
4
1
215
my $class = shift;
34
4
9
my $list = shift;
35
4
9
my $args;
36
4
50
26
$args = @$list == 1 ? $list->[0] : {@$list};
37
4
11
@$list = ();
38
4
50
33
47
croak __PACKAGE__." can receive either a hash or a hash reference."
39
unless ref $args and ref $args eq "HASH";
40
4
21
for (keys %$args) {
41
0
0
0
if (/^typesdb$/i) {
42
0
0
$typesdb = $args->{$_};
43
} else {
44
0
0
push @$list, $_ => $args->{$_};
45
}
46
}
47
4
18
return;
48
}
49
50
=head1 SYNOPSIS
51
52
use Test::More;
53
use Test::Collectd::Plugins typesdb => ["/usr/share/collectd/types.db"];
54
55
plan tests => 4;
56
57
load_ok ("Collectd::Plugins::Some::Plugin");
58
read_ok ("Collectd::Plugins::Some::Plugin", "plugin_name_as_returned_by_dispatch");
59
read_config_ok ("My::Plugin", "my_plugin", "/path/to/my_plugin.conf");
60
61
my $expected = [[{{ plugin => "my_plugin", type => "gauge", values => [ 42 ] }}]];
62
my $got = read_values_config ("My::Plugin", "my_plugin", "/path/to/my_plugin.conf");
63
64
is_deeply ($got, $expected);
65
66
done_testing;
67
68
Testing collectd modules outside of collectd's perl interpreter is tedious, as you cannot
69
simply 'use' them. In fact you can't even 'use Collectd', try it and come back.
70
This module lets you test collectd plugins outside of the collectd daemon. It is supposed
71
to be the first step in testing plugins, detecting syntax errors and common mistakes.
72
There are some caveats (see dedicated section), and you should use the usual collectd testing
73
steps afterwards e.g. enabling debug at compile time, then running the collectd binary in
74
the foreground while using some logging plugin, plus some write plugin. I usually use logfile
75
to STDOUT and csv plugin.
76
77
=head1 MODULE vs. PLUGIN
78
79
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.
80
81
=head1 SUBROUTINES/METHODS
82
83
=head2 load_ok <$module> <$message>
84
85
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.
86
87
=cut
88
89
sub load_ok ($;$) {
90
16
16
1
22000
my $module = shift;
91
16
100
92
my $msg = shift || "load OK";
92
16
39
_load_module($module);
93
16
420
__PACKAGE__->builder->is_eq($@, "", $msg);
94
}
95
96
sub _load_module ($) {
97
37
37
58
my $module = shift;
98
37
2728
eval "require $module";
99
}
100
101
sub _init_plugin ($) {
102
20
50
20
71
my $plugin = shift or die "_init_plugin needs plugin name";
103
20
79
my $init = $FakeCollectd{$plugin}->{Callback}->{Init};
104
20
100
59
if (defined $init) {
105
4
303
eval "$init()";
106
} else {
107
16
30
return 1;
108
}
109
4
50
46
if ($@) {
110
0
0
return undef;
111
} else {
112
4
14
return $init;
113
}
114
}
115
116
sub _read ($) {
117
3
50
3
12
my $plugin = shift or die "_read needs plugin name";
118
3
13
my $reader = $FakeCollectd{$plugin}->{Callback}->{Read};
119
3
50
14
if (defined $reader) {
120
3
216
eval "$reader()";
121
3
63
return $reader;
122
} else {
123
0
0
eval { die "_read: No reader defined for plugin `$plugin'" };
0
0
124
0
0
return undef;
125
}
126
}
127
128
sub _reset_values ($) {
129
6
6
12
my $plugin = shift;
130
6
100
29
if (exists $FakeCollectd{$plugin}->{Values}) {
131
3
7
undef @{$FakeCollectd{$plugin}->{Values}};
3
26
132
}
133
6
26
return 1;
134
}
135
136
sub _values ($) {
137
3
50
3
15
my $plugin = shift or die "_values needs plugin name";
138
3
50
17
if (exists $FakeCollectd{$plugin}->{Values}) {
139
3
7
return @{$FakeCollectd{$plugin}->{Values}}
3
19
140
} else {
141
return undef
142
0
0
}
143
}
144
145
sub _config ($$) {
146
2
50
2
10
my $plugin = shift or die "_config(plugin,config)";
147
2
50
8
my $cfg = shift or die "_config(plugin,config)";
148
149
2
6
my $cb = $FakeCollectd{$plugin}->{Callback}->{Config};
150
2
50
7
unless ($cb) {
151
0
0
eval {croak "plugin $plugin does not provide a config callback"};
0
0
152
0
0
return undef;
153
}
154
2
50
13
my $config = Test::Collectd::Config::parse($cfg) or croak "failed to parse config";
155
# this fires up the plugin's config callback with provided config
156
5
5
40
eval {no strict "refs"; &$cb($config)}; # or croak("config callback $cb failed: $@");
5
10
5
9955
2
6
2
17
157
2
50
35
if ($@) {
158
0
0
return undef;
159
} else {
160
2
13
return $config;
161
}
162
}
163
164
=head2 plan tests => $num
165
166
See L.
167
168
=cut
169
170
#sub plan { __PACKAGE__ -> builder -> plan (@_) }
171
#sub diag { __PACKAGE__ -> builder -> diag (@_) }
172
173
=head2 read_ok <$module> <$plugin> [$message]
174
175
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:
176
177
=over 2
178
179
=cut
180
181
sub read_ok ($$;$) {
182
3
3
1
1581
my $module = shift;
183
3
7
my $plugin = shift;
184
3
50
14
my $msg = shift || "read OK";
185
186
3
32
my $tb = __PACKAGE__->builder;
187
188
$tb -> subtest($msg, sub {
189
190
3
50
3
2233
$tb -> ok (_load_module($module), "load plugin module") or $tb -> diag ($@);
191
3
50
1217
$tb -> ok (_reset_values($plugin), "reset values") or $tb -> diag ($@);
192
3
50
1139
$tb -> ok (_init_plugin($plugin),"init plugin"); $tb -> diag ($@) if $@;
3
1172
193
3
50
13
$tb -> ok (_read($plugin),"read plugin") or $tb -> diag ($@);
194
3
1745
my @values = _values ($plugin);
195
3
50
14
$tb -> ok(@values, "read callback returned some values") or $tb -> diag ($@);
196
3
2629
$tb -> ok(scalar @values, "dispatch called");
197
3
1176
for (@values) {
198
4
462
$tb->is_eq(ref $_,"ARRAY","value is array");
199
200
=item * There shall be only one and only one hashref argument
201
202
=cut
203
204
4
2743
$tb -> ok(scalar @$_, "plugin called dispatch with arguments");
205
4
1464
$tb -> cmp_ok (@$_, '>', 1, "only one value_list expected");
206
4
2858
my $ref = ref $_->[0];
207
4
21
$tb -> is_eq($ref, "HASH", "value is HASH"); # this should be handled already earlier
208
4
1963
my %dispatch = %{$_->[0]};
4
40
209
210
=item * The following keys are mandatory: plugin, type, values
211
212
=cut
213
214
4
17
for (qw(plugin type values)) {
215
12
50
2892
$tb -> ok(exists $dispatch{$_}, "mandatory key '$_' exists") or return;
216
}
217
218
=item * Only the following keys are valid: plugin, type, values, time, interval, host, plugin_instance, type_instance.
219
220
=cut
221
222
4
1553
for (keys %dispatch) {
223
28
23435
$tb -> like ($_, qr/^(plugin|type|values|time|interval|host|plugin_instance|type_instance)$/, "key $_ is valid");
224
}
225
226
=item * The key C must be present in the C file.
227
228
=cut
229
230
4
1983
my @type = _get_type($dispatch{type});
231
4
46
$tb -> ok (scalar @type, "type $dispatch{type} found in " . join (", ", @$typesdb));
232
233
=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.
234
235
=cut
236
237
4
1845
my $vref = ref $dispatch{values};
238
4
89
$tb -> is_eq ($vref, "ARRAY", "values is ARRAY");
239
4
2471
$tb -> is_eq(scalar @{$dispatch{values}}, scalar @type, "number of dispatched 'values' matches type spec for '$dispatch{type}'");
4
37
240
241
4
3193
my $i=0;
242
4
11
for (@{$dispatch{values}}) {
4
16
243
6
45
$tb -> ok (defined $_, "value $i for $dispatch{plugin} ($dispatch{type}) is defined");
244
6
2193
$i++;
245
}
246
247
=item * All other keys must be scalar strings with at most 63 characters: C, C, C, C and C.
248
249
=cut
250
251
4
14
for (qw(plugin type host plugin_instance type_instance)) {
252
20
50
8039
if (exists $dispatch{$_}) {
253
20
33
my $ref = ref $dispatch{$_};
254
20
80
$tb -> is_eq ($ref, "", "$_ is SCALAR");
255
20
10996
$tb -> cmp_ok(length $dispatch{$_}, '<', 63, "$_ is valid");
256
}
257
}
258
259
=item * The keys C and C must be a positive integers.
260
261
=cut
262
263
4
2020
for (qw(time interval)) {
264
8
100
40
if (exists $dispatch{$_}) {
265
4
22
$tb -> cmp_ok($dispatch{$_},'>',0,"$_ is valid");
266
}
267
}
268
269
=item * The keys C, C and C may use all ASCII characters except "/".
270
271
=cut
272
273
4
2713
for (qw/host plugin_instance type_instance/) {
274
12
50
4535
if (exists $dispatch{$_}) {
275
12
96
$tb -> unlike($dispatch{$_}, qr/\//, "$_ valid");
276
}
277
}
278
279
=item * The keys C and C may use all ASCII characters except "/" and "-".
280
281
=cut
282
283
4
2590
for (qw/plugin type/) {
284
8
50
2310
if (exists $dispatch{$_}) {
285
8
62
$tb -> unlike($dispatch{$_}, qr/[\/-]/, "$_ valid");
286
}
287
}
288
289
=back
290
291
=cut
292
293
}
294
3
74
}); # end subtest
295
}
296
297
=head2 read_config_ok <$module> <$plugin> <$config> [$message]
298
299
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.
300
301
=cut
302
303
sub read_config_ok ($$$;$) {
304
1
1
1
13
my $module = shift;
305
1
3
my $plugin = shift;
306
1
3
my $config = shift;
307
1
50
10
my $msg = shift || "read with config OK";
308
309
1
19
my $tb = __PACKAGE__->builder;
310
$tb -> subtest($msg, sub {
311
1
1
1035
$tb -> plan ( tests => 3 );
312
1
217
$tb -> ok (_load_module($module), "load plugin module");
313
1
50
492
$tb -> ok (_config($plugin,$config),"config ok") or $tb -> diag ($@);
314
1
512
read_ok ($module,$plugin,$msg);
315
}
316
1
27
);
317
}
318
319
320
=head2 read_values (module, plugin, [ config ])
321
322
Returns arrayref containing the list of arguments passed to L. Example:
323
324
[
325
# first call to L
326
[
327
{ plugin => "myplugin", type => "gauge", values => [ 1 ] },
328
],
329
# second call to L
330
[
331
{ plugin => "myplugin", type => "gauge", values => [ 2 ] },
332
],
333
]
334
335
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").
336
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.
337
338
=cut
339
340
sub read_values ($$;$) {
341
17
17
1
18177
my $module = shift;
342
17
32
my $plugin = shift;
343
17
30
my $config = shift;
344
17
42
_load_module($module);
345
17
74
_init_plugin($plugin);
346
# plugin with config callback
347
17
100
46
if ($config) {
348
1
5
_config($plugin,$config);
349
#unless (ref $config eq "HASH") {
350
#croak "third param to read_values must be a valid config hash";
351
#}
352
#my $cb = $FakeCollectd{$plugin}->{Callback}->{Config};
353
#unless ($cb) {
354
#croak "plugin $plugin does not provide a config callback";
355
#}
356
## this fires up the plugin's config callback with provided config
357
#eval {no strict "refs"; &$cb($config)} or croak("config callback $cb failed: $@");
358
}
359
#
360
17
60
my $reader = $FakeCollectd{$plugin}->{Callback}->{Read};
361
17
100
115
return unless $reader;
362
3
148
_reset_values($plugin);
363
3
220
eval "$reader()";
364
3
50
51
return if $@;
365
3
50
18
if (exists $FakeCollectd{$plugin}->{Values}) {
366
3
7
@{$FakeCollectd{$plugin}->{Values}};
3
22
367
} else {
368
0
0
return;
369
}
370
}
371
372
sub _get_type {
373
4
4
15
my $type = shift;
374
4
100
216
if ($typesdb) {
375
2
7
my $ref = ref $typesdb;
376
2
50
14
if ($ref eq "HASH") {
50
377
0
0
warn "typesdb is a hash, discarding its keys";
378
0
0
$typesdb = [values %$typesdb];
379
} elsif ($ref eq "") {
380
0
0
$typesdb = [ $typesdb ];
381
}
382
} else {
383
2
28
require File::ShareDir;
384
2
17
$typesdb = [ File::ShareDir::module_file(__PACKAGE__, "types.db") ];
385
2
1299
warn "no typesdb - using builtin ", join ", ", @$typesdb;
386
}
387
4
18
for my $file (@$typesdb) {
388
4
46
my $fh = IO::File -> new($file, "r");
389
4
50
889
unless ($fh) {
390
0
0
cluck "Error opening types.db: $!";
391
0
0
return undef;
392
}
393
4
135
while (<$fh>) {
394
289
684
my ($t, @ds) = split /\s+/, $_;
395
289
100
955
if ($t eq $type) {
396
4
15
my @ret;
397
4
14
for (@ds) {
398
6
25
my @stuff = split /:/;
399
6
190
push @ret, {
400
ds => $stuff[0],
401
type => $stuff[1],
402
min => $stuff[2],
403
max => $stuff[3],
404
};
405
}
406
4
525
return @ret;
407
}
408
}
409
}
410
0
return ();
411
}
412
413
=head1 CAVEATS
414
415
=head2 FakeCollectd
416
417
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.
418
419
=head2 methods
420
421
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.
422
423
=head2 config
424
425
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.
426
427
=head2 types.db
428
429
If no types.db list is being specified during construction, the object will try to use the shipped version.
430
Also, if a list is given, the first appearance of the type will be used; this may differ from collectd's mechanism.
431
432
=head2 SEE ALSO
433
434
L, L
435
436
=head1 AUTHOR
437
438
Fabien Wernli, C<< >>
439
440
=head1 BUGS
441
442
Please report any bugs or feature requests to L.
443
444
=head1 SUPPORT
445
446
You can find documentation for this module with the perldoc command.
447
448
perldoc Test::Collectd::Plugins
449
450
You can also look for information at:
451
452
=over 4
453
454
=item * Github: https://github.com/faxm0dem/Test-Collectd-Plugins
455
456
=item * RT: CPAN's request tracker (report bugs here)
457
458
L
459
460
=item * AnnoCPAN: Annotated CPAN documentation
461
462
L
463
464
=item * CPAN Ratings
465
466
L
467
468
=item * Search CPAN
469
470
L
471
472
=back
473
474
475
=head1 LICENSE AND COPYRIGHT
476
477
Copyright 2012 Fabien Wernli.
478
479
This program is free software; you can redistribute it and/or modify it
480
under the terms of either: the GNU General Public License as published
481
by the Free Software Foundation; or the Artistic License.
482
483
See http://dev.perl.org/licenses/ for more information.
484
485
486
=cut
487
488
1; # End of Test::Collectd::Plugins
489