File Coverage

blib/lib/Test/Clustericious/Log.pm
Criterion Covered Total %
statement 155 172 90.1
branch 49 64 76.5
condition 4 4 100.0
subroutine 20 21 95.2
pod 4 4 100.0
total 232 265 87.5


line stmt bran cond sub pod time code
1             package Test::Clustericious::Log;
2              
3 35     35   257505 use strict;
  35         124  
  35         961  
4 35     35   172 use warnings;
  35         65  
  35         785  
5 35     35   597 use 5.010001;
  35         141  
6 35     35   1851 use Test2::Plugin::FauxHomeDir;
  35         340058  
  35         281  
7 35     35   11177 use File::Glob qw( bsd_glob );
  35         79  
  35         2024  
8 35     35   4802 use Clustericious::Log ();
  35         138  
  35         693  
9 35     35   229 use Carp qw( carp );
  35         70  
  35         1614  
10 35     35   195 use base qw( Exporter );
  35         78  
  35         3625  
11 35     35   204 use Test2::API qw( context );
  35         66  
  35         1486  
12 35     35   2302 use YAML::XS qw( Dump );
  35         13682  
  35         42190  
13              
14             our @EXPORT = qw( log_events log_context log_like log_unlike );
15             our %EXPORT_TAGS = ( all => \@EXPORT );
16              
17             # ABSTRACT: Clustericious logging in tests.
18             our $VERSION = '1.27'; # VERSION
19              
20              
21             sub log_events
22             {
23 35     35 1 2150 @{ Test::Clustericious::Log::Appender->new->{list} };
  35         99  
24             }
25              
26              
27             sub log_context (&)
28             {
29 10     10 1 12747 my($code) = @_;
30 10         43 my $old = Test::Clustericious::Log::Appender->new->{list};
31 10         32 local Test::Clustericious::Log::Appender->new->{list} = [];
32            
33 10         22 my @ret;
34             my $ret;
35            
36 10 100       33 if(wantarray)
    100          
37             {
38 3         9 @ret = $code->()
39             }
40             elsif(defined wantarray)
41             {
42 1         7 $ret = $code->();
43             }
44             else
45             {
46 6         15 $code->();
47             }
48            
49 10         5435 push @$old, @{ Test::Clustericious::Log::Appender->new->{list} };
  10         33  
50              
51 10 100       47 wantarray ? @ret : $ret;
52             }
53              
54              
55             sub _event_match
56             {
57 147     147   210 my($pattern, $event) = @_;
58              
59 147         164 my $match = 1;
60 147         239 foreach my $key (keys %$pattern)
61             {
62 162         207 my $pattern = $pattern->{$key};
63 162 100       269 if(ref $pattern eq 'Regexp')
64             {
65 125 100       460 $match = 0 unless $event->{$key} =~ $pattern;
66             }
67             else
68             {
69 37 100       76 $match = 0 unless $event->{$key} eq $pattern;
70             }
71             }
72            
73 147         298 $match;
74             }
75              
76              
77             sub log_like ($;$)
78             {
79 12     12 1 3725 my($pattern, $message) = @_;
80            
81 12   100     45 $message ||= "log matches pattern";
82 12 100       34 $pattern = { message => $pattern } unless ref $pattern eq 'HASH';
83            
84 12         28 my $ctx = context();
85 12         730 my $ok = 0;
86            
87 12         26 foreach my $event (log_events)
88             {
89 55 100       80 if(_event_match($pattern, $event))
90             {
91 11         21 $ok = 1;
92 11         16 last;
93             }
94             }
95            
96 12         44 $ctx->ok($ok, $message);
97              
98 12 100       1632 unless($ok)
99             {
100            
101 1         7 $ctx->diag("None of the events matched the pattern:");
102 1         209 $ctx->diag(
103             Dump({
104             events => [log_events],
105             pattern => $pattern,
106             })
107             );
108             }
109            
110 12         259 $ctx->release;
111            
112 12         302 $ok;
113             }
114              
115             sub log_unlike ($;$)
116             {
117 9     9 1 14081 my($pattern, $message) = @_;
118            
119 9   100     45 $message ||= "log does not match pattern";
120 9 100       38 $pattern = { message => $pattern } unless ref $pattern eq 'HASH';
121              
122 9         31 my $ctx = context();
123 9         646 my @match;
124            
125 9         25 foreach my $event (log_events)
126             {
127 92 100       145 if(_event_match($pattern, $event))
128             {
129 3         6 push @match, $event;
130             }
131             }
132            
133 9         39 $ctx->ok(!scalar @match, $message);
134            
135 9         1496 foreach my $match (@match)
136             {
137 3         133 $ctx->diag("This event matched, but should not have:");
138 3         623 $ctx->diag(
139             Dump({
140             event => $match,
141             pattern => $pattern,
142             })
143             );
144             }
145              
146 9         267 $ctx->release;
147            
148 9         215 !scalar @match;
149             }
150              
151              
152             sub import
153             {
154 43     43   6105 my($class) = shift;
155              
156             # first caller wins
157 43         90 state $counter = 0;
158 43 100       190 if($counter++)
159             {
160 9         32 my $caller = caller;
161 9 50       42 unless($caller eq 'Test::Clustericious::Cluster')
162             {
163 0         0 my $ctx = context();
164 0         0 $ctx->diag("you must use Test::Clustericious::Log before Test::Clustericious::Cluster");
165 0         0 $ctx->release;
166             }
167 9         176 return;
168             }
169              
170 34         846 my $home = bsd_glob('~');
171 34 100       1485 mkdir "$home/etc" unless -d "$home/etc";
172 34 50       2203 mkdir "$home/log" unless -d "$home/log";
173              
174 34         200 my $config = {
175             FileX => [ 'TRACE', 'FATAL' ],
176             NoteX => [ 'DEBUG', 'WARN' ],
177             DiagX => [ 'ERROR', 'FATAL' ],
178             TestX => [ 'TRACE', 'FATAL' ],
179             };
180              
181 34         80 my $args;
182 34 50       127 if(@_ == 1)
183             {
184 0         0 die;
185             }
186             else
187             {
188 34         88 $args = { @_ };
189             }
190            
191 34         204 foreach my $type (qw( file note diag ))
192             {
193 102 100       258 if(defined $args->{$type})
194             {
195 6         18 my $name = ucfirst($type) . 'X';
196 6 100       40 if($args->{$type} =~ /^(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)(..(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)|)$/)
    100          
    50          
197             {
198 2         6 my($min,$max) = ($1,$3);
199 2 100       5 $max = $min unless $max;
200 2         10 $config->{$name} = [ $min, $max ];
201             }
202             elsif($args->{$type} eq 'NONE')
203             {
204 3         13 delete $config->{$name};
205             }
206             elsif($args->{$type} eq 'ALL')
207             {
208 1         3 $config->{$name} = [ 'TRACE', 'FATAL' ];
209             }
210             else
211             {
212 0         0 carp "illegal log range: " . $args->{$type};
213             }
214             }
215             }
216            
217 34         1636 open my $fh, '>', "$home/etc/log4perl.conf";
218              
219 34         237 print $fh "log4perl.rootLogger=TRACE, ";
220 34 50       151 print $fh "FileX, " if defined $config->{FileX};
221 34 50       123 print $fh "NoteX, " if defined $config->{NoteX};
222 34 100       104 print $fh "DiagX, " if defined $config->{DiagX};
223 34 50       117 print $fh "TestX, " if defined $config->{TestX};
224 34         81 print $fh "\n";
225            
226 34         201 while(my($appender, $levels) = each %$config)
227             {
228 133         228 my($min, $max) = @{ $levels };
  133         330  
229 133         279 print $fh "log4perl.filter.Match$appender = Log::Log4perl::Filter::LevelRange\n";
230 133         287 print $fh "log4perl.filter.Match$appender.LevelMin = $min\n";
231 133         227 print $fh "log4perl.filter.Match$appender.LevelMax = $max\n";
232 133         500 print $fh "log4perl.filter.Match$appender.AcceptOnMatch = true\n";
233             }
234            
235 34         70 print $fh "log4perl.appender.FileX=Log::Log4perl::Appender::File\n";
236 34         91 print $fh "log4perl.appender.FileX.filename=$home/log/test.log\n";
237 34         62 print $fh "log4perl.appender.FileX.mode=append\n";
238 34         67 print $fh "log4perl.appender.FileX.layout=PatternLayout\n";
239 34         60 print $fh "log4perl.appender.FileX.layout.ConversionPattern=[%P %p{1} %rms] %F:%L %m%n\n";
240 34         58 print $fh "log4perl.appender.FileX.Filter=MatchFileX\n";
241              
242 34         61 print $fh "log4perl.appender.TestX=Test::Clustericious::Log::Appender\n";
243 34         52 print $fh "log4perl.appender.TestX.layout=PatternLayout\n";
244 34         69 print $fh "log4perl.appender.TestX.layout.ConversionPattern=%m\n";
245 34         61 print $fh "log4perl.appender.TestX.Filter=MatchTestX\n";
246            
247 34         132 print $fh "log4perl.appender.NoteX=Log::Log4perl::Appender::TAP\n";
248 34         59 print $fh "log4perl.appender.NoteX.method=note\n";
249 34         58 print $fh "log4perl.appender.NoteX.layout=PatternLayout\n";
250 34         64 print $fh "log4perl.appender.NoteX.layout.ConversionPattern=%5p %m%n\n";
251 34         52 print $fh "log4perl.appender.NoteX.Filter=MatchNoteX\n";
252              
253 34         55 print $fh "log4perl.appender.DiagX=Log::Log4perl::Appender::TAP\n";
254 34         51 print $fh "log4perl.appender.DiagX.method=diag\n";
255 34         56 print $fh "log4perl.appender.DiagX.layout=PatternLayout\n";
256 34         80 print $fh "log4perl.appender.DiagX.layout.ConversionPattern=%5p %m%n\n";
257 34         55 print $fh "log4perl.appender.DiagX.Filter=MatchDiagX\n";
258            
259 34         1185 close $fh;
260              
261 34 100       1110 if($args->{import})
262             {
263 2 50       8 @_ = ($class, ref $args->{import} ? @{ $args->{import} } : ($args->{import}));
  0         0  
264 2         226 goto &Exporter::import;
265             }
266              
267             }
268              
269             sub _summary
270             {
271 0     0   0 my($ctx, $real, $new) = @_;
272            
273 0         0 my $home = bsd_glob('~');
274            
275 0         0 my $hub = $ctx->hub;
276            
277 0 0       0 if($hub->failed)
278             {
279 0 0       0 if($ENV{CLUSTERICIOUS_LOG_SPEW_OFF})
    0          
280             {
281 0         0 $ctx->diag("not spewing the entire log (unset CLUSTERICIOUS_LOG_SPEW_OFF to turn back on)");
282             }
283             elsif(-r "$home/log/test.log")
284             {
285 0         0 $ctx->diag("detailed log");
286 0         0 open my $fh, '<', "$home/log/test.log";
287 0         0 $ctx->diag(<$fh>);
288 0         0 close $fh;
289             }
290             else
291             {
292 0         0 $ctx->diag("no detailed log");
293             }
294             }
295             }
296              
297             package Test::Clustericious::Log::Appender;
298              
299 35     35   2962 use Storable ();
  35         16682  
  35         536  
