Move CPANPLUS from lib/ to ext/
[p5sagit/p5-mst-13.2.git] / ext / CPANPLUS / lib / CPANPLUS / Internals / Report.pm
CommitLineData
6aaee015 1package CPANPLUS::Internals::Report;
2
3use strict;
4
5use CPANPLUS::Error;
6use CPANPLUS::Internals::Constants;
7use CPANPLUS::Internals::Constants::Report;
8
9use Data::Dumper;
10
11use Params::Check qw[check];
6aaee015 12use Module::Load::Conditional qw[can_load];
5bc5f6dc 13use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
6aaee015 14
15$Params::Check::VERBOSE = 1;
16
17### for the version ###
18require CPANPLUS::Internals;
19
20=head1 NAME
21
22CPANPLUS::Internals::Report
23
24=head1 SYNOPSIS
25
26 ### enable test reporting
27 $cb->configure_object->set_conf( cpantest => 1 );
28
29 ### set custom mx host, shouldn't normally be needed
30 $cb->configure_object->set_conf( cpantest_mx => 'smtp.example.com' );
31
32=head1 DESCRIPTION
33
34This module provides all the functionality to send test reports to
35C<http://testers.cpan.org> using the C<Test::Reporter> module.
36
37All methods will be called automatically if you have C<CPANPLUS>
38configured to enable test reporting (see the C<SYNOPSIS>).
39
40=head1 METHODS
41
42=head2 $bool = $cb->_have_query_report_modules
43
44This function checks if all the required modules are here for querying
45reports. It returns true and loads them if they are, or returns false
46otherwise.
47
48=head2 $bool = $cb->_have_send_report_modules
49
50This function checks if all the required modules are here for sending
51reports. It returns true and loads them if they are, or returns false
52otherwise.
53
54=cut
5879cbe1 55
56### XXX remove this list and move it into selfupdate, somehow..
57### this is dual administration
6aaee015 58{ my $query_list = {
5879cbe1 59 'File::Fetch' => '0.13_02',
5bc5f6dc 60 'YAML::Tiny' => '0.0',
61 'File::Temp' => '0.0',
6aaee015 62 };
63
64 my $send_list = {
65 %$query_list,
5bc5f6dc 66 'Test::Reporter' => '1.34',
6aaee015 67 };
68
69 sub _have_query_report_modules {
70 my $self = shift;
71 my $conf = $self->configure_object;
72 my %hash = @_;
73
74 my $tmpl = {
75 verbose => { default => $conf->get_conf('verbose') },
76 };
77
78 my $args = check( $tmpl, \%hash ) or return;
79
80 return can_load( modules => $query_list, verbose => $args->{verbose} )
81 ? 1
82 : 0;
83 }
84
85 sub _have_send_report_modules {
86 my $self = shift;
87 my $conf = $self->configure_object;
88 my %hash = @_;
89
90 my $tmpl = {
91 verbose => { default => $conf->get_conf('verbose') },
92 };
93
94 my $args = check( $tmpl, \%hash ) or return;
95
96 return can_load( modules => $send_list, verbose => $args->{verbose} )
97 ? 1
98 : 0;
99 }
100}
101
102=head2 @list = $cb->_query_report( module => $modobj, [all_versions => BOOL, verbose => BOOL] )
103
104This function queries the CPAN testers database at
105I<http://testers.cpan.org/> for test results of specified module objects,
4443dd53 106module names or distributions.
6aaee015 107
108The optional argument C<all_versions> controls whether all versions of
109a given distribution should be grabbed. It defaults to false
110(fetching only reports for the current version).
111
112Returns the a list with the following data structures (for CPANPLUS
4443dd53 113version 0.042) on success, or false on failure. The contents of the
114data structure depends on what I<http://testers.cpan.org> returns,
115but generally looks like this:
6aaee015 116
117 {
118 'grade' => 'PASS',
119 'dist' => 'CPANPLUS-0.042',
120 'platform' => 'i686-pld-linux-thread-multi'
4443dd53 121 'details' => 'http://nntp.x.perl.org/group/perl.cpan.testers/98316'
122 ...
6aaee015 123 },
124 {
125 'grade' => 'PASS',
126 'dist' => 'CPANPLUS-0.042',
127 'platform' => 'i686-linux-thread-multi'
4443dd53 128 'details' => 'http://nntp.x.perl.org/group/perl.cpan.testers/99416'
129 ...
6aaee015 130 },
131 {
132 'grade' => 'FAIL',
133 'dist' => 'CPANPLUS-0.042',
134 'platform' => 'cygwin-multi-64int',
135 'details' => 'http://nntp.x.perl.org/group/perl.cpan.testers/99371'
4443dd53 136 ...
6aaee015 137 },
138 {
139 'grade' => 'FAIL',
140 'dist' => 'CPANPLUS-0.042',
141 'platform' => 'i586-linux',
142 'details' => 'http://nntp.x.perl.org/group/perl.cpan.testers/99396'
4443dd53 143 ...
6aaee015 144 },
145
146The status of the test can be one of the following:
147UNKNOWN, PASS, FAIL or NA (not applicable).
148
149=cut
150
151sub _query_report {
152 my $self = shift;
153 my $conf = $self->configure_object;
154 my %hash = @_;
155
156 my($mod, $verbose, $all);
157 my $tmpl = {
158 module => { required => 1, allow => IS_MODOBJ,
159 store => \$mod },
160 verbose => { default => $conf->get_conf('verbose'),
161 store => \$verbose },
162 all_versions => { default => 0, store => \$all },
163 };
164
165 check( $tmpl, \%hash ) or return;
166
167 ### check if we have the modules we need for querying
168 return unless $self->_have_query_report_modules( verbose => 1 );
169
6aaee015 170
5bc5f6dc 171 ### XXX no longer use LWP here. However, that means we don't
172 ### automagically set proxies anymore!!!
173 # my $ua = LWP::UserAgent->new;
174 # $ua->agent( CPANPLUS_UA->() );
175 #
6aaee015 176 ### set proxies if we have them ###
5bc5f6dc 177 # $ua->env_proxy();
6aaee015 178
179 my $url = TESTERS_URL->($mod->package_name);
5bc5f6dc 180 my $ff = File::Fetch->new( uri => $url );
6aaee015 181
182 msg( loc("Fetching: '%1'", $url), $verbose );
183
5bc5f6dc 184 my $res = do {
185 my $tempdir = File::Temp::tempdir();
186 my $where = $ff->fetch( to => $tempdir );
187
188 unless( $where ) {
189 error( loc( "Fetching report for '%1' failed: %2",
190 $url, $ff->error ) );
191 return;
192 }
6aaee015 193
5bc5f6dc 194 my $fh = OPEN_FILE->( $where );
195
196 do { local $/; <$fh> };
197 };
198
199 my ($aref) = eval { YAML::Tiny::Load( $res ) };
6aaee015 200
5bc5f6dc 201 if( $@ ) {
202 error(loc("Error reading result: %1", $@));
203 return;
204 };
6aaee015 205
4443dd53 206 my $dist = $mod->package_name .'-'. $mod->package_version;
207 my $details = TESTERS_DETAILS_URL->($mod->package_name);
6aaee015 208
209 my @rv;
210 for my $href ( @$aref ) {
211 next unless $all or defined $href->{'distversion'} &&
212 $href->{'distversion'} eq $dist;
213
4443dd53 214 $href->{'details'} = $details;
215
216 ### backwards compatibility :(
a3de5d0b 217 $href->{'dist'} ||= $href->{'distversion'};
218 $href->{'grade'} ||= $href->{'action'} || $href->{'status'};
4443dd53 219
220 push @rv, $href;
6aaee015 221 }
222
223 return @rv if @rv;
224 return;
225}
226
227=pod
228
4443dd53 229=head2 $bool = $cb->_send_report( module => $modobj, buffer => $make_output, failed => BOOL, [save => BOOL, address => $email_to, verbose => BOOL, force => BOOL]);
6aaee015 230
231This function sends a testers report to C<cpan-testers@perl.org> for a
232particular distribution.
233It returns true on success, and false on failure.
234
235It takes the following options:
236
237=over 4
238
239=item module
240
241The module object of this particular distribution
242
243=item buffer
244
245The output buffer from the 'make/make test' process
246
247=item failed
248
249Boolean indicating if the 'make/make test' went wrong
250
251=item save
252
253Boolean indicating if the report should be saved locally instead of
254mailed out. If provided, this function will return the location the
255report was saved to, rather than a simple boolean 'TRUE'.
256
257Defaults to false.
258
259=item address
260
261The email address to mail the report for. You should never need to
262override this, but it might be useful for debugging purposes.
263
264Defaults to C<cpan-testers@perl.org>.
265
6aaee015 266=item verbose
267
268Boolean indicating on whether or not to be verbose.
269
270Defaults to your configuration settings
271
272=item force
273
274Boolean indicating whether to force the sending, even if the max
275amount of reports for fails have already been reached, or if you
276may already have sent it before.
277
278Defaults to your configuration settings
279
280=back
281
282=cut
283
284sub _send_report {
285 my $self = shift;
286 my $conf = $self->configure_object;
287 my %hash = @_;
288
289 ### do you even /have/ test::reporter? ###
290 unless( $self->_have_send_report_modules(verbose => 1) ) {
291 error( loc( "You don't have '%1' (or modules required by '%2') ".
292 "installed, you cannot report test results.",
293 'Test::Reporter', 'Test::Reporter' ) );
294 return;
295 }
296
297 ### check arguments ###
4443dd53 298 my ($buffer, $failed, $mod, $verbose, $force, $address, $save,
6aaee015 299 $tests_skipped );
300 my $tmpl = {
301 module => { required => 1, store => \$mod, allow => IS_MODOBJ },
302 buffer => { required => 1, store => \$buffer },
303 failed => { required => 1, store => \$failed },
304 address => { default => CPAN_TESTERS_EMAIL, store => \$address },
305 save => { default => 0, store => \$save },
6aaee015 306 verbose => { default => $conf->get_conf('verbose'),
307 store => \$verbose },
308 force => { default => $conf->get_conf('force'),
309 store => \$force },
310 tests_skipped
311 => { default => 0, store => \$tests_skipped },
312 };
313
314 check( $tmpl, \%hash ) or return;
315
316 ### get the data to fill the email with ###
317 my $name = $mod->module;
318 my $dist = $mod->package_name . '-' . $mod->package_version;
319 my $author = $mod->author->author;
320 my $email = $mod->author->email || CPAN_MAIL_ACCOUNT->( $author );
321 my $cp_conf = $conf->get_conf('cpantest') || '';
322 my $int_ver = $CPANPLUS::Internals::VERSION;
323 my $cb = $mod->parent;
324
325
4443dd53 326 ### will be 'fetch', 'make', 'test', 'install', etc ###
327 my $stage = TEST_FAIL_STAGE->($buffer);
328
6aaee015 329 ### determine the grade now ###
330
331 my $grade;
332 ### check if this is a platform specific module ###
333 ### if we failed the test, there may be reasons why
334 ### an 'NA' might have to be insted
335 GRADE: { if ( $failed ) {
336
337
338 ### XXX duplicated logic between this block
339 ### and REPORTED_LOADED_PREREQS :(
340
341 ### figure out if the prereqs are on CPAN at all
342 ### -- if not, send NA grade
343 ### Also, if our version of prereqs is too low,
344 ### -- send NA grade.
345 ### This is to address bug: #25327: do not count
346 ### as FAIL modules where prereqs are not filled
347 { my $prq = $mod->status->prereqs || {};
348
349 while( my($prq_name,$prq_ver) = each %$prq ) {
350 my $obj = $cb->module_tree( $prq_name );
4443dd53 351 my $sub = CPANPLUS::Module->can(
352 'module_is_supplied_with_perl_core' );
6aaee015 353
4443dd53 354 ### if we can't find the module and it's not supplied with core.
355 ### this addresses: #32064: NA reports generated for failing
356 ### tests where core prereqs are specified
357 ### Note that due to a bug in Module::CoreList, in some released
358 ### version of perl (5.8.6+ and 5.9.2-4 at the time of writing)
359 ### 'Config' is not recognized as a core module. See this bug:
360 ### http://rt.cpan.org/Ticket/Display.html?id=32155
361 if( not $obj and not $sub->( $prq_name ) ) {
6aaee015 362 msg(loc( "Prerequisite '%1' for '%2' could not be obtained".
363 " from CPAN -- sending N/A grade",
364 $prq_name, $name ), $verbose );
365
366 $grade = GRADE_NA;
367 last GRADE;
368 }
369
370 if( $cb->_vcmp( $prq_ver, $obj->installed_version ) > 0 ) {
371 msg(loc( "Installed version of '%1' ('%2') is too low for ".
372 "'%3' (needs '%4') -- sending N/A grade",
373 $prq_name, $obj->installed_version,
374 $name, $prq_ver ), $verbose );
375
376 $grade = GRADE_NA;
377 last GRADE;
378 }
379 }
380 }
381
382 unless( RELEVANT_TEST_RESULT->($mod) ) {
383 msg(loc(
384 "'%1' is a platform specific module, and the test results on".
385 " your platform are not relevant --sending N/A grade.",
386 $name), $verbose);
387
388 $grade = GRADE_NA;
389
390 } elsif ( UNSUPPORTED_OS->( $buffer ) ) {
391 msg(loc(
392 "'%1' is a platform specific module, and the test results on".
393 " your platform are not relevant --sending N/A grade.",
394 $name), $verbose);
395
396 $grade = GRADE_NA;
397
398 ### you dont have a high enough perl version?
399 } elsif ( PERL_VERSION_TOO_LOW->( $buffer ) ) {
400 msg(loc("'%1' requires a higher version of perl than your current ".
401 "version -- sending N/A grade.", $name), $verbose);
402
403 $grade = GRADE_NA;
404
405 ### perhaps where were no tests...
406 ### see if the thing even had tests ###
407 } elsif ( NO_TESTS_DEFINED->( $buffer ) ) {
408 $grade = GRADE_UNKNOWN;
4443dd53 409 ### failures in PL or make/build stage are now considered UNKNOWN
410 } elsif ( $stage !~ /\btest\b/ ) {
411
412 $grade = GRADE_UNKNOWN
6aaee015 413
414 } else {
415
416 $grade = GRADE_FAIL;
417 }
418
419 ### if we got here, it didn't fail and tests were present.. so a PASS
420 ### is in order
421 } else {
422 $grade = GRADE_PASS;
423 } }
424
425 ### so an error occurred, let's see what stage it went wrong in ###
4443dd53 426
427 ### the header -- always include so the CPANPLUS version is apparent
428 my $message = REPORT_MESSAGE_HEADER->( $int_ver, $author );
429
6aaee015 430 if( $grade eq GRADE_FAIL or $grade eq GRADE_UNKNOWN) {
431
432 ### return if one or more missing external libraries
433 if( my @missing = MISSING_EXTLIBS_LIST->($buffer) ) {
434 msg(loc("Not sending test report - " .
435 "external libraries not pre-installed"));
436 return 1;
437 }
438
6aaee015 439 ### return if we're only supposed to report make_test failures ###
440 return 1 if $cp_conf =~ /\bmaketest_only\b/i
441 and ($stage !~ /\btest\b/);
442
6aaee015 443 ### the bit where we inform what went wrong
444 $message .= REPORT_MESSAGE_FAIL_HEADER->( $stage, $buffer );
445
446 ### was it missing prereqs? ###
447 if( my @missing = MISSING_PREREQS_LIST->($buffer) ) {
448 if(!$self->_verify_missing_prereqs(
449 module => $mod,
450 missing => \@missing
451 )) {
452 msg(loc("Not sending test report - " .
453 "bogus missing prerequisites report"));
454 return 1;
455 }
456 $message .= REPORT_MISSING_PREREQS->($author,$email,@missing);
457 }
458
459 ### was it missing test files? ###
460 if( NO_TESTS_DEFINED->($buffer) ) {
461 $message .= REPORT_MISSING_TESTS->();
462 }
463
464 ### add a list of what modules have been loaded of your prereqs list
465 $message .= REPORT_LOADED_PREREQS->($mod);
466
467 ### the footer
5bc5f6dc 468 $message .= REPORT_MESSAGE_FOOTER->();
6aaee015 469
470 ### it may be another grade than fail/unknown.. may be worth noting
471 ### that tests got skipped, since the buffer is not added in
472 } elsif ( $tests_skipped ) {
473 $message .= REPORT_TESTS_SKIPPED->();
4443dd53 474 } elsif( $grade eq GRADE_NA) {
475
476 ### the bit where we inform what went wrong
477 $message .= REPORT_MESSAGE_FAIL_HEADER->( $stage, $buffer );
6aaee015 478
4443dd53 479 ### the footer
480 $message .= REPORT_MESSAGE_FOOTER->();
6aaee015 481
6aaee015 482 }
4443dd53 483
5bc5f6dc 484 msg( loc("Sending test report for '%1'", $dist), $verbose);
6aaee015 485
486 ### reporter object ###
4443dd53 487 my $reporter = do {
488 my $args = $conf->get_conf('cpantest_reporter_args') || {};
489
490 unless( UNIVERSAL::isa( $args, 'HASH' ) ) {
491 error(loc("'%1' must be a hashref, ignoring...",
492 'cpantest_reporter_args'));
493 $args = {};
494 }
495
496 Test::Reporter->new(
497 grade => $grade,
498 distribution => $dist,
499 via => "CPANPLUS $int_ver",
500 timeout => $conf->get_conf('timeout') || 60,
501 debug => $conf->get_conf('debug'),
502 %$args,
503 );
504 };
505
6aaee015 506 ### set a custom mx, if requested
507 $reporter->mx( [ $conf->get_conf('cpantest_mx') ] )
508 if $conf->get_conf('cpantest_mx');
509
510 ### set the from address ###
511 $reporter->from( $conf->get_conf('email') )
512 if $conf->get_conf('email') !~ /\@example\.\w+$/i;
513
514 ### give the user a chance to programattically alter the message
515 $message = $self->_callbacks->munge_test_report->($mod, $message, $grade);
516
517 ### add the body if we have any ###
518 $reporter->comments( $message ) if defined $message && length $message;
519
520 ### do a callback to ask if we should send the report
521 unless ($self->_callbacks->send_test_report->($mod, $grade)) {
522 msg(loc("Ok, not sending test report"));
523 return 1;
524 }
525
526 ### do a callback to ask if we should edit the report
527 if ($self->_callbacks->edit_test_report->($mod, $grade)) {
528 ### test::reporter 1.20 and lower don't have a way to set
529 ### the preferred editor with a method call, but it does
530 ### respect your env variable, so let's set that.
531 local $ENV{VISUAL} = $conf->get_program('editor')
532 if $conf->get_program('editor');
533
534 $reporter->edit_comments;
535 }
536
6aaee015 537 ### allow to be overridden, but default to the normal address ###
538 $reporter->address( $address );
539
540 ### should we save it locally? ###
541 if( $save ) {
542 if( my $file = $reporter->write() ) {
543 msg(loc("Successfully wrote report for '%1' to '%2'",
544 $dist, $file), $verbose);
545 return $file;
546
547 } else {
548 error(loc("Failed to write report for '%1'", $dist));
549 return;
550 }
551
6aaee015 552 ### XXX should we do an 'already sent' check? ###
4443dd53 553 } elsif( $reporter->send( ) ) {
6aaee015 554 msg(loc("Successfully sent '%1' report for '%2'", $grade, $dist),
555 $verbose);
556 return 1;
557
558 ### something broke :( ###
559 } else {
560 error(loc("Could not send '%1' report for '%2': %3",
561 $grade, $dist, $reporter->errstr));
562 return;
563 }
564}
565
566sub _verify_missing_prereqs {
567 my $self = shift;
568 my %hash = @_;
569
570 ### check arguments ###
571 my ($mod, $missing);
572 my $tmpl = {
573 module => { required => 1, store => \$mod },
574 missing => { required => 1, store => \$missing },
575 };
576
577 check( $tmpl, \%hash ) or return;
578
579
580 my %missing = map {$_ => 1} @$missing;
581 my $conf = $self->configure_object;
582 my $extract = $mod->status->extract;
583
584 ### Read pre-requisites from Makefile.PL or Build.PL (if there is one),
585 ### of the form:
586 ### 'PREREQ_PM' => {
587 ### 'Compress::Zlib' => '1.20',
588 ### 'Test::More' => 0,
589 ### },
590 ### Build.PL uses 'requires' instead of 'PREREQ_PM'.
591
592 my @search;
593 push @search, ($extract ? MAKEFILE_PL->( $extract ) : MAKEFILE_PL->());
594 push @search, ($extract ? BUILD_PL->( $extract ) : BUILD_PL->());
595
596 for my $file ( @search ) {
597 if(-e $file and -r $file) {
598 my $slurp = $self->_get_file_contents(file => $file);
599 my ($prereq) =
600 ($slurp =~ /'?(?:PREREQ_PM|requires)'?\s*=>\s*{(.*?)}/s);
601 my @prereq =
602 ($prereq =~ /'?([\w\:]+)'?\s*=>\s*'?\d[\d\.\-\_]*'?/sg);
603 delete $missing{$_} for(@prereq);
604 }
605 }
606
607 return 1 if(keys %missing); # There ARE missing prerequisites
608 return; # All prerequisites accounted for
609}
610
6111;
612
613
614# Local variables:
615# c-indentation-style: bsd
616# c-basic-offset: 4
617# indent-tabs-mode: nil
618# End:
619# vim: expandtab shiftwidth=4: