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   43024 use warnings;
  4         7  
  4         113  
4 4     4   14 use strict;
  4         3  
  4         66  
5              
6 4     4   1861 use Devel::Platform::Info;
  4         1299  
  4         92  
7 4     4   1561 use Probe::Perl;
  4         4602  
  4         89  
8 4     4   1790 use Config::Perl::V;
  4         6495  
  4         160  
9 4     4   49 use Carp ();
  4         4  
  4         56  
10 4     4   13 use File::Spec;
  4         3  
  4         66  
11 4     4   1501 use Capture::Tiny qw(capture);
  4         65411  
  4         207  
12 4     4   1613 use CPAN::Testers::Common::Client::PrereqCheck;
  4         10  
  4         125  
13 4     4   1374 use CPAN::Testers::Common::Client::History;
  4         13  
  4         122  
14              
15 4     4   19 use constant MAX_OUTPUT_LENGTH => 1_000_000;
  4         4  
  4         9454  
16              
17             our $VERSION = '0.13';
18              
19              
20             #==================================
21             # CONSTRUCTOR
22             #==================================
23              
24             sub new {
25 2     2 1 2231 my ($class, %params) = @_;
26 2         6 my $self = bless {}, $class;
27              
28 2 50       6 Carp::croak q[Please specify a distname] unless $params{distname};
29 2 50       5 Carp::croak q[Please specify the dist's author] unless $params{author};
30 2 50       5 Carp::croak q[Please specify a grade for the dist] unless $params{grade};
31              
32 2         10 $self->_init( %params );
33              
34 2         9 return $self;
35             }
36              
37             sub _init {
38 2     2   6 my ($self, %params) = @_;
39              
40 2         6 $self->grade( $params{grade} );
41 2         5 $self->distname( $params{distname} );
42 2         3 $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       9 ? "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         3 }
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         4 my $key = $output . '_output';
67 6 100       13 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 10 my ($self, $comment) = @_;
82 7 100       16 $self->{_comment} = $comment if $comment;
83 7         35 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         12 return $self->{_via};
90             }
91              
92             sub author {
93 5     5 1 611 my ($self, $author) = @_;
94 5 100       15 $self->{_author} = $author if $author;
95 5         13 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 7 my ($self, $distname) = @_;
104 7 100       18 $self->{_distname} = $distname if $distname;
105              
106 7         19 return $self->{_distname};
107             }
108              
109             sub grade {
110 11     11 1 20 my ($self, $grade) = @_;
111 11 100       36 $self->{_grade} = lc $grade if $grade;
112 11         78 return $self->{_grade};
113             }
114              
115             sub command {
116 4     4 1 8 my ($self, $command) = @_;
117 4 100       11 $self->{_command} = $command if $command;
118 4   100     37 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         3 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         5 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 1 my ($self) = @_;
143              
144 1         2 my $grade = $self->grade;
145 1         3 my $dist_name = $self->distname;
146 1 50 33     5 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         5 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 2 my $self = shift;
159              
160             # some data is repeated between facts, so we keep a 'cache'
161 2         11 $self->{_config} = Config::Perl::V::myconfig();
162 2         1098 $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         170741 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         31 my $populator = '_populate_' . lc $fact;
175 18         67 $self->{_data}{$fact} = $self->$populator;
176             }
177              
178 2         6 return $self->metabase_data;
179             }
180              
181 7     7 1 2469 sub metabase_data { return shift->{_data} }
182              
183             sub email {
184 2     2 1 3 my $self = shift;
185 2   66     3 my $metabase_data = $self->metabase_data || $self->populate;
186              
187 2         14 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         8 return $self->{_platform};
200             }
201              
202              
203             sub _populate_perlconfig {
204 2     2   5 my $self = shift;
205 2         3 return @{ $self->{_config} }{qw(build config)};
  2         12  
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     29 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   56 my $self = shift;
236 2         11 return $self->comments;
237             }
238              
239             sub _populate_installedmodules {
240 2     2   3 my $self = shift;
241              
242 2         15 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         7 my $results = _version_finder( map { $_ => 0 } @toolchain_mods );
  40         46  
266              
267 2         18 my %toolchain = map { $_ => $results->{$_}{have} } @toolchain_mods;
  40         58  
268 2         9 my %prereqs = ();
269              
270 2         35 return { prereqs => \%prereqs, toolchain => \%toolchain };
271             }
272              
273              
274             sub _populate_legacyreport {
275 2     2   4 my $self = shift;
276             return {
277 2         6 %{ $self->_populate_testsummary },
  2         8  
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         25 }
293              
294             sub _populate_testoutput {
295 2     2   5 my $self = shift;
296 2         11 return $self->{_output};
297             }
298              
299              
300             #=====================================================
301             # FORMATTERS -- functions to aid email formatting
302             #=====================================================
303              
304             sub _format_vars_report {
305 4     4   3 my $variables = shift;
306              
307 4         5 my $report = "";
308 4         22 foreach my $var ( sort keys %$variables ) {
309 30         27 my $value = $variables->{$var};
310 30 50       33 $value = '[undef]' if ! defined $value;
311 30         37 $report .= " $var = $value\n";
312             }
313 4         13 return $report;
314             }
315              
316 124 100   124   221 sub _fix_unknown { defined $_[0] ? $_[0] : 'unknown' }
317              
318             sub _format_toolchain_report {
319 3     3   950 my $installed = shift;
320 3         33 my $mod_width = _max_length( keys %$installed );
321             my $ver_width = _max_length(
322 3         11 map { _fix_unknown( $installed->{$_} ) } keys %$installed
  44         40  
323             );
324              
325 3         13 my $format = " \%-${mod_width}s \%-${ver_width}s\n";
326              
327 3         4 my $report = "";
328 3         16 $report .= sprintf( $format, "Module", "Have" );
329 3         14 $report .= sprintf( $format, "-" x $mod_width, "-" x $ver_width );
330              
331 3         21 for my $var ( sort keys %$installed ) {
332             $report .= sprintf(" \%-${mod_width}s \%-${ver_width}s\n",
333 44         62 $var, _fix_unknown($installed->{$var}) );
334             }
335              
336 3         44 return $report;
337             }
338              
339             sub _format_prereq_report {
340 4     4   1470 my $prereqs = shift;
341 4         6 my (%have, %prereq_met, $report);
342              
343 4         13 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         30 my $requires = $prereqs->{$section}{requires};
348 12 100 66     86 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         32 foreach my $mod ( keys %$results ) {
353 11         28 $have{$section}{$mod} = $results->{$mod}{have};
354 11         34 $prereq_met{$section}{$mod} = $results->{$mod}{met};
355             }
356             }
357              
358             # find formatting widths
359 4         14 my ($name_width, $need_width, $have_width) = (6, 4, 4);
360 4         8 foreach my $section ( @prereq_sections ) {
361 12         19 my $requires = $prereqs->{$section}{requires};
362 12 100 66     56 next unless $requires and ref $requires eq 'HASH';
363              
364 8         21 foreach my $module ( keys %$requires ) {
365 18         17 my $name_length = length $module;
366 18         30 my $need_length = length $requires->{$module};
367 18         38 my $have_length = length _fix_unknown( $have{$section}{$module} );
368 18 100       32 $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       33 $have_width = $have_length if $have_length > $have_width;
371             }
372             }
373              
374 4         16 my $format_str =
375             " \%1s \%-${name_width}s \%-${need_width}s \%-${have_width}s\n";
376              
377             # generate the report
378 4         6 foreach my $section ( @prereq_sections ) {
379 12         16 my $requires = $prereqs->{$section}{requires};
380 12 100 66     62 next unless $requires and ref $requires eq 'HASH' and keys %$requires;
      100        
381              
382 5         40 $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         25 foreach my $module ( sort {lc $a cmp lc $b} keys %$requires ) {
  29         36  
391 18         21 my $need = $requires->{$module};
392 18         32 my $have = _fix_unknown( $have{$section}{$module} );
393 18 100       40 my $bad = $prereq_met{$section}{$module} ? " " : "!";
394 18         50 $report .= sprintf( $format_str, $bad, $module, $need, $have);
395             }
396 5         8 $report .= "\n";
397             }
398              
399 4   100     56 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   14 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       68 if ( $var =~ m{^/(.+)/$} ) {
437 8         15 my $re = $1;
438 8         31 foreach my $found ( grep { /$re/ } keys %ENV ) {
  176         234  
439 18 50       46 $env_found{$found} = $ENV{$found} if exists $ENV{$found};
440             }
441             }
442             else {
443 36 100       57 $env_found{$var} = $ENV{$var} if exists $ENV{$var};
444             }
445             }
446              
447 2         10 return \%env_found;
448             }
449              
450             sub _get_special_vars {
451 2     2   42 my %special_vars = (
452             EXECUTABLE_NAME => $^X,
453             UID => $<,
454             EUID => $>,
455             GID => $(,
456             EGID => $),
457             );
458              
459 2 50 33     11 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         10 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   24 my ($first, @rest) = @_;
488 6         9 my $max = length $first;
489 6         15 for my $term ( @rest ) {
490 82 100       163 $max = length $term if length $term > $max;
491             }
492 6         12 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   11 my ($prefix) = @_;
506             # @CHARS from File::Temp
507 6         88 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       20 $prefix = q{} unless defined $prefix;
513 6         99 $prefix .= $CHARS[ int( rand(@CHARS) ) ] for 0 .. 7;
514 6         216 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   63 my %prereqs = @_;
536              
537 6         81 my $perl = Probe::Perl->find_perl_interpreter();
538 6         135 my @prereq_results;
539              
540 6         21 my $prereq_input = _temp_filename( 'CTCC-' );
541 6 50       698 open my $fh, '>', $prereq_input
542             or die "Could not create temporary '$prereq_input' for prereq analysis: $!";
543 6         16 print {$fh} map { "$_ $prereqs{$_}\n" } keys %prereqs;
  6         26  
  51         127  
544 6         231 close $fh;
545              
546 6     6   348 my ( $prereq_result, $error, $exit ) = capture { system( $perl, $version_finder, $prereq_input ) };
  6         1317318  
547 6         4956 unlink $prereq_input;
548              
549 6 50       37 if ( length $error ) {
550 0         0 print STDERR $error;
551             }
552 6 50       24 if ( not length $prereq_result) {
553 0         0 warn "Got no output from CPAN::Testers::Common::Client::PrereqCheck";
554             }
555 6         10 my %result;
556 6         43 for my $line ( split "\n", $prereq_result ) {
557 51 50       74 next unless length $line;
558 51         113 my ($mod, $met, $have) = split " ", $line;
559 51 50 33     246 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         162 $result{$mod}{have} = $have;
564 51         77 $result{$mod}{met} = $met;
565             }
566 6         89 return \%result;
567             }
568              
569              
570             sub _create_email {
571 2     2   5 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     8 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         55 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__