File Coverage

blib/lib/CPAN/Reporter/Smoker.pm
Criterion Covered Total %
statement 210 244 86.0
branch 76 114 66.6
condition 14 25 56.0
subroutine 24 25 96.0
pod 1 1 100.0
total 325 409 79.4


line stmt bran cond sub pod time code
1 8     8   304684 use 5.006;
  8         24  
2 8     8   30 use strict;
  8         12  
  8         190  
3 8     8   36 use warnings;
  8         10  
  8         477  
4             package CPAN::Reporter::Smoker;
5              
6             our $VERSION = '0.29';
7              
8 8     8   36 use Carp;
  8         7  
  8         559  
9 8     8   29 use Config;
  8         12  
  8         335  
10 8     8   6125 use CPAN 1.93;
  8         1675554  
  8         1890  
11 8     8   120 use CPAN::Tarzip;
  8         21  
  8         221  
12 8     8   40 use CPAN::HandleConfig;
  8         16  
  8         214  
13 8     8   4439 use CPAN::Reporter::History 1.1702;
  8         402762  
  8         474  
14 8     8   4979 use Compress::Zlib 1.2;
  8         325943  
  8         1588  
15 8     8   59 use Fcntl ':flock';
  8         14  
  8         731  
16 8     8   41 use File::Basename qw/basename dirname/;
  8         11  
  8         441  
17 8     8   38 use File::Spec 3.27;
  8         114  
  8         191  
18 8     8   1343 use File::Temp 0.20;
  8         13195  
  8         652  
19 8     8   41 use List::Util 1.03 qw/shuffle/;
  8         135  
  8         594  
20 8     8   3525 use Probe::Perl 0.01;
  8         6609  
  8         209  
21 8     8   3051 use Term::Title 0.01;
  8         4200  
  8         288  
22              
23 8     8   44 use Exporter;
  8         11  
  8         19712  
