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