File Coverage

blib/lib/CPAN/YACSmoke.pm
Criterion Covered Total %
statement 77 415 18.5
branch 3 160 1.8
condition 0 98 0.0
subroutine 26 47 55.3
pod 9 9 100.0
total 115 729 15.7


line stmt bran cond sub pod time code
1             =head1 NAME
2            
3             CPAN::YACSmoke - Yet Another CPAN Smoke Tester
4            
5             =begin readme
6            
7             =head1 REQUIREMENTS
8            
9             This package requires the following modules (most of which are not
10             included with Perl):
11            
12             CPANPLUS
13             Config::IniFiles
14             File::Basename
15             File::HomeDir
16             File::Path
17             File::Spec
18             File::Temp
19             IO::File
20             LWP::Simple
21             Module::Pluggable
22             Path::Class
23             Regexp::Assemble
24             SDBM_File
25             Sort::Versions
26             Test::Reporter
27             URI
28             if
29            
30             These dependencies (such as L and L) may require
31             additional modules.
32            
33             Windows users should also have L installed.
34            
35             =head1 INSTALLATION
36            
37             Installation can be done using the traditional Makefile.PL or the newer
38             Build.PL methods.
39            
40             Using Makefile.PL:
41            
42             perl Makefile.PL
43             make test
44             make install
45            
46             (On Windows platforms you should use C instead.)
47            
48             Using Build.PL (if you have Module::Build installed):
49            
50             perl Build.PL
51             perl Build test
52             perl Build install
53            
54             =end readme
55            
56             =head1 SYNOPSIS
57            
58             perl -MCPAN::YACSmoke -e test
59            
60             =head1 DESCRIPTION
61            
62             This module uses the backend of L to run tests on modules
63             recently uploaded to CPAN and post results to the CPAN Testers list.
64            
65             =begin readme
66            
67             See the module documentation for more information.
68            
69             =head1 REVISION HISTORY
70            
71             =for readme include file=Changes type=text start=0.03 stop=0.03_05
72            
73             =end readme
74            
75             =for readme stop
76            
77             It will create a database file in the F<.cpanplus> directory, which it
78             uses to track tested distributions. This information will be used to
79             keep from posting multiple reports for the same module, and to keep
80             from testing modules that use non-passing modules as prerequisites.
81            
82             If it is given multiple versions of the same distribution to test, it
83             will test the most recent version only. If that version fails, then
84             it will test a previous version.
85            
86             By default it uses CPANPLUS configuration settings.
87            
88             =cut
89            
90             package CPAN::YACSmoke;
91            
92 8     8   169841 use 5.006001;
  8         35  
  8         304  
93 8     8   42 use strict;
  8         19  
  8         318  
94 8     8   44 use warnings;
  8         22  
  8         238  
95            
96 8     8   8409 use CPANPLUS::Backend 0.051;
  8         4001968  
  8         376  
97 8     8   127 use CPANPLUS::Configure;
  8         19  
  8         170  
98 8     8   42 use CPANPLUS::Error;
  8         14  
  8         584  
99            
100 8     8   49 use File::Path;
  8         17  
  8         409  
101 8     8   146 use File::Basename;
  8         16  
  8         626  
102 8     8   46 use File::HomeDir qw( home );
  8         16  
  8         598  
103 8     8   8468 use File::Spec::Functions qw( splitpath );
  8         7485  
  8         651  
104 8     8   6867 use LWP::Simple;
  8         644477  
  8         90  
105 8     8   10088 use Path::Class;
  8         182608  
  8         615  
106 8     8   102 use POSIX qw( O_CREAT O_RDWR ); # for SDBM_File
  8         20  
  8         91  
107 8     8   17158 use Regexp::Assemble;
  8         138375  
  8         308  
108 8     8   7841 use SDBM_File;
  8         6088  
  8         392  
109 8     8   6595 use Sort::Versions;
  8         5586  
  8         1036  
110 8     8   57 use URI;
  8         18  
  8         221  
111 8     8   12907 use Module::Pluggable search_path => ["CPAN::YACSmoke::Plugin"];
  8         65576  
  8         81  
112 8     8   798 use Carp;
  8         16  
  8         581  
113 8     8   10603 use Config::IniFiles;
  8         121389  
  8         383  
114            
115 8     8   104 use if ($^O eq "MSWin32"), "File::HomeDir::Win32";
  8         16  
  8         96  
116            
117             # use YAML 'Dump';
118            
119             require Test::Reporter;
120             require YAML;
121            
122             our $VERSION = '0.03_07';
123             $VERSION = eval $VERSION;
124            
125             require Exporter;
126            
127             our @ISA = qw( Exporter );
128             our %EXPORT_TAGS = (
129             'all' => [ qw( mark test excluded purge flush ) ],
130             'default' => [ qw( mark test excluded ) ],
131             );
132            
133             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
134             our @EXPORT = ( @{ $EXPORT_TAGS{'default'} } );
135            
136 8     8   1294 use constant DATABASE_FILE => 'cpansmoke.dat';
  8         19  
  8         555  
137 8     8   45 use constant CONFIG_FILE => 'cpansmoke.ini';
  8         16  
  8         43780  
138            
139             my $extn = qr/(?:\.(?:tar\.gz|tgz|zip))/; # supported archive extensions
140            
141             =head1 OBJECT INTERFACE
142            
143             =over 4
144            
145             =cut
146            
147             {
148             my %Checked;
149             my $TiedObj;
150            
151             # We use the TiedObj flag instead of tied(%Checked) because the
152             # function creates an additional reference in the scope of an
153             # if (tied %Checked) { ... } which causes a warning etc.
154            
155             sub _connect_db {
156 0     0   0 my $self = shift;
157 0         0 my $filename = $self->{database_file};
158 0 0       0 if ($TiedObj) {
159             # error("Already connected to the database!");
160             } else {
161 0         0 $TiedObj = tie %Checked, 'SDBM_File', $filename, O_CREAT|O_RDWR, 0644;
162 0         0 $self->{checked} = \%Checked;
163 0         0 $self->_debug("Connected to database ($filename).");
164             }
165             }
166            
167             sub _disconnect_db {
168 1     1   2 my $self = shift;
169            
170 1 50       14 if ($TiedObj) {
171 0         0 $TiedObj = undef;
172 0         0 $self->{checked} = undef;
173 0         0 untie %Checked;
174 0         0 $self->_debug("Disconnected from database.");
175             # } else {
176             # error("Not connected to the database!");
177             }
178             }
179            
180             my $CONF = CPANPLUS::Configure->new();
181             sub _connect_configure {
182 0     0   0 return $CONF;
183             }
184            
185             my $CpanPlus;
186            
187             sub _connect_cpanplus {
188 0     0   0 my $self = shift;
189 0 0       0 return $self->{cpan} = $CpanPlus if ($CpanPlus);
190            
191 0         0 my $conf = shift;
192            
193 0         0 $CpanPlus = CPANPLUS::Backend->new($conf);
194            
195 0 0       0 if ($CPANPLUS::Backend::VERSION >= 0.052) {
196            
197             # TODO: if PASS included skipped tests, add a comment
198            
199             $CpanPlus->_register_callback(
200             name => 'munge_test_report',
201             code => sub {
202 0     0   0 my $mod = shift;
203 0   0     0 my $report = shift || "";
204 0 0       0 $report =~ s/\[MSG\] \[[\w: ]+\] Extracted .*?\n//sg if($self->{suppress_extracted});
205 0         0 $report .=
206             "\nThis report was machine-generated by CPAN::YACSmoke $VERSION.\n";
207 0         0 return $report;
208             },
209 0         0 );
210             }
211            
212             # BUG: this callback does not seem to get called consistently, if at all.
213            
214             $CpanPlus->_register_callback(
215             name => 'install_prerequisite',
216             code => sub {
217 0     0   0 my $mod = shift;
218 0         0 my $root;
219 0 0       0 if ($mod->package =~ /^(.+)$extn$/) {
220 0         0 $root = $1;
221             }
222             else {
223 0         0 error("Cannot handle ".$mod->package);
224 0         0 return;
225             }
226            
227 0 0       0 unless ($TiedObj) {
228 0         0 croak "Not connected to database!";
229             }
230 0         0 while (my $arg = shift) {
231 0         0 $arg->package =~ m/^(.+)$extn$/;
232 0         0 my $package = $1;
233            
234             # BUG: Exclusion does not seem to work for prereqs.
235             # Sometimes it seems that the install_prerequisite
236             # callback is not even called! Need to investigate.
237            
238 0 0       0 if ($self->_is_excluded_dist($package)) { # prereq on excluded list
239 0         0 msg("Prereq $package is excluded");
240 0         0 return;
241             }
242            
243 0         0 my $checked = $Checked{$package};
244 0 0 0     0 if (defined $checked &&
245             $checked =~ /aborted|fail|na/ ) {
246            
247 0 0       0 if ($self->{ignore_bad_prereqs}) {
248 0         0 msg("Known uninstallable prereqs $package - may have problems\n");
249             } else {
250 0         0 msg("Known uninstallable prereqs $package - aborting install\n");
251 0         0 $Checked{$root} = "aborted";
252 0         0 return;
253             }
254             }
255             }
256 0         0 return 1;
257             },
258 0         0 );
259            
260             $CpanPlus->_register_callback(
261             name => 'send_test_report',
262             code => sub {
263            
264 0 0   0   0 unless ($TiedObj) {
265 0         0 exit error("Not connected to database!");
266             }
267 0         0 my $mod = shift;
268 0         0 my $grade = lc shift;
269 0 0       0 if ($mod->{package} =~ /^(.+)$extn$/) {
270 0         0 my $package = $1;
271 0         0 my $checked = $Checked{$package};
272            
273             # TODO: option to report only passing tests
274            
275 0 0       0 return unless ($self->{cpantest});
276            
277             # Simplified algorithm for reporting:
278             # * don't send a report if
279             # - we get the same results as the last report sent
280             # - it passed the last test but not now
281             # - it didn't pass the last test or now
282            
283 0 0 0     0 return if (defined $checked && (
      0        
284             ($checked eq $grade) ||
285             ($checked ne 'pass' && $grade ne 'pass')));
286            
287 0         0 $Checked{$package} = $grade;
288            
289 0   0     0 return ((!$self->{report_pass_only}) || ($grade eq 'pass'));
290            
291             } else {
292 0         0 error("Unable to parse package information\n");
293 0         0 return;
294             }
295             },
296 0         0 );
297            
298             $CpanPlus->_register_callback(
299             name => 'edit_test_report',
300 0     0   0 code => sub { return; },
301 0         0 );
302            
303 0         0 return $self->{cpan} = $CpanPlus;
304             }
305             }
306            
307             my @CPANPLUS_FIELDS = qw(
308             verbose debug force cpantest
309             prereqs skiptest
310             prefer_bin prefer_makefile
311             makeflags makemakerflags
312             md5 signature
313             extractdir fetchdir
314             );
315            
316             my @CONFIG_FIELDS = (@CPANPLUS_FIELDS, qw(
317             recent_list_age ignore_cpanplus_bugs fail_max
318             exclude_dists test_max audit_log
319             ignore_bad_prereqs report_pass_only
320             allow_retries flush_flag suppress_extracted
321             ));
322            
323            
324             =item new( [ %config ] )
325            
326             The object interface is created normally through the test() or mark()
327             functions of the procedural interface. However, it can be accessed
328             with a set of configuration settings to extend the capabilities of
329             the package.
330            
331             CPANPLUS configuration settings (inherited from CPANPLUS unless
332             otherwise noted) are:
333            
334             verbose
335             debug
336             force
337             cpantest
338             report_pass_only
339             prereqs
340             prefer_bin
341             prefer_makefile - enabled by default
342             makeflags
343             makemakerflags
344             md5
345             signature
346             extractdir
347             fetchdir
348            
349             CPAN::YACSmoke configuration settings are:
350            
351             ignore_cpanplus_bugs
352             ignore_bad_prereqs
353             fail_max
354             exclude_dists
355             test_max
356             allow_retries
357             suppress_extracted
358             flush_flag - used by purge()
359            
360             list_from - List plugin required, default Recent
361            
362             recent_list_age - used with the Recent plugin
363             recent_list_path - used with the Recent plugin
364             mailbox - used with the Outlook plugin
365             nntp_id - used with the NNTP plugins
366             webpath - used with the WebList plugin
367            
368             audit_log - log file to write progress to
369            
370             config_file - an INI file with the above settings
371             database_file - the local cpansmoke database
372            
373             All settings can use defaults. With regards to the last setting,
374             the INI file should contain one setting per line, except the values
375             for the exclude_dists setting, which are laid out as:
376            
377             [CONFIG]
378             exclude_dists=<
379             mod_perl
380             HERE
381            
382             The above would then ignore any distribution that include the string
383             'mod_perl' in its name. This is useful for distributions which use
384             external C libraries, which are not installed, or for which testing
385             is problematic.
386            
387             The setting 'test_max' is used to restrict the number of distributions
388             tested in a single run. As some distributions can take some time to be
389             tested, it may be more suitable to run in small batches at a time. The
390             default setting is 100 distributions.
391            
392             The setting 'allow_retries' defaults to include grades of UNGRADED, IGNORED
393             and ABORTED. If you wish to change this, for example to only allow grades
394             of UNGRADED to be retried, you can specify as:
395            
396             [CONFIG]
397             allow_retries=ungraded
398            
399             Often module authors prefer to see the details of failed tests. You can
400             make this the default setting using:
401            
402             [CONFIG]
403             makeflags=TEST_VERBOSE=1
404            
405             Note that sending verbose failure reports for packages with thousands
406             of tests will be quite large (!), and may be blocked by mail and news
407             servers.
408            
409             See L for more information on the INI file format.
410            
411             =back
412            
413             =cut
414            
415             sub new {
416 0   0 0 1 0 my $class = shift || __PACKAGE__;
417            
418             ## Ensure CPANPLUS knows we automated.
419             ## (Q: Should we use Env::C to set this instead?)
420            
421 0         0 $ENV{AUTOMATED_TESTING} = 1;
422 0         0 $ENV{PERL_MM_USE_DEFAULT} = 1; # despite verbose setting
423            
424 0         0 my $conf = _connect_configure();
425            
426             ## set internal defaults
427 0         0 my $self = {
428             conf => $conf,
429             checked => undef,
430             ignore_cpanplus_bugs => ($CPANPLUS::Backend::VERSION >= 0.052),
431             fail_max => 3, # max failed versions to try
432             exclude_dists => [ ], # Regexps to exclude
433             test_max => 100, # max distributions per run
434             allow_retries => 'aborted|ungraded',
435             };
436            
437 0         0 bless $self, $class;
438            
439             ## set from CPANPLUS defaults
440 0         0 foreach my $field (@CPANPLUS_FIELDS) {
441 0         0 $self->{$field} = $conf->get_conf($field);
442             }
443            
444            
445             ## force overide of default settings
446            
447 0         0 $self->{skiptest} = 0;
448 0         0 $self->{prereqs} = 2; # force to ask callback
449            
450             # Makefile.PL shows which tests failed, whereas Build.PL does
451             # not when reports are sent through CPANPLUS 0.053, hence the
452             # prefer_makefile=1 default.
453            
454 0         0 $self->{prefer_makefile} = 1;
455            
456             # If we have TEST_VERBOSE=1 by default, then many FAIL reports
457             # will be huge. A lot of module authors will want that, but
458             # it's not the best idea to send those out immediately.
459            
460             ## $self->{makeflags} = 'TEST_VERBOSE=1';
461            
462 0         0 my %config = @_;
463            
464             ## config_file is an .ini file
465            
466 0   0     0 $config{config_file} ||=
467             file($self->basedir(), CONFIG_FILE)->stringify;
468            
469 0 0 0     0 if($config{config_file} && -r $config{config_file}) {
470 0         0 my $cfg = Config::IniFiles->new(-file => $config{config_file});
471 0         0 foreach my $field (@CONFIG_FIELDS) {
472 0         0 my $val = $cfg->val( 'CONFIG', $field );
473 0 0       0 $self->{$field} = $val if(defined $val);
474             # msg("Setting $field = $val") if (defined $val);
475            
476             }
477 0         0 my @list = $cfg->val( 'CONFIG', 'exclude_dists' );
478 0 0       0 $self->{exclude_dists} = [ @list ] if(@list);
479             }
480            
481 0 0       0 if ($self->{audit_log}) {
482 0         0 my ($vol, $path, $file) = splitpath($self->{audit_log});
483 0 0 0     0 unless ($vol || $path) {
484 0         0 $self->{audit_log} = file($self->basedir(), $file)->stringify;
485             }
486             }
487            
488            
489             ## command line switches override
490 0         0 foreach my $field (@CONFIG_FIELDS, 'audit_cb') {
491 0 0       0 if (exists $config{$field}) {
492 0         0 $self->{$field} = $config{$field};
493             }
494             }
495            
496             ## reset CPANPLUS defaults
497 0         0 foreach my $field (@CPANPLUS_FIELDS) {
498 0         0 $conf->set_conf($field => $self->{$field});
499             }
500            
501 0 0       0 $self->{test_max} = 0 if($self->{test_max} < 0); # sanity check
502            
503            
504             ## determine the data source plugin
505            
506 0   0     0 $config{list_from} ||= 'Recent';
507 0         0 my $plugin;
508 0         0 my @plugins = $self->plugins();
509 0         0 for(@plugins) {
510 0 0       0 $plugin = $_ if($_ =~ /$config{list_from}/);
511             }
512            
513 0 0       0 croak("no plugin available of that name\n") unless($plugin);
514 0         0 eval "CORE::require $plugin";
515 0 0       0 croak "Couldn't require $plugin : $@" if $@;
516 0         0 $config{smoke} = $self;
517 0         0 $self->{plugin} = $plugin->new(\%config);
518            
519            
520             ## determine the database file
521            
522 0   0     0 $self->{database_file} ||=
523             file($self->basedir(), DATABASE_FILE)->stringify;
524            
525 0         0 $self->_connect_db();
526 0         0 $self->_connect_cpanplus($conf);
527            
528 0         0 return $self;
529             }
530            
531            
532             sub DESTROY {
533 1     1   6091 my $self = shift;
534 1         6 $self->_audit("Disconnecting from database");
535 1         5 $self->_disconnect_db();
536             }
537            
538             =head2 METHODS
539            
540             =over 4
541            
542             =item homedir
543            
544             Obtains the users home directory
545            
546             =cut
547            
548             # TODO: use CPANPLUS function
549            
550             sub homedir {
551 0     0 1 0 my $self = shift;
552 0 0       0 return $self->{homedir} = dir(shift) if (@_);
553            
554 0         0 my $home = dir(home());
555            
556 0         0 $self->{homedir} = $home;
557            
558 0         0 $self->_audit("homedir = " . $self->{homedir});
559 0         0 return $self->{homedir}->stringify;
560             }
561            
562             =item basedir
563            
564             Obtains the base directory for downloading and testing distributions.
565            
566             =cut
567            
568             sub basedir {
569 0     0 1 0 my $self = shift;
570 0 0       0 return $self->{basedir} = shift if (@_);
571            
572 0 0       0 unless (defined $self->{basedir}) {
573 0   0     0 $self->{basedir} = $self->{conf}->get_conf("base") || $self->homedir();
574             }
575 0         0 return $self->{basedir};
576             }
577            
578             =item builddir
579            
580             Obtains the build directory for unpacking and testing distributions.
581            
582             =back
583            
584             =cut
585            
586             sub builddir {
587 0     0 1 0 my $self = shift;
588            
589 0         0 require Config;
590            
591 0         0 return dir(
592             $self->{conf}->get_conf('base'),
593             $Config::Config{version},
594             $self->{conf}->_get_build('moddir'),
595             )->stringify;
596             }
597            
598            
599             sub _is_excluded_dist {
600 0     0   0 my $self = shift;
601 0         0 my $dist = shift;
602 0 0       0 unless($self->{re}) {
603 0         0 $self->{re} = new Regexp::Assemble;
604 0         0 $self->{re}->add( @{ $self->{exclude_dists} } );
  0         0  
605             }
606            
607 0 0       0 return 1 if($dist =~ $self->{re}->re);
608 0         0 return 0;
609             }
610            
611             sub _remove_excluded_dists {
612 0     0   0 my $self = shift;
613 0         0 my @dists = ( );
614 0         0 my $removed = 0;
615            
616 0         0 while (my $dist = shift) {
617 0         0 my $file = basename($dist);
618 0 0       0 if ($self->_is_excluded_dist($file)) {
619 0         0 chomp($file);
620 0         0 $self->_track("Excluding $dist");
621 0         0 $removed = 1;
622             } else {
623 0         0 push @dists, $dist;
624             }
625             }
626 0 0       0 $self->_audit('') if($removed);
627 0         0 return @dists;
628             }
629            
630             sub _build_path_list {
631 0     0   0 my $self = shift;
632 0         0 my $ignored = 0;
633            
634 0         0 my %paths = ( );
635 0         0 while (my $line = shift) {
636 0 0       0 if ($line =~ /^(.*)\-(.+)$extn$/) {
    0          
637 0         0 my $dist = $1;
638 0         0 my @dirs = split /\/+/, $dist;
639 0         0 my $ver = $2;
640            
641             # due to rt.cpan.org bugs #11093, #11125 in CPANPLUS
642            
643 0 0 0     0 if ($self->{ignore_cpanplus_bugs} || (
      0        
644             (@dirs == 4) && ($ver =~ /^[\d\.\_]+$/)) ) {
645            
646 0 0       0 if (exists $paths{$dist}) {
647 0         0 unshift @{ $paths{$dist} }, $ver;
  0         0  
648             } else {
649 0         0 $paths{$dist} = [ $ver ];
650             }
651            
652             } else {
653 0         0 $self->_track("Ignoring $dist-$ver (due to CPAN+ bugs)");
654 0         0 $ignored = 1;
655             }
656            
657             # check for previously parsed package string
658             } elsif ($line =~ /^(.*)\-(.+)$/) {
659 0         0 my $dist = $1;
660 0         0 my @dirs = split /\/+/, $dist;
661 0         0 my $ver = $2;
662            
663 0 0       0 if (@dirs == 1) { # previously parsed
664 0 0       0 if (exists $paths{$dist}) {
665 0         0 unshift @{ $paths{$dist} }, $ver;
  0         0  
666             } else {
667 0         0 $paths{$dist} = [ $ver ];
668             }
669             }
670             }
671             }
672 0 0       0 $self->_audit('') if($ignored);
673 0         0 return %paths;
674             }
675            
676             =head1 PROCEDURAL INTERFACE
677            
678             =head2 EXPORTS
679            
680             The following routines are exported by default. They are intended to
681             be called from the command-line, though they could be used from a
682             script.
683            
684             =over
685            
686             =cut
687            
688             =item test( [ %config, ] [ $dist [, $dist .... ] ] )
689            
690             perl -MCPAN::YACSmoke -e test
691            
692             perl -MCPAN::YACSmoke -e test('R/RR/RRWO/Some-Dist-0.01.tar.gz')
693            
694             Runs tests on CPAN distributions. Arguments should be paths of
695             individual distributions in the author directories. If no arguments
696             are given, it will download the F file from CPAN and use that.
697            
698             By default it uses CPANPLUS configuration settings. If CPANPLUS is set
699             not to send test reports, then it will not send test reports.
700            
701             For further use of configuration settings see the new() constructor.
702            
703             =cut
704            
705             sub test {
706 0     0 1 0 my $smoker;
707 0         0 eval {
708 0 0 0     0 if ((ref $_[0]) && $_[0]->isa(__PACKAGE__)) {
709 0         0 $smoker = shift;
710             }
711             };
712 0 0       0 my %config = ref($_[0]) eq 'HASH' ? %{ shift() } : ();
  0         0  
713 0   0     0 $smoker ||= __PACKAGE__->new(%config);
714            
715 0         0 $smoker->_audit("\n".('-'x40)."\n");
716            
717 0         0 my @distros = @_;
718 0 0       0 unless (@distros) {
719 0         0 @distros = $smoker->{plugin}->download_list();
720 0 0       0 unless (@distros) {
721 0         0 exit error("No new distributions uploaded to be tested");
722             }
723             }
724            
725 0         0 my %paths = $smoker->_build_path_list(
726             $smoker->_remove_excluded_dists( @distros )
727             );
728            
729             # only test as many distributions as specified
730 0         0 my @testlist;
731 0         0 push @testlist, keys %paths;
732            
733 0         0 foreach my $distpath (sort @testlist) {
734 0 0       0 last unless($smoker->{test_max} > 0);
735            
736 0         0 my @versions = @{ $paths{$distpath} };
  0         0  
737 0         0 my @dirs = split /\/+/, $distpath;
738 0         0 my $dist = $dirs[-1];
739            
740             # When there are multiple recent versions of a distribution, we
741             # only want to test the latest one. If it fails, then we'll
742             # check previous distributions.
743            
744 0         0 my $passed = 0;
745 0         0 my $fail_count = 0;
746 0         0 my $report = 1;
747            
748             # TODO - if test fails due to bad prereqs, set $fail_count to
749             # fail_max and abort testing versions (based on an option)
750            
751 0   0     0 while ( (!$passed) && ($fail_count < $smoker->{fail_max}) &&
      0        
752             (my $ver = shift @versions) ) {
753 0         0 my $distpathver = join("-", $distpath, $ver);
754 0         0 my $distver = join("-", $dist, $ver);
755            
756 0   0     0 my $grade = $smoker->{checked}->{$distver} || 'ungraded';
757            
758 0 0 0     0 if (($grade eq 'ungraded') ||
      0        
759             ($smoker->{allow_retries} && $grade =~ /$smoker->{allow_retries}/)) {
760            
761 0 0       0 my $mod = $smoker->{cpan}->parse_module( module => $distpathver)
762             or error("Invalid distribution $distver\n");
763            
764 0 0 0     0 if ($mod && (!$mod->is_bundle)) {
765 0         0 $smoker->_audit(('-'x40)."\n");
766 0         0 $smoker->_track("Testing $distpathver");
767 0         0 $smoker->{test_max}--;
768 0         0 $report = 1;
769            
770 0         0 eval {
771            
772 0         0 CPANPLUS::Error->flush();
773            
774             # TODO: option to not re-test prereqs that are known to
775             # pass (maybe if we use DBD::SQLite for the database and
776             # mark the date of the result?)
777            
778 0         0 my $stat = $smoker->{cpan}->install(
779             modules => [ $mod ],
780             target => 'create',
781             allow_build_interactively => 0,
782             # other settings now set via set_config() method
783             );
784            
785             # TODO: check the $stat and react appropriately
786            
787 0         0 my $stack = CPANPLUS::Error->stack_as_string();
788 0 0       0 $stack =~ s/\[MSG\] \[[\w: ]+\] Extracted .*?\n//sg if($smoker->{suppress_extracted});
789 0         0 $smoker->_audit($stack);
790            
791             # TODO: option to mark uncompleted tests as aborted vs ungraded
792             # aborted should indicate a fault in testing the distribution
793             # ungraded should indicate a fault in testing a prerequisite
794             # 'Out of memory' faults, known failing prereqs, CPANPLUS faults,
795             # etc should all be covered by these. Otherwise it would be a FAIL.
796            
797 0   0     0 $grade = ($smoker->{checked}->{$distver} ||= 'aborted');
798 0         0 $passed = ($grade eq 'pass');
799            
800 0         0 $smoker->_audit("\nReport Grade for $distver is ".uc($smoker->{checked}->{$distver})."\n");
801            
802             }; # end eval block
803             }
804             } else {
805 0 0       0 if($report == 1) {
806 0         0 $smoker->_audit(('-'x40)."\n");
807 0         0 $report = 0;
808             }
809 0         0 $passed = ($grade eq 'pass');
810 0         0 $smoker->_audit("$distpathver already tested and graded ".uc($grade)."\n");
811             }
812 0 0       0 $fail_count++, unless ($passed);
813            
814             # Mark older versions so that they are not tested
815 0 0       0 if ($passed) {
816 0         0 while (my $ver = shift @versions) {
817 0         0 my $distver = join("-", $dist, $ver);
818 0         0 $smoker->{checked}->{$distver} = "ignored";
819             }
820             }
821             }
822             }
823 0         0 $smoker = undef;
824            
825             # TODO: repository fills up. An option to flush it is needed.
826            
827             }
828            
829             =item mark( [ %config, ] $dist [, $grade ] ] )
830            
831             perl -MCPAN::YACSmoke -e mark('Some-Dist-0.01')
832            
833             perl -MCPAN::YACSmoke -e mark('Some-Dist-0.01', 'fail')
834            
835             Retrieves the test result in the database, or changes the test result.
836            
837             It can be useful to update the status of a distribution that once
838             failed or was untestable but now works, so as to test modules which
839             make use of it.
840            
841             Grades can be one of (case insensitive):
842            
843             aborted = tests aborted (uninstallable prereqs or other failure in test)
844             pass = passed tests
845             fail = failed tests
846             unknown = no tests available
847             na = not applicable to platform or installed libraries
848             ungraded = no grade (test possibly aborted by user)
849             none = undefines a grade
850             ignored = package was ignored (a newer version was tested)
851            
852            
853             For further use of configuration settings see the new() constructor.
854            
855             =cut
856            
857             sub mark {
858 0     0 1 0 my $smoker;
859 0         0 eval {
860 0 0 0     0 if ((ref $_[0]) && $_[0]->isa(__PACKAGE__)) {
861 0         0 $smoker = shift;
862             }
863             };
864            
865 0 0       0 my %config = ref($_[0]) eq 'HASH' ? %{ shift() } : ( verbose => 1, );
  0         0  
866 0   0     0 $smoker ||= __PACKAGE__->new(%config);
867            
868 0         0 $smoker->_audit("\n".('-'x40)."\n");
869            
870 0   0     0 my $distver = shift || "";
871 0   0     0 my $grade = lc shift || "";
872            
873             # See POD above for a description of the grades
874            
875 0 0       0 if ($grade) {
876 0 0       0 unless ($grade =~ /(pass|fail|unknown|na|none|ungraded|aborted|ignored)/) {
877 0         0 return error("Invalid grade: '$grade'");
878             }
879 0 0       0 if ($grade eq "none") {
880 0         0 $grade = undef;
881             }
882 0         0 $smoker->{checked}->{$distver} = $grade;
883 0   0     0 $smoker->_track("result for '$distver' marked as '" . ($grade||"none")."'");
884             } else {
885 0 0       0 my @distros = ($distver ? ($distver) : $smoker->{plugin}->download_list());
886 0         0 my %paths = $smoker->_build_path_list(
887             $smoker->_remove_excluded_dists( @distros )
888             );
889 0         0 foreach my $distpath (sort { versioncmp($a, $b) } keys %paths) {
  0         0  
890 0         0 my $dist = $distpath;
891 0         0 $dist =~ s!.*/!!;
892 0         0 foreach my $ver (@{ $paths{$distpath} }) {
  0         0  
893 0         0 $grade = $smoker->{checked}->{"$dist-$ver"};
894 0 0       0 if ($grade) {
895 0         0 $smoker->_track("result for '$distpath-$ver' is '$grade'");
896             } else {
897 0         0 $smoker->_track("no result for '$distpath-$ver'");
898             }
899             }
900             }
901             }
902 0         0 $smoker = undef;
903 0 0       0 return $grade if($distver);
904             }
905            
906             =item excluded( [ %config, ] [ $dist [, $dist ... ] ] )
907            
908             perl -MCPAN::YACSmoke -e excluded('Some-Dist-0.01')
909            
910             perl -MCPAN::YACSmoke -e excluded()
911            
912             Given a list of distributions, indicates which ones would be excluded from
913             testing, based on the exclude_dist list that is created.
914            
915             For further use of configuration settings see the new() constructor.
916            
917             =cut
918            
919             sub excluded {
920 0     0 1 0 my $smoker;
921 0         0 eval {
922 0 0 0     0 if ((ref $_[0]) && $_[0]->isa(__PACKAGE__)) {
923 0         0 $smoker = shift;
924             }
925             };
926 0 0       0 my %config = ref($_[0]) eq 'HASH' ? %{ shift() } : ();
  0         0  
927 0   0     0 $smoker ||= __PACKAGE__->new(%config);
928            
929 0         0 $smoker->_audit("\n".('-'x40)."\n");
930            
931 0         0 my @distros = @_;
932 0 0       0 unless (@distros) {
933 0         0 @distros = $smoker->{plugin}->download_list();
934 0 0       0 unless (@distros) {
935 0         0 exit err("No new distributions uploaded to be tested");
936             }
937             }
938            
939 0         0 my @dists = $smoker->_remove_excluded_dists( @distros );
940 0         0 $smoker->_audit('EXCLUDED: '.(scalar(@distros) - scalar(@dists))." distributions\n\n");
941 0         0 $smoker = undef;
942 0         0 return @dists;
943             }
944            
945             # TODO: a method to purge older versions of test results from Checked
946             # database. (That is, if the latest version tested is 1.23, we don't
947             # need to keep earlier results around.) There should be an option to
948             # disable this behaviour.
949            
950             =item purge( [ %config, ] [ $dist [, $dist ... ] ] )
951            
952             perl -MCPAN::YACSmoke -e purge()
953            
954             perl -MCPAN::YACSmoke -e purge('Some-Dist-0.01')
955            
956             Purges the entries from the local cpansmoke database. The criteria for purging
957             is that a distribution must have a more recent version, which has previously
958             been marked as a PASS. However, if one or more distributions are passed as a
959             parameter list, those specific distributions will be purged.
960            
961             If the flush_flag is set, via the config hash, to a true value, the directory
962             path created for each older copy of a distribution is deleted.
963            
964             For further use of configuration settings see the new() constructor.
965            
966             =cut
967            
968             sub purge {
969 0     0 1 0 my $smoker;
970 0         0 eval {
971 0 0 0     0 if ((ref $_[0]) && $_[0]->isa(__PACKAGE__)) {
972 0         0 $smoker = shift;
973             }
974             };
975 0 0       0 my %config = ref($_[0]) eq 'HASH' ? %{ shift() } : ();
  0         0  
976 0   0     0 $smoker ||= __PACKAGE__->new(%config);
977            
978 0   0     0 my $flush = $smoker->{flush_flag} || 0;
979 0         0 my %distvars;
980 0         0 my $override = 0;
981            
982 0 0       0 if(@_) {
983 0         0 $override = 1;
984 0         0 for(@_) {
985 0 0       0 next unless(/^(.*)\-(.+)$/);
986 0         0 push @{$distvars{$1}}, $2;
  0         0  
987             }
988             } else {
989 0         0 for(keys %{$smoker->{checked}}) {
  0         0  
990 0 0       0 next unless(/^(.*)\-(.+)$/);
991 0         0 push @{$distvars{$1}}, $2;
  0         0  
992             }
993             }
994            
995 0         0 for my $dist (sort keys %distvars) {
996 0         0 my $passed = $override;
997 0         0 my @vers = sort { versioncmp($a, $b) } @{$distvars{$dist}};
  0         0  
  0         0  
998 0         0 while(@vers) {
999 0         0 my $vers = pop @vers; # the latest
1000 0 0       0 if($passed) {
    0          
1001 0         0 $smoker->_track("'$dist-$vers' ['".
1002             uc($smoker->{checked}->{"$dist-$vers"}).
1003             "'] has been purged");
1004 0         0 delete $smoker->{checked}->{"$dist-$vers"};
1005 0 0       0 if($flush) {
1006 0         0 my $builddir =
1007             file($smoker->basedir(), "$dist-$vers")->stringify;
1008 0 0       0 rmtree($builddir) if(-d $builddir);
1009             }
1010             }
1011             elsif($smoker->{checked}->{"$dist-$vers"} eq 'pass') {
1012 0         0 $passed = 1;
1013             }
1014             }
1015             }
1016            
1017             }
1018            
1019             =item flush( [ %config, ] [ 'all' | 'old' ] )
1020            
1021             perl -MCPAN::YACSmoke -e flush()
1022            
1023             perl -MCPAN::YACSmoke -e flush('all')
1024            
1025             perl -MCPAN::YACSmoke -e flush('old')
1026            
1027             Removes unrequired build directories from the designated CPANPLUS build
1028             directory. Note that this deletes directories regardless of whether the
1029             associated distribution was tested.
1030            
1031             Default flush is 'all'. The 'old' option will only delete the older
1032             distributions, of multiple instances of a distribution.
1033            
1034             Note that this cannot be done reliably using last access or modify time, as
1035             the intention is for this distribution to be used on any OS that CPANPLUS
1036             is installed on. In this case not all OSs support the full range of return
1037             values from the stat function.
1038            
1039             For further use of configuration settings see the new() constructor.
1040            
1041             =cut
1042            
1043             sub flush {
1044 0     0 1 0 my $smoker;
1045 0         0 eval {
1046 0 0 0     0 if ((ref $_[0]) && $_[0]->isa(__PACKAGE__)) {
1047 0         0 $smoker = shift;
1048             }
1049             };
1050 0 0       0 my %config = ref($_[0]) eq 'HASH' ? %{ shift() } : ();
  0         0  
1051 0   0     0 $smoker ||= __PACKAGE__->new(%config);
1052            
1053 0   0     0 my $param = shift || 'all';
1054 0         0 my %dists;
1055            
1056 0         0 opendir(DIR, $smoker->builddir());
1057 0         0 while(my $dir = readdir(DIR)) {
1058 0 0       0 next if($dir =~ /^\.+$/);
1059            
1060 0 0       0 if($param eq 'old') {
1061 0         0 $dir =~ /(.*)-(.+)$extn/;
1062 0         0 $dists{$1}->{$2} = "$dir";
1063             } else {
1064 0         0 rmtree($dir);
1065 0         0 $smoker->_track("'$dir' flushed");
1066             }
1067             }
1068 0         0 closedir(DIR);
1069            
1070 0 0       0 if($param eq 'old') {
1071 0         0 for my $dist (keys %dists) {
1072 0         0 for(sort { versioncmp($a, $b) } keys %{$dists{$dist}}) {
  0         0  
  0         0  
1073 0         0 rmtree($dists{$dist}->{$_});
1074 0         0 $smoker->_track("'$dists{$dist}->{$_}' flushed");
1075             }
1076             }
1077             }
1078            
1079             }
1080            
1081             ## Private Methods
1082            
1083             sub _track {
1084 0     0   0 my ($self,$message) = @_;
1085 0         0 msg($message, $self->{verbose});
1086 0         0 $self->_audit($message);
1087             }
1088            
1089             sub _debug {
1090 0     0   0 my ($self,$message) = @_;
1091 0 0       0 return unless($self->{debug});
1092 0         0 $self->_audit($message);
1093             }
1094            
1095             sub _audit {
1096 1     1   3 my $self = shift;
1097 1 50       8 $self->{audit_cb}->(@_) if($self->{audit_cb});
1098 1 50       6 return unless($self->{audit_log});
1099            
1100 0 0         my $FH = IO::File->new(">>".$self->{audit_log})
1101             or exit error("Failed to write to file [$self->{audit_log}]: $!\n");
1102 0           print $FH join("\n",@_) . "\n";
1103 0           $FH->close;
1104             }
1105            
1106             1;
1107             __END__