File Coverage

blib/lib/CPAN/Testers/Common/Client.pm
Criterion Covered Total %
statement 223 245 91.0
branch 60 84 71.4
condition 27 44 61.3
subroutine 44 45 97.7
pod 12 12 100.0
total 366 430 85.1


line stmt bran cond sub pod time code
1             # TODO: several resources per client?
2             package CPAN::Testers::Common::Client;
3 4     4   83675 use warnings;
  4         7  
  4         151  
4 4     4   16 use strict;
  4         5  
  4         86  
5              
6 4     4   1896 use Devel::Platform::Info;
  4         1742  
  4         117  
7 4     4   1727 use Probe::Perl;
  4         5592  
  4         125  
8 4     4   2202 use Config::Perl::V;
  4         7511  
  4         208  
9 4     4   28 use Carp ();
  4         6  
  4         68  
10 4     4   18 use File::Spec;
  4         5  
  4         92  
11 4     4   1681 use Capture::Tiny qw(capture);
  4         141279  
  4         289  
12 4     4   1864 use CPAN::Testers::Common::Client::PrereqCheck;
  4         13  
  4         136  
13 4     4   1564 use CPAN::Testers::Common::Client::History;
  4         12  
  4         137  
14              
15 4     4   23 use constant MAX_OUTPUT_LENGTH => 1_000_000;
  4         5  
  4         9946  
16              
17             our $VERSION = '0.12';
18              
19              
20             #==================================
21             # CONSTRUCTOR
22             #==================================
23              
24             sub new {
25 2     2 1 2231 my ($class, %params) = @_;
26 2         5 my $self = bless {}, $class;
27              
28 2 50       7 Carp::croak q[Please specify a distname] unless $params{distname};
29 2 50       6 Carp::croak q[Please specify the dist's author] unless $params{author};
30 2 50       6 Carp::croak q[Please specify a grade for the dist] unless $params{grade};
31              
32 2         8 $self->_init( %params );
33              
34 2         7 return $self;
35             }
36              
37             sub _init {
38 2     2   5 my ($self, %params) = @_;
39              
40 2         5 $self->grade( $params{grade} );
41 2         4 $self->distname( $params{distname} );
42 2         4 $self->author( $params{author} );
43              
44             $self->via( exists $params{via}
45             ? $params{via}
46 2 100       11 : "your friendly CPAN Testers client version $VERSION"
47             );
48              
49             $self->comments( exists $params{comments}
50             ? $params{comments}
51             : $ENV{AUTOMATED_TESTING}
52 2 50       8 ? "this report is from an automated smoke testing program\nand was not reviewed by a human for accuracy"
    100          
53             : 'none provided'
54             );
55              
56 2 100       6 $self->command( $params{command} ) if exists $params{command};
57              
58 2 100       7 if ( $params{prereqs} ) {
    50          
59             $self->{_meta}{prereqs} = $params{prereqs}
60 1         4 }
61             elsif ( $params{build_dir} ) {
62 0         0 $self->_get_prereqs( $params{build_dir} );
63             }
64              
65 2         5 foreach my $output ( qw( configure build test ) ) {
66 6         7 my $key = $output . '_output';
67 6 100       11 if (exists $params{$key}) {
68 3         8 $self->{_output}{$output} = $params{$key};
69             }
70             }
71              
72 2         4 return;
73             }
74              
75              
76             #======================================
77             # ACCESSORS
78             #======================================
79              
80             sub comments {
81 7     7 1 13 my ($self, $comment) = @_;
82 7 100       17 $self->{_comment} = $comment if $comment;
83 7         27 return $self->{_comment};
84             }
85              
86             sub via {
87 5     5 1 6 my ($self, $via) = @_;
88 5 100       15 $self->{_via} = $via if $via;
89 5         14 return $self->{_via};
90             }
91              
92             sub author {
93 5     5 1 616 my ($self, $author) = @_;
94 5 100       11 $self->{_author} = $author if $author;
95 5         14 return $self->{_author};
96             }
97              
98              
99             #FIXME? the distname in CPAN::Reporter is validated
100             # under a specific regex in line 368. We should
101             # move that logic here.
102             sub distname {
103 7     7 1 10 my ($self, $distname) = @_;
104 7 100       15 $self->{_distname} = $distname if $distname;
105              
106 7         20 return $self->{_distname};
107             }
108              
109             sub grade {
110 11     11 1 20 my ($self, $grade) = @_;
111 11 100       31 $self->{_grade} = lc $grade if $grade;
112 11         77 return $self->{_grade};
113             }
114              
115             sub command {
116 4     4 1 7 my ($self, $command) = @_;
117 4 100       12 $self->{_command} = $command if $command;
118 4   100     31 return $self->{_command} || '';
119             }
120              
121             #====================================
122             # PUBLIC METHODS
123             #====================================
124              
125             sub is_duplicate {
126 1     1 1 2 my ($self) = @_;
127              
128 1         2 my $grade = $self->grade;
129 1         2 my $dist_name = $self->distname;
130 1 50 33     6 return 0 unless $grade && $dist_name;
131              
132             #FIXME: CPAN::Reporter allows for 3 phases: 'PL', 'make' or 'test'.
133             # Until this is properly ported, we'll only use the 'test' phase.
134 1         4 return CPAN::Testers::Common::Client::History::is_duplicate({
135             phase => 'test',
136             grade => $grade,
137             dist_name => $dist_name,
138             });
139             }
140              
141             sub record_history {
142 1     1 1 6 my ($self) = @_;
143              
144 1         6 my $grade = $self->grade;
145 1         5 my $dist_name = $self->distname;
146 1 50 33     13 return unless $grade && $dist_name;
147              
148             #FIXME: CPAN::Reporter allows for 3 phases: 'PL', 'make' or 'test'.
149             # Until this is properly ported, we'll only use the 'test' phase.
150 1         11 return CPAN::Testers::Common::Client::History::record_history({
151             phase => 'test',
152             grade => $grade,
153             dist_name => $dist_name,
154             });
155             }
156              
157             sub populate {
158 2     2 1 3 my $self = shift;
159              
160             # some data is repeated between facts, so we keep a 'cache'
161 2         14 $self->{_config} = Config::Perl::V::myconfig();
162 2         1209 $self->{_platform} = Devel::Platform::Info->new->get_info();
163              
164             # LegacyReport creates the email, therefore it must
165             # be set last so all other data is already in place.
166 2         395573 my @facts = qw(
167             TestSummary TestOutput TesterComment
168             Prereqs InstalledModules
169             PlatformInfo PerlConfig TestEnvironment
170             LegacyReport
171             );
172              
173 2         8 foreach my $fact ( @facts ) {
174 18         30 my $populator = '_populate_' . lc $fact;
175 18         69 $self->{_data}{$fact} = $self->$populator;
176             }
177              
178 2         7 return $self->metabase_data;
179             }
180              
181 7     7 1 2490 sub metabase_data { return shift->{_data} }
182              
183             sub email {
184 2     2 1 4 my $self = shift;
185 2   66     6 my $metabase_data = $self->metabase_data || $self->populate;
186              
187 2         12 return $metabase_data->{LegacyReport}{textreport};
188             }
189              
190              
191             #===================================================
192             # POPULATORS -- these functions populate
193             # the object with data, triggered by the
194             # populate() method.
195             #===================================================
196              
197             sub _populate_platforminfo {
198 2     2   5 my $self = shift;
199 2         9 return $self->{_platform};
200             }
201              
202              
203             sub _populate_perlconfig {
204 2     2   5 my $self = shift;
205 2         5 return @{ $self->{_config} }{qw(build config)};
  2         15  
206             }
207              
208             sub _populate_testenvironment {
209              
210             return {
211 2     2   10 environment_vars => _get_env_vars(),
212             special_vars => _get_special_vars(),
213             };
214             }
215              
216             sub _populate_prereqs {
217 2     2   3 my $self = shift;
218              
219             # TODO: update Fact::Prereqs to use the new meta::spec for prereqs
220             # TODO: add the 'test' prereqs?
221             return $self->{_meta}{prereqs}
222             || {
223 2   100     28 runtime => { requires => {} },
224             build => { requires => {} },
225             configure => { requires => {} },
226             };
227             #{
228             # configure_requires => $self->{_meta}{configure_requires} || {},
229             # build_requires => $self->{_meta}{build_requires} || {},
230             # requires => $self->{_meta}{requires} || {},
231             #};
232             }
233              
234             sub _populate_testercomment {
235 2     2   48 my $self = shift;
236 2         9 return $self->comments;
237             }
238              
239             sub _populate_installedmodules {
240 2     2   3 my $self = shift;
241              
242 2         16 my @toolchain_mods= qw(
243             CPAN
244             CPAN::Meta
245             Cwd
246             ExtUtils::CBuilder
247             ExtUtils::Command
248             ExtUtils::Install
249             ExtUtils::MakeMaker
250             ExtUtils::Manifest
251             ExtUtils::ParseXS
252             File::Spec
253             JSON
254             JSON::PP
255             Module::Build
256             Module::Signature
257             Parse::CPAN::Meta
258             Test::Harness
259             Test::More
260             YAML
261             YAML::Syck
262             version
263             );
264              
265 2         4 my $results = _version_finder( map { $_ => 0 } @toolchain_mods );
  40         42  
266              
267 2         17 my %toolchain = map { $_ => $results->{$_}{have} } @toolchain_mods;
  40         55  
268 2         6 my %prereqs = ();
269              
270 2         36 return { prereqs => \%prereqs, toolchain => \%toolchain };
271             }
272              
273              
274             sub _populate_legacyreport {
275 2     2   4 my $self = shift;
276             return {
277 2         2 %{ $self->_populate_testsummary },
  2         7  
278             textreport => $self->_create_email,
279             }
280             }
281              
282             sub _populate_testsummary {
283 4     4   10 my $self = shift;
284              
285             return {
286             grade => $self->grade,
287             osname => $self->{_platform}{osname},
288             osversion => $self->{_platform}{osvers},
289             archname => $self->{_platform}{archname},
290             perl_version => $self->{_config}{config}{version},
291             }
292 4         24 }
293              
294             sub _populate_testoutput {
295 2     2   5 my $self = shift;
296 2         9 return $self->{_output};
297             }
298              
299              
300             #=====================================================
301             # FORMATTERS -- functions to aid email formatting
302             #=====================================================
303              
304             sub _format_vars_report {
305 4     4   4 my $variables = shift;
306              
307 4         5 my $report = "";
308 4         25 foreach my $var ( sort keys %$variables ) {
309 30         23 my $value = $variables->{$var};
310 30 50       35 $value = '[undef]' if ! defined $value;
311 30         36 $report .= " $var = $value\n";
312             }
313 4         13 return $report;
314             }
315              
316 124 100   124   246 sub _fix_unknown { defined $_[0] ? $_[0] : 'unknown' }
317              
318             sub _format_toolchain_report {
319 3     3   1644 my $installed = shift;
320 3         34 my $mod_width = _max_length( keys %$installed );
321             my $ver_width = _max_length(
322 3         12 map { _fix_unknown( $installed->{$_} ) } keys %$installed
  44         43  
323             );
324              
325 3         14 my $format = " \%-${mod_width}s \%-${ver_width}s\n";
326              
327 3         7 my $report = "";
328 3         18 $report .= sprintf( $format, "Module", "Have" );
329 3         17 $report .= sprintf( $format, "-" x $mod_width, "-" x $ver_width );
330              
331 3         26 for my $var ( sort keys %$installed ) {
332             $report .= sprintf(" \%-${mod_width}s \%-${ver_width}s\n",
333 44         61 $var, _fix_unknown($installed->{$var}) );
334             }
335              
336 3         47 return $report;
337             }
338              
339             sub _format_prereq_report {
340 4     4   2331 my $prereqs = shift;
341 4         7 my (%have, %prereq_met, $report);
342              
343 4         15 my @prereq_sections = qw( runtime build configure );
344              
345             # see what prereqs are satisfied in subprocess
346 4         9 foreach my $section ( @prereq_sections ) {
347 12         28 my $requires = $prereqs->{$section}{requires};
348 12 100 66     112 next unless $requires and ref $requires eq 'HASH' and keys %$requires > 0;
      100        
349              
350 5         28 my $results = _version_finder( %$requires );
351              
352 5         38 foreach my $mod ( keys %$results ) {
353 11         36 $have{$section}{$mod} = $results->{$mod}{have};
354 11         39 $prereq_met{$section}{$mod} = $results->{$mod}{met};
355             }
356             }
357              
358             # find formatting widths
359 4         9 my ($name_width, $need_width, $have_width) = (6, 4, 4);
360 4         10 foreach my $section ( @prereq_sections ) {
361 12         26 my $requires = $prereqs->{$section}{requires};
362 12 100 66     69 next unless $requires and ref $requires eq 'HASH';
363              
364 8         24 foreach my $module ( keys %$requires ) {
365 18         21 my $name_length = length $module;
366 18         35 my $need_length = length $requires->{$module};
367 18         54 my $have_length = length _fix_unknown( $have{$section}{$module} );
368 18 100       45 $name_width = $name_length if $name_length > $name_width;
369 18 50       28 $need_width = $need_length if $need_length > $need_width;
370 18 100       42 $have_width = $have_length if $have_length > $have_width;
371             }
372             }
373              
374 4         20 my $format_str =
375             " \%1s \%-${name_width}s \%-${need_width}s \%-${have_width}s\n";
376              
377             # generate the report
378 4         9 foreach my $section ( @prereq_sections ) {
379 12         19 my $requires = $prereqs->{$section}{requires};
380 12 100 66     72 next unless $requires and ref $requires eq 'HASH' and keys %$requires;
      100        
381              
382 5         58 $report .= "$section:\n\n"
383             . sprintf( $format_str, " ", qw/Module Need Have/ )
384             . sprintf( $format_str, " ",
385             "-" x $name_width,
386             "-" x $need_width,
387             "-" x $have_width
388             );
389              
390 5         33 foreach my $module ( sort {lc $a cmp lc $b} keys %$requires ) {
  27         43  
391 18         28 my $need = $requires->{$module};
392 18         39 my $have = _fix_unknown( $have{$section}{$module} );
393 18 100       48 my $bad = $prereq_met{$section}{$module} ? " " : "!";
394 18         65 $report .= sprintf( $format_str, $bad, $module, $need, $have);
395             }
396 5         13 $report .= "\n";
397             }
398              
399 4   100     73 return $report || " No requirements found\n";
400             }
401              
402              
403             #==============================================
404             # AUXILIARY (PRIVATE) METHODS AND FUNCTIONS
405             #==============================================
406              
407             sub _get_env_vars {
408             # Entries bracketed with "/" are taken to be a regex; otherwise literal
409 2     2   17 my @env_vars= qw(
410             /HARNESS/
411             /LC_/
412             /PERL/
413             /_TEST/
414             CCFLAGS
415             COMSPEC
416             INCLUDE
417             INSTALL_BASE
418             LANG
419             LANGUAGE
420             LD_LIBRARY_PATH
421             LDFLAGS
422             LIB
423             NON_INTERACTIVE
424             NUMBER_OF_PROCESSORS
425             PATH
426             PREFIX
427             PROCESSOR_IDENTIFIER
428             SHELL
429             TERM
430             TEMP
431             TMPDIR
432             );
433              
434 2         6 my %env_found = ();
435 2         6 foreach my $var ( @env_vars ) {
436 44 100       82 if ( $var =~ m{^/(.+)/$} ) {
437 8         15 my $re = $1;
438 8         30 foreach my $found ( grep { /$re/ } keys %ENV ) {
  176         234  
439 18 50       47 $env_found{$found} = $ENV{$found} if exists $ENV{$found};
440             }
441             }
442             else {
443 36 100       53 $env_found{$var} = $ENV{$var} if exists $ENV{$var};
444             }
445             }
446              
447 2         15 return \%env_found;
448             }
449              
450             sub _get_special_vars {
451 2     2   37 my %special_vars = (
452             EXECUTABLE_NAME => $^X,
453             UID => $<,
454             EUID => $>,
455             GID => $(,
456             EGID => $),
457             );
458              
459 2 50 33     10 if ( $^O eq 'MSWin32' && eval 'require Win32' ) { ## no critic
460 0         0 $special_vars{'Win32::GetOSName'} = Win32::GetOSName();
461 0         0 $special_vars{'Win32::GetOSVersion'} = join( ', ', Win32::GetOSVersion() );
462 0         0 $special_vars{'Win32::FsType'} = Win32::FsType();
463 0         0 $special_vars{'Win32::IsAdminUser'} = Win32::IsAdminUser();
464             }
465 2         16 return \%special_vars;
466             }
467              
468             sub _get_prereqs {
469 0     0   0 my ($self, $dir) = @_;
470 0         0 my $meta;
471              
472 0         0 foreach my $meta_file ( qw( META.json META.yml META.yaml ) ) {
473 0         0 my $meta_path = File::Spec->catfile( $dir, $meta_file );
474 0 0       0 if (-e $meta_path) {
475 0         0 $meta = eval { Parse::CPAN::Meta->load_file( $dir ) };
  0         0  
476 0 0       0 last if $meta;
477             }
478             }
479              
480 0 0 0     0 if ($meta and $meta->{'meta-spec'}{version} < 2) {
481 0         0 $self->{_meta}{prereqs} = $meta->{prereqs};
482             }
483 0         0 return;
484             }
485              
486             sub _max_length {
487 6     6   25 my ($first, @rest) = @_;
488 6         9 my $max = length $first;
489 6         15 for my $term ( @rest ) {
490 82 100       178 $max = length $term if length $term > $max;
491             }
492 6         20 return $max;
493             }
494              
495             #--------------------------------------------------------------------------#
496             # _temp_filename -- stand-in for File::Temp for backwards compatibility
497             #
498             # takes an optional prefix, adds 8 random chars and returns
499             # an absolute pathname
500             #
501             # NOTE -- manual unlink required
502             #--------------------------------------------------------------------------#
503              
504             sub _temp_filename {
505 6     6   12 my ($prefix) = @_;
506             # @CHARS from File::Temp
507 6         97 my @CHARS = (qw/ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
508             a b c d e f g h i j k l m n o p q r s t u v w x y z
509             0 1 2 3 4 5 6 7 8 9 _
510             /);
511              
512 6 50       17 $prefix = q{} unless defined $prefix;
513 6         114 $prefix .= $CHARS[ int( rand(@CHARS) ) ] for 0 .. 7;
514 6         228 return File::Spec->catfile(File::Spec->tmpdir(), $prefix);
515             }
516              
517              
518             #--------------------------------------------------------------------------#
519             # _version_finder
520             #
521             # module => version pairs
522             #
523             # This is done via an external program to show installed versions exactly
524             # the way they would be found when test programs are run. This means that
525             # any updates to PERL5LIB will be reflected in the results.
526             #
527             # File-finding logic taken from CPAN::Module::inst_file(). Logic to
528             # handle newer Module::Build prereq syntax is taken from
529             # CPAN::Distribution::unsat_prereq()
530             #
531             #--------------------------------------------------------------------------#
532             my $version_finder = $INC{'CPAN/Testers/Common/Client/PrereqCheck.pm'};
533              
534             sub _version_finder {
535 6     6   51 my %prereqs = @_;
536              
537 6         75 my $perl = Probe::Perl->find_perl_interpreter();
538 6         133 my @prereq_results;
539              
540 6         20 my $prereq_input = _temp_filename( 'CTCC-' );
541 6 50       780 open my $fh, '>', $prereq_input
542             or die "Could not create temporary '$prereq_input' for prereq analysis: $!";
543 6         10 print {$fh} map { "$_ $prereqs{$_}\n" } keys %prereqs;
  6         26  
  51         132  
544 6         215 close $fh;
545              
546 6     6   324 my ( $prereq_result, $error, $exit ) = capture { system( $perl, $version_finder, $prereq_input ) };
  6         1786358  
547 6         5525 unlink $prereq_input;
548              
549 6 50       42 if ( length $error ) {
550 0         0 print STDERR $error;
551             }
552 6 50       25 if ( not length $prereq_result) {
553 0         0 warn "Got no output from CPAN::Testers::Common::Client::PrereqCheck";
554             }
555 6         15 my %result;
556 6         50 for my $line ( split "\n", $prereq_result ) {
557 51 50       94 next unless length $line;
558 51         130 my ($mod, $met, $have) = split " ", $line;
559 51 50 33     275 unless ( defined($mod) && defined($met) && defined($have) ) {
      33        
560 0         0 warn "Error parsing output from CPAN::Testers::Common::Client::PrereqCheck:\n$line";
561 0         0 next;
562             }
563 51         159 $result{$mod}{have} = $have;
564 51         90 $result{$mod}{met} = $met;
565             }
566 6         93 return \%result;
567             }
568              
569              
570             sub _create_email {
571 2     2   3 my $self = shift;
572              
573 2         19 my %intro_para = (
574             'pass' => <<'HERE',
575             Thank you for uploading your work to CPAN. Congratulations!
576             All tests were successful.
577             HERE
578              
579             'fail' => <<'HERE',
580             Thank you for uploading your work to CPAN. However, there was a problem
581             testing your distribution.
582              
583             If you think this report is invalid, please consult the CPAN Testers Wiki
584             for suggestions on how to avoid getting FAIL reports for missing library
585             or binary dependencies, unsupported operating systems, and so on:
586              
587             http://wiki.cpantesters.org/wiki/CPANAuthorNotes
588             HERE
589              
590             'unknown' => <<'HERE',
591             Thank you for uploading your work to CPAN. However, attempting to
592             test your distribution gave an inconclusive result.
593              
594             This could be because your distribution had an error during the make/build
595             stage, did not define tests, tests could not be found, because your tests were
596             interrupted before they finished, or because the results of the tests could not
597             be parsed. You may wish to consult the CPAN Testers Wiki:
598              
599             http://wiki.cpantesters.org/wiki/CPANAuthorNotes
600             HERE
601              
602             'na' => <<'HERE',
603             Thank you for uploading your work to CPAN. While attempting to build or test
604             this distribution, the distribution signaled that support is not available
605             either for this operating system or this version of Perl. Nevertheless, any
606             diagnostic output produced is provided below for reference. If this is not
607             what you expect, you may wish to consult the CPAN Testers Wiki:
608              
609             http://wiki.cpantesters.org/wiki/CPANAuthorNotes
610             HERE
611              
612             );
613              
614 2         7 my $metabase_data = $self->metabase_data;
615             my %data = (
616             author => $self->author,
617             dist_name => $self->distname,
618             perl_version => $metabase_data->{TestSummary}{perl_version},
619             via => $self->via,
620             grade => $self->grade,
621             comment => $self->comments,
622             command => $self->command,
623             test_log => $metabase_data->{TestOutput}{test} || '',
624             prereq_pm => _format_prereq_report( $metabase_data->{Prereqs} ),
625             env_vars => _format_vars_report( $metabase_data->{TestEnvironment}{environment_vars} ),
626             special_vars => _format_vars_report( $metabase_data->{TestEnvironment}{special_vars} ),
627 2   100     10 toolchain_versions => _format_toolchain_report( $metabase_data->{InstalledModules}{toolchain} ),
628             );
629              
630 2 50       9 if ( length $data{test_log} > MAX_OUTPUT_LENGTH ) {
631 0         0 my $max_k = int(MAX_OUTPUT_LENGTH/1000) . "K";
632 0         0 $data{test_log} = substr( $data{test_log}, 0, MAX_OUTPUT_LENGTH)
633             . "\n\n[Output truncated after $max_k]\n\n";
634             }
635              
636 2         45 return <<"EOEMAIL";
637             Dear $data{author},
638              
639             This is a computer-generated report for $data{dist_name}
640             on perl $data{perl_version}, created by $data{via}.
641              
642             $intro_para{ $data{grade} }
643             Sections of this report:
644              
645             * Tester comments
646             * Program output
647             * Prerequisites
648             * Environment and other context
649              
650             ------------------------------
651             TESTER COMMENTS
652             ------------------------------
653              
654             Additional comments from tester:
655              
656             $data{comment}
657              
658             ------------------------------
659             PROGRAM OUTPUT
660             ------------------------------
661              
662             Output from '$data{command}':
663              
664             $data{test_log}
665             ------------------------------
666             PREREQUISITES
667             ------------------------------
668              
669             Prerequisite modules loaded:
670              
671             $data{prereq_pm}
672             ------------------------------
673             ENVIRONMENT AND OTHER CONTEXT
674             ------------------------------
675              
676             Environment variables:
677              
678             $data{env_vars}
679             Perl special variables (and OS-specific diagnostics, for MSWin32):
680              
681             $data{special_vars}
682             Perl module toolchain versions installed:
683              
684             $data{toolchain_versions}
685             EOEMAIL
686              
687             }
688              
689             42;
690             __END__