300 35     35   176 use Carp ();
  35         68  
  35         4422  
301             our @ISA = qw( Log::Log4perl::Appender );
302              
303             sub new
304             {
305 87     87   251418 my($class) = @_;
306            
307 87 50       260 Carp::croak "not subclassable"
308             unless $class eq __PACKAGE__;
309            
310 87         137 state $self;
311            
312 87 100       191 unless(defined $self)
313             {
314 22         182 $self = bless { list => [] }, __PACKAGE__;
315             }
316            
317 87         522 $self;
318             }
319              
320             sub log
321             {
322 566     566   817022 my($self, %args) = @_;
323            
324 566         1129 push @{ $self->{list} }, Storable::dclone(\%args);
  566         17869  
325              
326 566         2422 ();
327             }
328              
329             1;
330              
331             __END__
332              
333             =pod
334              
335             =encoding UTF-8
336              
337             =head1 NAME
338              
339             Test::Clustericious::Log - Clustericious logging in tests.
340              
341             =head1 VERSION
342              
343             version 1.27
344              
345             =head1 SYNOPSIS
346              
347             use Test::Clustericious::Log;
348             use Test::More;
349             use MyClustericiousApp;
350            
351             my $app = MyClustericiousApp->new;
352            
353             ok $test, 'test description';
354             ...
355              
356             =head1 DESCRIPTION
357              
358             This module redirects the L<Log::Log4perl> output from a
359             L<Clustericious> application to TAP using L<Test2::API>. By default
360             it sends DEBUG to WARN messages to C<note> and ERROR to FATAL to
361             C<diag>, so you should only see error and fatal messages if you run
362             C<prove -l> on your test but will see debug and warn messages if you run
363             C<prove -lv>.
364              
365             If the test fails for any reason, the entire log file will be printed
366             out using C<diag> when the test is complete. This is useful for CPAN
367             testers reports.
368              
369             In order to control the verbosity of the various logs, you can specify a
370             range of level for each of C<note>, C<diag> and C<file> (file being the
371             log file that is spewed IF the test file as a whole fails).
372              
373             use Test::Clustericious::Log note => 'TRACE..ERROR', diag => 'FATAL';
374              
375             Note that only one set of ranges can be specified for the entire
376             process, so the first module that uses L<Test::Clustericious::Log> gets
377             to specify the ranges. The defaults are somewhat reasonable: the log
378             file gets everything (C<TRACE..FATAL>), C<note> gets most stuff
379             (C<DEBUG..WARN>) and C<diag> gets errors, including fatal errors
380             (C<ERROR..FATAL>).
381              
382             This module also provides some functions for testing the log events of a
383             Clustericious application.
384              
385             =head1 FUNCTIONS
386              
387             In order to import functions from L<Test::Clustericious::Log>, you must
388             pass an "import" to your use line. The value is a list in the usual
389             L<Exporter> format.
390              
391             use Test::Clustericious::Log import => ':all';
392             use Test::Clustericious::Log import => [ 'log_events', 'log_like' ];
393              
394             =head2 log_events
395              
396             my @events = log_events;
397              
398             Returns the set of log events for the current log scope as a list of
399             hash references.
400              
401             =head2 log_context
402              
403             log_context {
404             # code
405             }
406              
407             Creates a log context for other L<Test::Clustericious::Log> functions to
408             operate on.
409              
410             =head2 log_like
411              
412             log_like \%pattern, $message;
413             log_like $pattern, $message;
414              
415             Test that at least one log event in the given context matches the
416             pattern defined by C<\%pattern> or C<$patter>. If you provide a hash
417             reference, then each key in the event much match the pattern values.
418             The pattern values may be either strings or regular expressions. If you
419             use the scalar form (second) then the pattern (either a regular
420             expression or string) must match the events message element.
421              
422             Note that only ONE message in the current context has to match because
423             usually you want to make sure that particular message shows up in the
424             log, but you don't care if other messages get added at a later time, and
425             you do not want that common type of change to cause tests to break.
426              
427             Examples:
428              
429             ERROR "Some error";
430             INFO "Exact message";
431             NOTE "some notice";
432            
433             log_like 'Exact message", 'this should pass';
434             log_like 'xact messag', 'but this would fail';
435             log_like qr{xact messg}, 'but this regex would pass';
436            
437             log_like { message => 'Exact message', log4p_level => 'INFO' }, 'also passes';
438             log_like { message => 'Exact message', log4p_level => 'ERROR' }, 'Fails, level does not match';
439              
440             =head2 log_unlike
441              
442             log_unlike \%pattern, $message;
443             log_unlike $pattern, $message;
444              
445             C<log_unlike> works like C<log_like>, except NONE of the events in the
446             current log context must match in order for the test to pass.
447              
448             =head1 AUTHOR
449              
450             Original author: Brian Duggan
451              
452             Current maintainer: Graham Ollis E<lt>plicease@cpan.orgE<gt>
453              
454             Contributors:
455              
456             Curt Tilmes
457              
458             Yanick Champoux
459              
460             =head1 COPYRIGHT AND LICENSE
461              
462             This software is copyright (c) 2013 by NASA GSFC.
463              
464             This is free software; you can redistribute it and/or modify it under
465             the same terms as the Perl 5 programming language system itself.
466              
467             =cut