24             our @ISA = 'Exporter';
25             our @EXPORT = qw/ start /; ## no critic Export
26              
27             #--------------------------------------------------------------------------#
28             # globals
29             #--------------------------------------------------------------------------#
30              
31             my $perl = Probe::Perl->find_perl_interpreter;
32             my $tmp_dir = File::Temp::tempdir(
33             'C-R-Smoker-XXXXXXXX', DIR => File::Spec->tmpdir, CLEANUP => 1
34             );
35              
36             #--------------------------------------------------------------------------#
37             # start -- start automated smoking
38             #--------------------------------------------------------------------------#
39             my %spec = (
40             clean_cache_after => {
41             default => 100,
42             is_valid => sub { /^\d+$/ },
43             },
44             restart_delay => {
45             default => 12 * 3600, # 12 hours
46             is_valid => sub { /^\d+$/ },
47             },
48             set_term_title => {
49             default => 1,
50             is_valid => sub { /^[01]$/ },
51             },
52             status_file => {
53             default => File::Spec->catfile( File::Spec->tmpdir, "smoker-status-$$.txt" ),
54             is_valid => sub { -d dirname( $_ ) },
55             },
56             list => {
57             default => undef,
58             is_valid => sub { !defined $_ || ref $_ eq 'ARRAY' || -r $_ }
59             },
60             install => {
61             default => 0,
62             is_valid => sub { /^[01]$/ },
63             },
64             'reverse' => {
65             default => 0,
66             is_valid => sub { /^[01]$/ },
67             },
68             'random' => {
69             default => 0,
70             is_valid => sub { /^[01]$/ },
71             },
72             force_trust => {
73             default => 0,
74             is_valid => sub { /^[01]$/ },
75             },
76             'reload_history_period' => {
77             default => 30*60,
78             is_valid => sub { /^\d+$/ },
79             },
80             filter => {
81             default => undef,
82             is_valid => sub { !defined $_ || ref $_ eq 'CODE' }
83             },
84             skip_dev_versions => {
85             default => 0,
86             is_valid => sub { /^[01]$/ },
87             },
88             '_start_from_timestamp' => {
89             default => 0,
90             is_valid => sub { /^(?:[\d.]{8}|0)$/ },
91             },
92             _hook_after_test => {
93             default => undef,
94             is_valid => sub { !defined $_ || ref $_ eq 'CODE' }
95             },
96             );
97              
98             sub start {
99 29     29 1 44190 my %args = map { $_ => $spec{$_}{default} } keys %spec;
  406         477  
100 29 100       286 croak "Invalid arguments to start(): must be key/value pairs"
101             if @_ % 2;
102 28         55 while ( @_ ) {
103 27         63 my ($key, $value) = splice @_, 0, 2;
104 27         27 local $_ = $value; # alias for validator
105             croak "Invalid argument to start(): $key => $value"
106 27 100 66     112 unless $spec{$key} && $spec{$key}{is_valid}->($value);
107 16         43 $args{$key} = $value;
108             }
109              
110             # Stop here if we're just testing
111 17 100       80 return 1 if $ENV{PERL_CR_SMOKER_SHORTCUT};
112              
113             # Notify before CPAN messages start
114 5         51 $CPAN::Frontend->mywarn( "Starting CPAN::Reporter::Smoker\n" );
115              
116             # Let things know we're running automated
117 5         301 local $ENV{AUTOMATED_TESTING} = 1;
118              
119             # Always accept default prompts
120 5         22 local $ENV{PERL_MM_USE_DEFAULT} = 1;
121 5         20 local $ENV{PERL_EXTUTILS_AUTOINSTALL} = "--defaultdeps";
122              
123             # Load CPAN configuration
124 5         5 my $init_cpan = 0;
125 5 50       15 unless ( $init_cpan++ ) {
126 5         53 CPAN::HandleConfig->load();
127 5         341 CPAN::Shell::setup_output;
128 5         109 CPAN::Index->reload;
129 5         191713 $CPAN::META->checklock(); # needed for cache scanning
130             }
131              
132             # Win32 SIGINT propogates all the way to us, so trap it before we smoke
133             # Must come *after* checklock() to override CPAN's $SIG{INT}
134 5         2179 local $SIG{INT} = \&_prompt_quit;
135              
136             # Master loop
137             # loop counter will increment with each restart - useful for testing
138 5         10 my $loop_counter = 0;
139              
140             # global cache of distros smoked to speed skips on restart
141 5         28 my %seen = map { $_->{dist} => 1 } CPAN::Reporter::History::have_tested();
  0         0  
142 5         510 my $history_loaded_at = time;
143              
144             SCAN_LOOP:
145 5         9 while ( 1 ) {
146 10         19 $loop_counter++;
147 10         19 my $loop_start_time = time;
148 10         17 my $dists;
149              
150             # Get the list of distributions to process
151 10 100       44 if ( $args{list} ) {
152             # Given a list
153 3 100       10 if ( ref $args{list} eq 'ARRAY' ) {
154 2         4 $dists = $args{list};
155             }
156             # Given a file
157             else {
158 1 50       30 open( my $list_fh, "<", $args{list} ) or die $!;
159 1         10 my @list = map { chomp; $_ } grep { /\S/ } <$list_fh>;
  2         3  
  2         4  
  4         9  
160 1         7 $dists = \@list;
161             }
162             }
163             else {
164             # Or get list from CPAN
165 7         55 my $package = _get_module_index( 'modules/02packages.details.txt.gz' );
166 7         20 my $find_ls = _get_module_index( 'indices/find-ls.gz' );
167 7         78 CPAN::Index->reload;
168 7         217 $CPAN::Frontend->mywarn( "Smoker: scanning and sorting index\n");
169              
170 7         204 $dists = _parse_module_index( $package, $find_ls, $args{skip_dev_versions}, $args{_start_from_timestamp} );
171              
172 7         897 $CPAN::Frontend->mywarn( "Smoker: found " . scalar @$dists . " distributions on CPAN\n");
173             }
174              
175             # Maybe reverse the list
176 10 100       298 if ( $args{'reverse'} ) {
177 1         7 $dists = [ reverse @$dists ];
178             }
179              
180             # Maybe shuffle the list
181 10 50       32 if ( $args{'random'} ) {
182 0         0 $dists = [ shuffle @$dists ];
183             }
184              
185             # Check if we need to manually reset test history during each dist loop
186 10         18 my $reset_string = q{};
187 10 50 33     39 if ( $CPAN::Config->{build_dir_reuse}
188             && $CPAN::META->can('reset_tested') )
189             {
190 0         0 $reset_string = 'CPAN::Index->reload; $CPAN::META->reset_tested; '
191             }
192              
193             # Force trust_test_report_history if requested
194 10         16 my $trust_string = q{};
195 10 50       28 if ( $args{force_trust} ) {
196 0         0 $trust_string = '$CPAN::Config->{trust_test_report_history} = 1; '
197             }
198              
199             # Clean cache on start and count dists tested to trigger cache cleanup
200 10         40 _clean_cache();
201 10         50178 my $dists_tested = 0;
202              
203             # Start smoking
204             DIST:
205 10         32 for my $d ( 0 .. $#{$dists} ) {
  10         39  
206 34         366 my $dist = CPAN::Shell->expandany($dists->[$d]);
207 34         3677 my $base = $dist->base_id;
208 34         2290 my $count = sprintf('%d/%d', $d+1, scalar @$dists);
209 34 100 33     265 if ( $seen{$base}++ ) {
    50          
    50          
210 16         77 $CPAN::Frontend->mywarn(
211             "Smoker: already tested $base [$count]\n");
212 16         409 next DIST;
213             }
214             elsif ( $args{filter} and $args{filter}->($dist) ) {
215 0         0 $CPAN::Frontend->mywarn(
216             "Smoker: dist skipped $base [$count]\n");
217 0         0 next DIST;
218             }
219 18         241 elsif ( CPAN::Distribution->new(%{$dist})->prefs->{disabled} ) {
220 0         0 $CPAN::Frontend->mywarn(
221             "Smoker: dist disabled $base [$count]\n");
222 0         0 next DIST;
223             }
224             else {
225             # record distribution being smoked
226 18         1521 my $time = scalar localtime();
227 18         70 my $msg = "$base [$count] at $time";
228 18 50       99 if ( $args{set_term_title} ) {
229 18         163 Term::Title::set_titlebar( "Smoking $msg" );
230             }
231 18         1578 $CPAN::Frontend->mywarn( "\nSmoker: testing $msg\n\n" );
232 18         733 local $ENV{PERL_CR_SMOKER_CURRENT} = $base;
233 18         1649 open my $status_fh, ">", $args{status_file};
234 18 50       78 if ( $status_fh ) {
235 18         107 flock $status_fh, LOCK_EX;
236 18         33 print {$status_fh} $msg;
  18         134  
237 18         537 flock $status_fh, LOCK_UN;
238 18         158 close $status_fh;
239             }
240             # invoke CPAN.pm to test distribution
241             system($perl, "-MCPAN", "-e",
242             "\$CPAN::Config->{test_report} = 1; " . $trust_string
243 18 50       44903809 . $reset_string . ($args{'install'} ? 'install' : 'test')
244             . "( '$dists->[$d]' )"
245             );
246 18         195 my $interrupted = 0;
247 18 50       239 if ( $? & 127 ) {
248 0         0 $interrupted = 1;
249 0         0 _prompt_quit( $? & 127 ) ;
250             }
251              
252 18 50       151 if ($args{_hook_after_test}) {
253 0         0 $args{_hook_after_test}->($dist, $interrupted);
254             }
255            
256             # cleanup and record keeping
257 18 50       169191 unlink $args{status_file} if -f $args{status_file};
258 18         765 $dists_tested++;
259             }
260 18 50       570 if ( $dists_tested >= $args{clean_cache_after} ) {
261 0         0 _clean_cache();
262 0         0 $dists_tested = 0;
263             }
264 18 50       119 if (time - $history_loaded_at > $args{reload_history_period}) { #_reload_history
265 0         0 %seen = map { $_->{dist} => 1 } CPAN::Reporter::History::have_tested();
  0         0  
266 0         0 $history_loaded_at = time;
267 0         0 $CPAN::Frontend->mywarn( "List of distros smoked updated\n");
268             }
269              
270 18 100       235 next SCAN_LOOP if time - $loop_start_time > $args{restart_delay};
271             }
272 5 50       102 last SCAN_LOOP if $ENV{PERL_CR_SMOKER_RUNONCE};
273 0 0       0 last SCAN_LOOP if $args{list};
274             # if here, we are out of distributions to test, so sleep
275 0         0 my $delay = int( $args{restart_delay} - ( time - $loop_start_time ));
276 0 0       0 if ( $delay > 0 ) {
277 0         0 $CPAN::Frontend->mywarn(
278             "\nSmoker: Finished all available dists. Sleeping for $delay seconds.\n\n"
279             );
280 0         0 sleep $delay ;
281             }
282             }
283              
284 5         83 CPAN::cleanup();
285 5         954 return $loop_counter;
286             }
287              
288             #--------------------------------------------------------------------------#
289             # private variables and functions
290             #--------------------------------------------------------------------------#
291              
292             sub _clean_cache {
293 10     10   21 my $phase = $CPAN::Config->{scan_cache};
294             # Possibly clean up cache if it exceeds defined size
295 10 100       29 if ( $CPAN::META->{cachemgr} ) {
296 5         39 $CPAN::META->{cachemgr}->scan_cache($phase);
297             }
298             else {
299 5         48 $CPAN::META->{cachemgr} = CPAN::CacheMgr->new($phase); # also scans cache
300             }
301             }
302              
303             sub _prompt_quit {
304 0     0   0 my ($sig) = @_;
305             # convert numeric to name
306 0 0       0 if ( $sig =~ /\d+/ ) {
307 0         0 my @signals = split q{ }, $Config{sig_name};
308 0   0     0 $sig = $signals[$sig] || '???';
309             }
310             $CPAN::Frontend->myprint(
311             "\nStopped during $ENV{PERL_CR_SMOKER_CURRENT}.\n"
312 0 0       0 ) if defined $ENV{PERL_CR_SMOKER_CURRENT};
313 0         0 $CPAN::Frontend->myprint(
314             "\nCPAN testing halted on SIG$sig. Continue (y/n)? [n]\n"
315             );
316 0         0 my $answer = ;
317 0 0       0 CPAN::cleanup(), exit 0 unless substr( lc($answer), 0, 1) eq 'y';
318 0         0 return;
319             }
320              
321             #--------------------------------------------------------------------------#
322             # _get_module_index
323             #
324             # download the 01modules index and return the local file name
325             #--------------------------------------------------------------------------#
326              
327             sub _get_module_index {
328 14     14   29 my ($remote_file) = @_;
329              
330 14         193 $CPAN::Frontend->mywarn(
331             "Smoker: getting $remote_file from CPAN\n");
332             # CPAN.pm may not use aslocal if it's a file:// mirror
333 14         1336 my $aslocal_file = File::Spec->catfile( $tmp_dir, basename( $remote_file ));
334 14         146 my $actual_local = CPAN::FTP->localize( $remote_file, $aslocal_file, 1 );
335 14 50       17054 if ( ! -r $actual_local ) {
336 0         0 die "Couldn't get '$remote_file' from your CPAN mirror. Halting\n";
337             }
338 14         32 return $actual_local;
339             }
340              
341             my $module_index_re = qr{
342             ^\s href="\.\./authors/id/./../ # skip prelude
343             ([^"]+) # capture to next dquote mark
344             .+? # skip to end of hyperlink
345             \s+ # skip spaces
346             \S+ # skip size
347             \s+ # skip spaces
348             (\S+) # capture day
349             \s+ # skip spaces
350             (\S+) # capture month
351             \s+ # skip spaces
352             (\S+) # capture year
353             }xms;
354              
355             my %months = (
356             Jan => '01', Feb => '02', Mar => '03', Apr => '04', May => '05',
357             Jun => '06', Jul => '07', Aug => '08', Sep => '09', Oct => '10',
358             Nov => '11', Dec => '12'
359             );
360              
361             # standard regexes
362             # note on archive suffixes -- .pm.gz shows up in 02packagesf
363             my %re = (
364             bundle => qr{^Bundle::},
365             mod_perl => qr{/mod_perl},
366             perls => qr{(?:
367             /(?:emb|syb|bio)?perl-\d
368             | /(?:parrot|ponie|kurila|Perl6-Pugs)-\d
369             | /perl-?5\.004
370             | /perl_mlb\.zip
371             )}xi,
372             archive => qr{\.(?:tar\.(?:bz2|gz|Z)|t(?:gz|bz)|(?
373             target_dir => qr{
374             ^(?:
375             modules/by-module/[^/]+/./../ |
376             modules/by-module/[^/]+/ |
377             modules/by-category/[^/]+/[^/]+/./../ |
378             modules/by-category/[^/]+/[^/]+/ |
379             authors/id/./../
380             )
381             }x,
382             leading_initials => qr{(.)/\1./},
383             );
384              
385             # match version and suffix
386             $re{version_suffix} = qr{([-._]v?[0-9].*)($re{archive})};
387              
388             # split into "AUTHOR/Name" and "Version"
389             $re{split_them} = qr{^(.+?)$re{version_suffix}$};
390              
391             # matches "AUTHOR/tarball.suffix" or AUTHOR/modules/tarball.suffix
392             # and not other "AUTHOR/subdir/whatever"
393              
394             # Just get AUTHOR/tarball.suffix from whatever file name is passed in
395             sub _get_base_id {
396 181     181   160 my $file = shift;
397 181         121 my $base_id = $file;
398 181         692 $base_id =~ s{$re{target_dir}}{};
399 181         227 return $base_id;
400             }
401              
402             sub _base_name {
403 138     138   145 my ($base_id) = @_;
404 138         2539 my $base_file = basename $base_id;
405 138         805 my ($base_name, $base_version) = $base_file =~ $re{split_them};
406 138         194 return $base_name;
407             }
408              
409             #--------------------------------------------------------------------------#
410             # _parse_module_index
411             #
412             # parse index and return array_ref of distributions in reverse date order
413             #--------------------------------------------------------------------------#-
414              
415             sub _parse_module_index {
416 10     10   2912 my ( $packages, $file_ls, $skip_dev_versions, $start_from_timestamp ) = @_;
417              
418             # first walk the packages list
419             # and build an index
420              
421 10         13 my (%valid_bases, %valid_distros, %mirror);
422 0         0 my (%latest, %latest_dev);
423              
424 10 50       64 my $gz = Compress::Zlib::gzopen($packages, "rb")
425             or die "Cannot open package list: $Compress::Zlib::gzerrno";
426              
427 10         14321 my $inheader = 1;
428 10         41 while ($gz->gzreadline($_) > 0) {
429 181 100       9650 if ($inheader) {
430 90 100       222 $inheader = 0 unless /\S/;
431 90         131 next;
432             }
433              
434 91         202 my ($module, $version, $path) = split;
435              
436 91         202 my $base_id = _get_base_id("authors/id/$path");
437              
438             # skip all perl-like distros
439 91 100       607 next if $base_id =~ $re{perls};
440              
441             # skip mod_perl environment
442 82 100       239 next if $base_id =~ $re{mod_perl};
443              
444             # skip all bundles
445 64 100       162 next if $module =~ $re{bundle};
446              
447 55         91 $valid_distros{$base_id}++;
448 55         73 my $base_name = _base_name( $base_id );
449 55 50       91 if ($base_name) {
450 55         253 $latest{$base_name} = {
451             datetime => 0,
452             base_id => $base_id
453             };
454             }
455             }
456              
457             # next walk the find-ls file
458 10         919 local *FH;
459 10         133 tie *FH, 'CPAN::Tarzip', $file_ls;
460              
461 10         22302 while ( defined ( my $line = ) ) {
462 329         13604 my %stat;
463 329         1302 @stat{qw/inode blocks perms links owner group size datetime name linkname/}
464             = split q{ }, $line;
465              
466 329 50 66     1048 unless ($stat{name} && $stat{perms} && $stat{datetime}) {
      33        
467 7         26 next;
468             }
469             # skip directories, symlinks and things that aren't a tarball
470 322 100 66     1393 next if $stat{perms} eq "l" || substr($stat{perms},0,1) eq "d";
471 157 100       747 next unless $stat{name} =~ $re{target_dir};
472 112 100       476 next unless $stat{name} =~ $re{archive};
473              
474 96 100 100     204 next if $start_from_timestamp && $stat{datetime} < $start_from_timestamp;
475              
476             # skip if not AUTHOR/tarball
477             # skip perls
478 90         119 my $base_id = _get_base_id($stat{name});
479 90 50       126 next unless $base_id;
480              
481 90 50       568 next if $base_id =~ $re{perls};
482              
483             # skip Perl6 distros: AUTHOR/Perl6/...
484 90 100       178 next if $base_id =~ m{\A\w+/Perl6/};
485              
486 83         90 my $base_name = _base_name( $base_id );
487              
488             # if $base_id matches 02packages, then it is the latest version
489             # and we definitely want it; also update datetime from the initial
490             # assumption of 0
491 83 100       133 if ( $valid_distros{$base_id} ) {
492 50         71 $mirror{$base_id} = $stat{datetime};
493 50 50       85 next unless $base_name;
494 50 50       158 if ( $stat{datetime} > $latest{$base_name}{datetime} ) {
495             $latest{$base_name} = {
496             datetime => $stat{datetime},
497 50         288 base_id => $base_id
498             };
499             }
500             }
501             # if not in the packages file, we only want it if it resembles
502             # something in the package file and we only the most recent one
503             else {
504             # skip if couldn't parse out the name without version number
505 33 50       56 next unless defined $base_name;
506              
507             # skip unless there's a matching base from the packages file
508 33 100       93 next unless $latest{$base_name};
509              
510 24 100       48 next if $skip_dev_versions;
511              
512             # keep only the latest
513 18   100     82 $latest_dev{$base_name} ||= { datetime => 0 };
514 18 50       56 if ( $stat{datetime} > $latest_dev{$base_name}{datetime} ) {
515             $latest_dev{$base_name} = {
516             datetime => $stat{datetime},
517 18         91 base_id => $base_id
518             };
519             }
520             }
521             }
522              
523 10 100       579 if ( !$start_from_timestamp ) {
524             # pick up anything from packages that wasn't found in find-ls
525             # usually because find-ls is updated more rarely than packages
526             # as it is missing from find-ls, timestamp would be set to 0
527 9         30 for my $name ( keys %latest ) {
528 49         41 my $base_id = $latest{$name}{base_id};
529 49 50       80 $mirror{$base_id} = $latest{$name}{datetime} unless $mirror{$base_id};
530             }
531             }
532              
533             # for dev versions, it must be newer than the latest version of
534             # the same base name from the packages file
535              
536 10         28 for my $name ( keys %latest_dev ) {
537 14 50       32 if ( ! $latest{$name} ) {
538 0         0 next;
539             }
540 14 100       34 next if $latest{$name}{datetime} > $latest_dev{$name}{datetime};
541             $mirror{ $latest_dev{$name}{base_id} } = $latest_dev{$name}{datetime}
542 13         31 }
543              
544 10         72 return [ sort { $mirror{$b} <=> $mirror{$a} } keys %mirror ];
  113         180  
545             }
546              
547             1;
548              
549             # ABSTRACT: Turnkey CPAN Testers smoking
550              
551             __END__