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 and C must be a positive integers.
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