File Coverage

blib/lib/CPAN/Reporter/Smoker.pm
Criterion Covered Total %
statement 206 237 86.9
branch 67 106 63.2
condition 11 22 50.0
subroutine 24 25 96.0
pod 1 1 100.0
total 309 391 79.0


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