File Coverage

blib/lib/CPAN/Testers/TailLog.pm
Criterion Covered Total %
statement 56 67 83.5
branch 15 30 50.0
condition 4 18 22.2
subroutine 14 16 87.5
pod 5 5 100.0
total 94 136 69.1


line stmt bran cond sub pod time code
1 4     4   191243 use 5.006; # our
  4         10  
2 4     4   12 use strict;
  4         5  
  4         60  
3 4     4   9 use warnings;
  4         8  
  4         3346  
4              
5             package CPAN::Testers::TailLog;
6              
7             our $VERSION = '0.001001';
8             our $DISTNAME = 'CPAN-Testers-TailLog';
9              
10             # ABSTRACT: Extract recent test statuses from metabase log
11              
12             # AUTHORITY
13              
14             sub new {
15 4 50   4 1 328 my $buildargs = { ref $_[1] ? %{ $_[1] } : @_[ 1 .. $#_ ] };
  0         0  
16 4 50       13 my $class = ref $_[0] ? ref $_[0] : $_[0];
17 4         8 my $self = bless $buildargs, $class;
18 4 50       19 $self->_check_cache_file if exists $self->{cache_file};
19 4 50       10 $self->_check_url if exists $self->{url};
20 4         10 $self;
21             }
22              
23             sub cache_file {
24             $_[0]->{cache_file} = $_[0]->_build_cache_file
25 10 100   10 1 44 unless exists $_[0]->{cache_file};
26 10         63 $_[0]->{cache_file};
27             }
28              
29             sub get_all {
30              
31             # If this fails, we just parse what we parsed last time
32             # Actually, not sure if mirror is atomic or not.
33             # Mirror is used here also to get automatic if-modified behaviour
34 4     4 1 140 $_[0]->_ua->mirror( $_[0]->url, $_[0]->cache_file );
35              
36             # So if the connection goes away and HTTP::Tiny fubars,
37             # we just pretend things are fine for now.
38             # mostly, because deciding how to handle error cases hurt
39             # my tiny brain
40              
41 4         708691 require Path::Tiny;
42 4         21 my (@lines) =
43             Path::Tiny::path( $_[0]->cache_file )->lines_utf8( { chomp => 1 } );
44              
45             # Skip prelude
46 4   66     94611 shift @lines while @lines and $lines[0] !~ /\A\s*\[/;
47 4         25 [ map { $_[0]->_parse_line($_) } @lines ];
  4000         4056  
48             }
49              
50             sub get_iter {
51 1     1 1 3 my $self = $_[0];
52 1         2 my $fetched = 0;
53 1         1 my $handle;
54             my $done;
55             return sub {
56 1001 50   1001   270822 return undef if $done;
57 1001   66     1484 $fetched ||= do {
58 1         4 $self->_ua->mirror( $self->url, $self->cache_file );
59 1         2571 1;
60             };
61 1001 100       1316 defined $handle or $handle = do {
62 1         5 require Path::Tiny;
63 1         2 $handle = Path::Tiny::path( $self->cache_file )->openr_utf8;
64             };
65 1001         12839 while ( my $line = <$handle> ) {
66 1001 100       7783 next if $line !~ /\A\s*\[/;
67 1000         1574 chomp $line;
68 1000         1657 return $self->_parse_line($line);
69             }
70 1         2 $done = 1;
71 1         4 return undef;
72 1         5 };
73             }
74              
75             sub url {
76 5 100   5 1 27 $_[0]->{url} = $_[0]->_build_url unless exists $_[0]->{url};
77 5         18 $_[0]->{url};
78             }
79              
80             # -- private ] --
81              
82             sub _parse_line {
83 5000     5000   3076 my %record;
84             @record{
85 5000         53257 qw( submitted reporter grade filename platform perl_version uuid accepted )
86             } = (
87             $_[1] =~ qr{
88             \A
89             \s*
90             \[ (.*? ) \] # submitted
91             \s*
92             \[ (.*? ) \] # reported
93             \s*
94             \[ (.*? ) \] # grade
95             \s*
96             \[ (.*?) \] # filename
97             \s*
98             \[ (.*?) \] # platform
99             \s*
100             \[ (.*?) \] # perl_version
101             \s*
102             \[ (.*?) \] # uuid
103             \s*
104             \[ (.*?) \] # accepted
105             }x
106             );
107 5000         17783 require CPAN::Testers::TailLog::Result;
108 5000         8547 CPAN::Testers::TailLog::Result->new( \%record );
109             }
110              
111             sub _ua {
112 5 100   5   33 $_[0]->{_ua} = $_[0]->_build_ua unless exists $_[0]->{_ua};
113 5         80 $_[0]->{_ua};
114             }
115              
116             # -- builders ] --
117             sub _build_cache_file {
118 4     4   2622 require File::Temp;
119 4         29508 my $temp = File::Temp->new(
120             TEMPLATE => $DISTNAME . '-XXXXX',
121             TMPDIR => 1,
122             SUFFIX => '.txt',
123             EXLOCK => 0,
124             );
125 4         1969 $_[0]->{_tempfile} = $temp;
126 4         675 require Path::Tiny;
127              
128             # Touching tempfiles required to get useful if-modified behaviour
129 4         7943 Path::Tiny::path( $temp->filename )->touch( time - ( 7 * 24 * 60 * 60 ) );
130 4         435 $temp->filename;
131             }
132              
133             sub _build_ua {
134 1     1   791 require HTTP::Tiny;
135 1         33455 HTTP::Tiny->new( agent => ( $DISTNAME . '/' . $VERSION ), );
136             }
137              
138             sub _build_url {
139 4     4   17 'http://metabase.cpantesters.org/tail/log.txt';
140             }
141              
142             # -- checkers ] --
143             sub _check_cache_file {
144 0     0     require Path::Tiny;
145 0           my $path = Path::Tiny::path( $_[0]->{cache_file} );
146 0           my $dir = $path->parent;
147 0 0 0       die "cache_file: Directory for $path not accessible: $?"
      0        
148             unless -e $dir
149             and -d $dir
150             and -r $dir;
151 0 0         if ( not -e $path ) {
152              
153             # Path doesn't exist, test creating it
154             # Hope touch dies if it can't be written
155 0           $path->touch( time - ( 7 * 24 * 60 * 60 ) );
156             }
157 0 0 0       return if -e $path and not -d $path and -w $path;
      0        
158 0           die "cache_file: $path exists but is unwriteable";
159             }
160              
161             sub _check_url {
162 0 0   0     die "url: Missing protocol in $_[0]->{url}" if $_[0]->{url} !~ qr{://};
163             die "url: Unknown protocol in $_[0]->{url}"
164 0 0         if $_[0]->{url} !~ qr{\Ahttps?://};
165             }
166              
167             1;
168              
169             =head1 NAME
170              
171             CPAN-Testers-TailLog - Extract recent test statuses from metabase log
172              
173             =head1 SYNOPSIS
174              
175             use CPAN::Testers::TailLog;
176              
177             my $tailer = CPAN::Testers::TailLog->new();
178             my $results = $tailer->get_all();
179             for my $item ( @{ $results } ) {
180             printf "%s: %s\n", $item->grade, $item->filename;
181             }
182              
183             =head1 DESCRIPTION
184              
185             B is a simple interface to the C C
186             located at C
187              
188             This module simply wraps the required HTTP Request mechanics, some persistent
189             caching glue for performance, and a trivial parsing layer to provide an object
190             oriented view of the log.
191              
192             =head1 METHODS
193              
194             =head2 new
195              
196             Creates an object for fetching results.
197              
198             my $tailer = CPAN::Testers::TailLog->new(
199             %options
200             );
201              
202             =head3 new:cache_file
203              
204             ->new( cache_file => "/path/to/file" )
205              
206             If not specified, defaults to a C file.
207              
208             This is good enough for in-memory persistence, so for code that is long lived
209             setting this is not really necessary.
210              
211             However, if you want a regularly exiting process, like a cron job, you'll
212             probably want to set this to a writeable path.
213              
214             This will ensure you save redundant bandwidth if you sync too quickly, as the
215             C will be used for C.
216              
217             Your C calls will still look the same, but they'll be a little faster,
218             you'll eat a little less bandwidth, and stress the remote server a little less.
219              
220             =head3 new:url
221              
222             ->new( url => "http://path/to/tail.log" )
223              
224             If not specified, uses the default URL,
225              
226             http://metabase.cpantesters.org/tail/log.txt
227              
228             Its not likely you'll have a use for this, but it may turn out useful for
229             debugging, or maybe somebody out there as an equivalent private server with
230             this log.
231              
232             =head2 cache_file
233              
234             Accessor for configured cache file path
235              
236             my $path = $tailer->cache_file
237              
238             =head2 get_all
239              
240             Fetches the most recent data possible as an C of
241             L
242              
243             my $arrayref = $tailer->get_all();
244              
245             Note that an arrayref will be returned regardless of what happens. It helps to
246             assume the result is just a dumb transfer.
247              
248             Though keep in mind non-Cs may be returned in error conditions
249             (Undecided).
250              
251             Calling this multiple times will be efficient using C
252             headers where applicable.
253              
254             Though even if nothing has changed, you'll get a full copy of the last state.
255              
256             If you want an "only what's changed since last time we checked, see F
257              
258             =head2 get_iter
259              
260             Returns a lazy C that returns one L at
261             a time.
262              
263             my $iter = $tailer->get_iter();
264             while ( my $item = $iter->() ) {
265             printf "%s %s\n", $item->grade, $item->filename;
266             }
267              
268             As with C, present design is mostly "dumb state transfer", so all this
269             really serves is a possible programming convenience. However, optimisations may
270             be applied here in future so that C<< $iter->() >> pulls items off the wire as
271             they arrive, saving you some traffic if you terminate early.
272              
273             Presently, an early termination only saves you a little disk IO, extra regex
274             parses and shaves a few object creations.
275              
276             =head2 url
277              
278             Accessor for configured log URL.
279              
280             my $url = $tailer->url;
281              
282             =head1 SEE ALSO
283              
284             =over 4
285              
286             =item * L
287              
288             Some of the logic of this module shares similarity with the contents of that
289             module, however, that module is designed as a standalone application that
290             simply shows the current status with some filtration options.
291              
292             It is not however designed for re-use.
293              
294             My objective is different, and I want to write a daemon that periodically polls
295             for new records, and creates a local database ( Similar to what likely happens
296             inside C ) of reports for quick searching, and I
297             figure this sort of logic can also be useful for somebody who wants a
298             C monitor.
299              
300             Some of the logic was cribbed from this and reduced to be closer to verbatim.
301              
302             =item * L
303              
304             C contains similar logic to this script as well, again,
305             prioritizing for simplicity and re-use.
306              
307             Any specific mangling with C is left to the consumer.
308              
309             =back
310              
311             =head1 AUTHOR
312              
313             Kent Fredric
314              
315             =head1 LICENSE
316              
317             This software is copyright (c) 2016 by Kent Fredric.
318              
319             This is free software; you can redistribute it and/or modify it under the same
320             terms as the Perl 5 programming language system itself.
321