File Coverage

blib/lib/CPAN/Reporter.pm
Criterion Covered Total %
statement 523 610 85.7
branch 226 306 73.8
condition 84 120 70.0
subroutine 59 61 96.7
pod 0 6 0.0
total 892 1103 80.8


line stmt bran cond sub pod time code
1 33     33   138671 use strict;
  33         78  
  33         1706  
2             package CPAN::Reporter;
3              
4             our $VERSION = '1.2019';
5              
6 33     33   192 use Config;
  33         57  
  33         1628  
7 33     33   797 use Capture::Tiny qw/ capture tee_merged /;
  33         6638  
  33         2178  
8 33     33   233 use CPAN 1.94 ();
  33         703  
  33         859  
9             #CPAN.pm was split into separate files in this version
10             #set minimum to it for simplicity
11 33     33   15580 use CPAN::Version ();
  33         43351  
  33         900  
12 33     33   263 use File::Basename qw/basename dirname/;
  33         68  
  33         1958  
13 33     33   235 use File::Find ();
  33         70  
  33         457  
14 33     33   171 use File::HomeDir ();
  33         72  
  33         580  
15 33     33   178 use File::Path qw/mkpath rmtree/;
  33         84  
  33         1749  
16 33     33   213 use File::Spec 3.19 ();
  33         733  
  33         828  
17 33     33   198 use File::Temp 0.16 qw/tempdir/;
  33         859  
  33         1505  
18 33     33   1575 use IO::File ();
  33         3280  
  33         500  
19 33     33   16325 use Parse::CPAN::Meta ();
  33         54372  
  33         751  
20 33     33   1146 use Probe::Perl ();
  33         2362  
  33         628  
21 33     33   1696 use Test::Reporter 1.54 ();
  33         36395  
  33         594  
22 33     33   18481 use CPAN::Reporter::Config ();
  33         124  
  33         925  
23 33     33   17549 use CPAN::Reporter::History ();
  33         177  
  33         849  
24 33     33   16002 use CPAN::Reporter::PrereqCheck ();
  33         126  
  33         872  
25              
26 33     33   244 use constant MAX_OUTPUT_LENGTH => 1_000_000;
  33         71  
  33         2279  
27              
28             #--------------------------------------------------------------------------#
29             # create temp lib dir for Devel::Autoflush
30             # so that PERL5OPT=-MDevel::Autoflush is found by any perl
31             #--------------------------------------------------------------------------#
32              
33 33     33   16169 use Devel::Autoflush 0.04 ();
  33         2310  
  33         273783  
34             # directory fixture
35             my $Autoflush_Lib = tempdir(
36             "CPAN-Reporter-lib-XXXX", TMPDIR => 1, CLEANUP => 1
37             );
38             # copy Devel::Autoflush to directory or clear autoflush_lib variable
39             _file_copy_quiet(
40             $INC{'Devel/Autoflush.pm'},
41             File::Spec->catfile( $Autoflush_Lib, qw/Devel Autoflush.pm/ )
42             ) or undef $Autoflush_Lib;
43              
44             #--------------------------------------------------------------------------#
45             # public API
46             #--------------------------------------------------------------------------#
47              
48             sub configure {
49 2     2 0 12857 goto &CPAN::Reporter::Config::_configure;
50             }
51              
52             sub grade_make {
53 16     16 0 2712 my @args = @_;
54 16 100       264 my $result = _init_result( 'make', @args ) or return;
55 15         198 _compute_make_grade($result);
56 15 100       150 if ( $result->{grade} eq 'discard' ) {
57             $CPAN::Frontend->myprint(
58             "\nCPAN::Reporter: test results were not valid, $result->{grade_msg}.\n\n",
59 4         364 $result->{prereq_pm}, "\n",
60             "Test report will not be sent"
61             );
62 4 100       306 CPAN::Reporter::History::_record_history( $result )
63             if not CPAN::Reporter::History::_is_duplicate( $result );
64             }
65             else {
66 11         192 _print_grade_msg($result->{make_cmd}, $result);
67 11 100       128 if ( $result->{grade} ne 'pass' ) { _dispatch_report( $result ) }
  9         121  
68             }
69 15         1537 return $result->{success};
70             }
71              
72             sub grade_PL {
73 34     34 0 12081 my @args = @_;
74 34 100       645 my $result = _init_result( 'PL', @args ) or return;
75 33         344 _compute_PL_grade($result);
76 33 100       225 if ( $result->{grade} eq 'discard' ) {
77             $CPAN::Frontend->myprint(
78             "\nCPAN::Reporter: test results were not valid, $result->{grade_msg}.\n\n",
79 9         688 $result->{prereq_pm}, "\n",
80             "Test report will not be sent"
81             );
82 9 100       663 CPAN::Reporter::History::_record_history( $result )
83             if not CPAN::Reporter::History::_is_duplicate( $result );
84             }
85             else {
86 24         417 _print_grade_msg($result->{PL_file} , $result);
87 24 100       216 if ( $result->{grade} ne 'pass' ) { _dispatch_report( $result ) }
  18         301  
88             }
89 33         3460 return $result->{success};
90             }
91              
92             sub grade_test {
93 119     119 0 5400 my @args = @_;
94 119 100       2590 my $result = _init_result( 'test', @args ) or return;
95 118         1463 _compute_test_grade($result);
96 118 100       827 if ( $result->{grade} eq 'discard' ) {
97             $CPAN::Frontend->myprint(
98             "\nCPAN::Reporter: test results were not valid, $result->{grade_msg}.\n\n",
99 15         912 $result->{prereq_pm}, "\n",
100             "Test report will not be sent"
101             );
102 15 100       1142 CPAN::Reporter::History::_record_history( $result )
103             if not CPAN::Reporter::History::_is_duplicate( $result );
104             }
105             else {
106 103         1516 _print_grade_msg( "Test", $result );
107 103         1046 _dispatch_report( $result );
108             }
109 118         12658 return $result->{success};
110             }
111              
112             sub record_command {
113 186     186 0 44566638 my ($command, $timeout) = @_;
114              
115             # XXX refactor this!
116             # Get configuration options
117 186 100       4255 if ( -r CPAN::Reporter::Config::_get_config_file() ) {
118 170         2424 my $config_obj = CPAN::Reporter::Config::_open_config_file();
119 170         624 my $config;
120 170 50       2031 $config = CPAN::Reporter::Config::_get_config_options( $config_obj )
121             if $config_obj;
122              
123 170   100     3174 $timeout ||= $config->{command_timeout}; # might still be undef
124             }
125              
126 186         2208 my ($cmd, $redirect) = _split_redirect($command);
127              
128             # Teeing a command loses its exit value so we must wrap the command
129             # and print the exit code so we can read it off of output
130 186         607 my $wrap_code;
131 186 100       869 if ( $timeout ) {
132 15 50       183 $wrap_code = $^O eq 'MSWin32'
133             ? _timeout_wrapper_win32($cmd, $timeout)
134             : _timeout_wrapper($cmd, $timeout);
135             }
136             # if no timeout or timeout wrap code wasn't available
137 186 100       839 if ( ! $wrap_code ) {
138 171         870 my $safecmd = quotemeta($cmd);
139 171         1235 $wrap_code = << "HERE";
140             my \$rc = system("$safecmd");
141             my \$ec = \$rc == -1 ? -1 : \$?;
142             print "($safecmd exited with \$ec)\\n";
143             HERE
144             }
145              
146             # write code to a tempfile for execution
147 186         1373 my $wrapper_name = _temp_filename( 'CPAN-Reporter-CW-' );
148 186 50       5223 my $wrapper_fh = IO::File->new( $wrapper_name, 'w' )
149             or die "Could not create a wrapper for $cmd\: $!";
150              
151 186         57172 $wrapper_fh->print( $wrap_code );
152 186         4729 $wrapper_fh->close;
153              
154             # tee the command wrapper
155 186         16105 my @tee_input = ( Probe::Perl->find_perl_interpreter, $wrapper_name );
156 186 100       6708 push @tee_input, $redirect if defined $redirect;
157 186         483 my $tee_out;
158             {
159             # ensure autoflush if we can
160 186 100       477 local $ENV{PERL5OPT} = _get_perl5opt() if _is_PL($command);
  186         1815  
161 186     186   10353 $tee_out = tee_merged { system( @tee_input ) };
  186         132174946  
162             }
163              
164             # cleanup
165 186 50       650691 unlink $wrapper_name unless $ENV{PERL_CR_NO_CLEANUP};
166              
167 186         34931 my @cmd_output = split qr{(?<=$/)}, $tee_out;
168 186 50       2200 if ( ! @cmd_output ) {
169 0         0 $CPAN::Frontend->mywarn(
170             "CPAN::Reporter: didn't capture command results for '$cmd'\n"
171             );
172 0         0 return;
173             }
174              
175             # extract the exit value
176 186         917 my $exit_value;
177 186 50       4339 if ( $cmd_output[-1] =~ m{exited with} ) {
178 186         4587 ($exit_value) = $cmd_output[-1] =~ m{exited with ([-0-9]+)};
179 186         1246 pop @cmd_output;
180             }
181              
182             # bail out on some errors
183 186 50       2292 if ( ! defined $exit_value ) {
    50          
184 0         0 $CPAN::Frontend->mywarn(
185             "CPAN::Reporter: couldn't determine exit value for '$cmd'\n"
186             );
187 0         0 return;
188             }
189             elsif ( $exit_value == -1 ) {
190 0         0 $CPAN::Frontend->mywarn(
191             "CPAN::Reporter: couldn't execute '$cmd'\n"
192             );
193 0         0 return;
194             }
195              
196 186         8039 return \@cmd_output, $exit_value;
197             }
198              
199             sub test {
200 76     76 0 18081161 my ($dist, $system_command) = @_;
201 76         992 my ($output, $exit_value) = record_command( $system_command );
202 76         973 return grade_test( $dist, $system_command, $output, $exit_value );
203             }
204              
205             #--------------------------------------------------------------------------#
206             # private functions
207             #--------------------------------------------------------------------------#
208              
209             #--------------------------------------------------------------------------#
210             # _compute_make_grade
211             #--------------------------------------------------------------------------#
212              
213             sub _compute_make_grade {
214 15     15   73 my $result = shift;
215 15         77 my ($grade,$msg);
216 15 100       106 if ( $result->{exit_value} ) {
217 13         197 $result->{grade} = "unknown";
218 13         153 $result->{grade_msg} = "Stopped with an error"
219             }
220             else {
221 2         37 $result->{grade} = "pass";
222 2         31 $result->{grade_msg} = "No errors"
223             }
224              
225 15         261 _downgrade_known_causes( $result );
226              
227 15         234 $result->{success} = $result->{grade} eq 'pass';
228 15         129 return;
229             }
230              
231             #--------------------------------------------------------------------------#
232             # _compute_PL_grade
233             #--------------------------------------------------------------------------#
234              
235             sub _compute_PL_grade {
236 33     33   149 my $result = shift;
237 33         122 my ($grade,$msg);
238 33 100       243 if ( $result->{exit_value} ) {
239 23         370 $result->{grade} = "unknown";
240 23         310 $result->{grade_msg} = "Stopped with an error"
241             }
242             else {
243 10         150 $result->{grade} = "pass";
244 10         104 $result->{grade_msg} = "No errors"
245             }
246              
247 33         555 _downgrade_known_causes( $result );
248              
249 33         495 $result->{success} = $result->{grade} eq 'pass';
250 33         286 return;
251             }
252              
253             #--------------------------------------------------------------------------#
254             # _compute_test_grade
255             #
256             # Don't shortcut to unknown unless _has_tests because a custom
257             # Makefile.PL or Build.PL might define tests in a non-standard way
258             #
259             # With test.pl and 'make test', any t/*.t might pass Test::Harness, but
260             # test.pl might still fail, or there might only be test.pl,
261             # so use exit code directly
262             #
263             # Likewise, if we have recursive Makefile.PL, then we don't trust the
264             # reverse-order parsing and should just take the exit code directly
265             #
266             # Otherwise, parse in reverse order for Test::Harness output or a couple
267             # other significant strings and stop after the first match. Going in
268             # reverse and stopping is done to (hopefully) avoid picking up spurious
269             # results from any test output. But then we have to check for
270             # unsupported OS strings in case those were printed but were not fatal.
271             #--------------------------------------------------------------------------#
272              
273             sub _compute_test_grade {
274 118     118   544 my $result = shift;
275 118         517 my ($grade,$msg);
276 118         446 my $output = $result->{output};
277              
278             # In some cases, get a result straight from the exit code
279 118 100 100     2834 if ( $result->{is_make} && ( -f "test.pl" || _has_recursive_make() ) ) {
      100        
280 16 100       228 if ( $result->{exit_value} ) {
281 10         131 $grade = "fail";
282 10         59 $msg = "'make test' error detected";
283             }
284             else {
285 6         28 $grade = "pass";
286 6         43 $msg = "'make test' no errors";
287             }
288             }
289             # Otherwise, get a result from Test::Harness output
290             else {
291             # figure out the right harness parser
292 102         1230 _expand_result( $result );
293 102         1269 my $harness_version = $result->{toolchain}{'Test::Harness'}{have};
294 102 50       4563 my $harness_parser = CPAN::Version->vgt($harness_version, '2.99_01')
295             ? \&_parse_tap_harness
296             : \&_parse_test_harness;
297             # parse lines in reverse
298 102         16667 for my $i ( reverse 0 .. $#{$output} ) {
  102         1376  
299 308 100       4370 if ( $output->[$i] =~ m{No support for OS|OS unsupported}ims ) { # from any *.t file
    100          
300 6         61 $grade = 'na';
301 6         35 $msg = 'This platform is not supported';
302             }
303             elsif ( $output->[$i] =~ m{^.?No tests defined}ms ) { # from M::B
304 8         67 $grade = 'unknown';
305 8         51 $msg = 'No tests provided';
306             }
307             else {
308 294         1189 ($grade, $msg) = $harness_parser->( $output->[$i] );
309             }
310 308 100       1210 last if $grade;
311             }
312             # fallback on exit value if no recognizable Test::Harness output
313 102 100       884 if ( ! $grade ) {
314 12 100       235 $grade = $result->{exit_value} ? "fail" : "pass";
315             $msg = ( $result->{is_make} ? "'make test' " : "'Build test' " )
316 12 100       209 . ( $result->{exit_value} ? "error detected" : "no errors");
    100          
317             }
318             }
319              
320 118         1404 $result->{grade} = $grade;
321 118         1222 $result->{grade_msg} = $msg;
322              
323 118         1658 _downgrade_known_causes( $result );
324              
325             $result->{success} = $result->{grade} eq 'pass'
326 118   100     1728 || $result->{grade} eq 'unknown';
327 118         816 return;
328             }
329              
330             #--------------------------------------------------------------------------#
331             # _dispatch_report
332             #
333             # Set up Test::Reporter and prompt user for edit, send
334             #--------------------------------------------------------------------------#
335              
336             sub _dispatch_report {
337 131     131   10686 my $result = shift;
338 131         903 my $phase = $result->{phase};
339              
340 131         1512 $CPAN::Frontend->myprint(
341             "CPAN::Reporter: preparing a CPAN Testers report for $result->{dist_name}\n"
342             );
343              
344             # Get configuration options
345 131         4882 my $config_obj = CPAN::Reporter::Config::_open_config_file();
346 131         501 my $config;
347 131 100       1439 $config = CPAN::Reporter::Config::_get_config_options( $config_obj )
348             if $config_obj;
349 131 100       994 if ( ! $config->{email_from} ) {
350 5         27 $CPAN::Frontend->mywarn( << "EMAIL_REQUIRED");
351              
352             CPAN::Reporter: required 'email_from' option missing an email address, so
353             test report will not be sent. See documentation for configuration details.
354              
355             Even though CPAN Testers no longer uses email, this email address will
356             show up in the report and help identify the tester. This is required
357             for compatibility with tools that process legacy reports for analysis.
358              
359             EMAIL_REQUIRED
360 5         96 return;
361             }
362              
363             # Need to know if this is a duplicate
364 126         1048 my $is_duplicate = CPAN::Reporter::History::_is_duplicate( $result );
365              
366             # Abort if the distribution name is not formatted according to
367             # CPAN Testers requirements: Dist-Name-version.suffix
368             # Regex from CPAN-Testers should extract name, separator, version
369             # and extension
370             my @format_checks = $result->{dist_basename} =~
371 126         3659 m{(.+)([\-\_])(v?\d.*)(\.(?:tar\.(?:gz|bz2)|tgz|zip))$}i;
372             ;
373 126 100       621 if ( ! grep { length } @format_checks ) {
  492         1435  
374 3         68 $CPAN::Frontend->mywarn( << "END_BAD_DISTNAME");
375              
376             CPAN::Reporter: the distribution name '$result->{dist_basename}' does not
377             appear to be packaged according to CPAN tester guidelines. Perhaps it is
378             not a normal CPAN distribution.
379              
380             Test report will not be sent.
381              
382             END_BAD_DISTNAME
383              
384             # record this as a discard, instead
385 3         117 $result->{grade} = 'discard';
386 3 50       70 CPAN::Reporter::History::_record_history( $result )
387             if not $is_duplicate;
388 3         29 return;
389             }
390              
391             # Gather 'expensive' data for the report
392 123         671 _expand_result( $result);
393              
394             # Skip if distribution name matches the send_skipfile
395 123 100 66     895 if ( $config->{send_skipfile} && -r $config->{send_skipfile} ) {
396 4         68 my $send_skipfile = IO::File->new( $config->{send_skipfile}, "r" );
397 4         766 my $dist_id = $result->{dist}->pretty_id;
398 4         168 while ( my $pattern = <$send_skipfile> ) {
399 11         42 chomp($pattern);
400             # ignore comments
401 11 100       48 next if substr($pattern,0,1) eq '#';
402             # if it doesn't match, continue with next pattern
403 7 100       188 next if $dist_id !~ /$pattern/i;
404             # if it matches, warn and return
405 3         51 $CPAN::Frontend->myprint( << "END_SKIP_DIST" );
406             CPAN::Reporter: '$dist_id' matched against the send_skipfile.
407              
408             Test report will not be sent.
409              
410             END_SKIP_DIST
411              
412 3         154 return;
413             }
414             }
415              
416             # Setup the test report
417 120         3862 my $tr = Test::Reporter->new;
418 120         4848 $tr->grade( $result->{grade} );
419 120         2452 $tr->distribution( $result->{dist_name} );
420             # Older Test::Reporter doesn't support distfile, but we need it for
421             # Metabase transport
422             $tr->distfile( $result->{dist}->pretty_id )
423 120 50       3946 if $Test::Reporter::VERSION >= 1.54;
424              
425             # Skip if duplicate and not sending duplicates
426 120 100       3066 if ( $is_duplicate ) {
427 74 100       515 if ( _prompt( $config, "send_duplicates", $tr->grade) =~ /^n/ ) {
428 2         11 $CPAN::Frontend->myprint(<< "DUPLICATE_REPORT");
429              
430             CPAN::Reporter: this appears to be a duplicate report for the $phase phase:
431 2         15 @{[$tr->subject]}
432              
433             Test report will not be sent.
434              
435             DUPLICATE_REPORT
436              
437 2         149 return;
438             }
439             }
440              
441             # Set debug and transport options, if supported
442 118 50       610 $tr->debug( $config->{debug} ) if defined $config->{debug};
443 118         726 my $transport = $config->{transport};
444 118 50 33     1646 unless ( defined $transport && length $transport ) {
445 0         0 $CPAN::Frontend->mywarn( << "TRANSPORT_REQUIRED");
446              
447             CPAN::Reporter: required 'transport' option missing so the test report
448             will not be sent. See documentation for configuration details.
449              
450             TRANSPORT_REQUIRED
451 0         0 return;
452             }
453 118         1023 my @transport_args = split " ", $transport;
454              
455             # special hack for Metabase arguments
456 118 100       602 if ($transport_args[0] eq 'Metabase') {
457 116         612 @transport_args = _validate_metabase_args(@transport_args);
458 116 50       668 unless (@transport_args) {
459 0         0 $CPAN::Frontend->mywarn( "Test report will not be sent.\n\n" );
460 0         0 return;
461             }
462             }
463              
464 118         312 eval { $tr->transport( @transport_args ) };
  118         919  
465 118 100       3883 if ($@) {
466 1         23 $CPAN::Frontend->mywarn(
467             "CPAN::Reporter: problem with Test::Reporter transport: \n" .
468             "$@\n" .
469             "Test report will not be sent\n"
470             );
471 1         55 return;
472             }
473              
474             # prepare mail transport
475 117         1098 $tr->from( $config->{email_from} );
476              
477             # Populate the test report
478 117         1909 $tr->comments( _report_text( $result ) );
479 117         2575 $tr->via( 'CPAN::Reporter ' . $CPAN::Reporter::VERSION );
480              
481             # prompt for editing report
482 117 50       1389 if ( _prompt( $config, "edit_report", $tr->grade ) =~ /^y/ ) {
483 0         0 my $editor = $config->{editor};
484 0 0       0 local $ENV{VISUAL} = $editor if $editor; ## no critic
485 0         0 $tr->edit_comments;
486             }
487              
488             # send_*_report can override send_report
489 117 100       1370 my $send_config = defined $config->{"send_$phase\_report"}
490             ? "send_$phase\_report"
491             : "send_report" ;
492 117 100       1103 if ( _prompt( $config, $send_config, $tr->grade ) =~ /^y/ ) {
493 114         845 $CPAN::Frontend->myprint( "CPAN::Reporter: sending test report with '" . $tr->grade .
494             "' via " . $transport_args[0] . "\n");
495 114 50       5381 if ( $tr->send() ) {
496 114 100       1726 CPAN::Reporter::History::_record_history( $result )
497             if not $is_duplicate;
498             }
499             else {
500 0         0 $CPAN::Frontend->mywarn( "CPAN::Reporter: " . $tr->errstr . "\n");
501              
502 0 0       0 if ( $config->{retry_submission} ) {
503 0         0 sleep(3);
504              
505 0         0 $CPAN::Frontend->mywarn( "CPAN::Reporter: second attempt\n");
506 0         0 $tr->errstr('');
507              
508 0 0       0 if ( $tr->send() ) {
509 0 0       0 CPAN::Reporter::History::_record_history( $result )
510             if not $is_duplicate;
511             }
512             else {
513 0         0 $CPAN::Frontend->mywarn( "CPAN::Reporter: " . $tr->errstr . "\n");
514             }
515             }
516              
517             }
518             }
519             else {
520 3         20 $CPAN::Frontend->myprint("CPAN::Reporter: test report will not be sent\n");
521             }
522              
523 117         1827 return;
524             }
525              
526             sub _report_timeout {
527 129     129   697 my $result = shift;
528 129 100       1025 if ($result->{exit_value} == 9) {
529 2         38 my $config_obj = CPAN::Reporter::Config::_open_config_file();
530 2         15 my $config;
531 2 50       43 $config = CPAN::Reporter::Config::_get_config_options( $config_obj )
532             if $config_obj;
533              
534 2 50       26 if ($config->{'_store_problems_in_dir'}) {
535 0         0 my $distribution = $result->{dist}->base_id;
536 0         0 my $file = "e9.$distribution.${\(time)}.$$.log";
  0         0  
537 0 0       0 if (open my $to_log_fh, '>>', $config->{'_store_problems_in_dir'}.'/'.$file) {
538 0         0 print $to_log_fh $distribution,"\n";
539 0         0 print $to_log_fh "stage: ",$result->{phase},"\n";
540 0         0 print $to_log_fh $Config{archname},"\n";
541 0         0 print $to_log_fh _report_text( $result );
542             } else {
543             $CPAN::Frontend->mywarn( "CPAN::Reporter: writing ".
544 0         0 $config->{'_store_problems_in_dir'}.'/'.$file. " failed\n");
545             }
546             }
547 2 50       35 if ($config->{'_problem_log'}) {
548 0         0 my $distribution = $result->{dist}->base_id;
549 0 0       0 if (open my $to_log_fh, '>>', $config->{'_problem_log'}) {
550 0         0 print $to_log_fh "$result->{phase} $distribution $Config{archname}\n";
551             } else {
552             $CPAN::Frontend->mywarn( "CPAN::Reporter: writing ".
553 0         0 $config->{'_store_problems_in_dir'}. " failed\n");
554             }
555             }
556             }
557             }
558              
559             #--------------------------------------------------------------------------#
560             # _downgrade_known_causes
561             # Downgrade failure/unknown grade if we can determine a cause
562             # If platform not supported => 'na'
563             # If perl version is too low => 'na'
564             # If stated prereqs missing => 'discard'
565             #--------------------------------------------------------------------------#
566              
567             sub _downgrade_known_causes {
568 169     169   1025 my ($result) = @_;
569 169         1116 my ($grade, $output) = ( $result->{grade}, $result->{output} );
570 169   50     1118 my $msg = $result->{grade_msg} || q{};
571              
572             # shortcut unless fail/unknown; but PL might look like pass but actually
573             # have "OS Unsupported" messages if someone printed message and then
574             # did "exit 0"
575 169 100       1013 return if $grade eq 'na';
576 163 100 100     1746 return if $grade eq 'pass' && $result->{phase} ne 'PL';
577              
578             # get prereqs
579 129         1085 _expand_result( $result );
580              
581 129         2375 _report_timeout( $result );
582              
583             # if process was halted with a signal, just set for discard and return
584 129 100       1033 if ( $result->{exit_value} & 127 ) {
585 2         23 $result->{grade} = 'discard';
586 2         22 $result->{grade_msg} = 'Command interrupted';
587 2         14 return;
588             }
589              
590             # look for perl version error messages from various programs
591             # "Error evaling..." type errors happen on Perl < 5.006 when modules
592             # define their version with "our $VERSION = ..."
593 127         515 my ($harness_error, $version_error, $unsupported) ;
594 127         1366 for my $line ( @$output ) {
595 3245 100 100     11383 if ( $result->{phase} eq 'test'
596             && $line =~ m{open3: IO::Pipe: Can't spawn.*?TAP/Parser/Iterator/Process.pm}
597             ) {
598 2         10 $harness_error++;
599 2         15 last;
600             }
601 3243 50 66     29373 if( $line =~ /(?
      100        
      66        
      66        
      33        
602             #?
603             $line =~ /Perl version .*? or higher required\. We run/ims || #EU::MM
604             $line =~ /ERROR: perl: Version .*? is installed, but we need version/ims ||
605             $line =~ /ERROR: perl \(.*?\) is installed, but we need version/ims ||
606             $line =~ /Error evaling version line 'BEGIN/ims ||
607             $line =~ /Could not eval '/ims
608             ) {
609 8         80 $version_error++;
610 8         59 last;
611             }
612 3235 100       15160 if ( $line =~ /No support for OS|OS unsupported/ims ) {
613 6         92 $unsupported++;
614 6         57 last;
615             }
616             }
617              
618             # if the test harness had an error, discard the report
619 127 100 100     7586 if ( $harness_error ) {
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
      100        
620 2         18 $grade = 'discard';
621 2         29 $msg = 'Test harness failure';
622             }
623             # check for explicit version error or just a perl version prerequisite
624             elsif ( $version_error || $result->{prereq_pm} =~ m{^\s+!\s+perl\s}ims ) {
625 12         195 $grade = 'na';
626 12         108 $msg = 'Perl version too low';
627             }
628             # check again for unsupported OS in case we took 'fail' from exit value
629             elsif ( $unsupported ) {
630 6         91 $grade = 'na';
631 6         58 $msg = 'This platform is not supported';
632             }
633             # check for Makefile without 'test' target; there are lots
634             # of variations on the error message, e.g. "target test", "target 'test'",
635             # "'test'", "`test'" and so on.
636             elsif (
637             $result->{is_make} && $result->{phase} eq 'test' && ! _has_test_target()
638             ) {
639 1         14 $grade = 'unknown';
640 1         11 $msg = 'No make test target';
641             }
642             # check the prereq report for missing or failure flag '!'
643             elsif ( $grade ne 'pass' && $result->{prereq_pm} =~ m{n/a}ims ) {
644 14         150 $grade = 'discard';
645 14         145 $msg = "Prerequisite missing:\n$result->{prereq_pm}";
646             }
647             elsif ( $grade ne 'pass' && $result->{prereq_pm} =~ m{^\s+!}ims ) {
648 8         160 $grade = 'discard';
649 8         114 $msg = "Prerequisite version too low:\n$result->{prereq_pm}";
650             }
651             # in PL stage -- if pass but no Makefile or Build, then this should
652             # be recorded as a discard
653             elsif ( $result->{phase} eq 'PL' && $grade eq 'pass'
654             && ! -f 'Makefile' && ! -f 'Build'
655             ) {
656 2         46 $grade = 'discard';
657 2         18 $msg = 'No Makefile or Build file found';
658             }
659             elsif ( $result->{command} =~ /Build.*?-j/ ) {
660 2         20 $grade = 'discard';
661 2         18 $msg = '-j is not a valid option for Module::Build (upgrade your CPAN.pm)';
662             }
663             elsif (
664             $result->{is_make} && $result->{phase} eq 'make' &&
665 45         277 grep { /Makefile out-of-date with respect to Makefile.PL/ } @$output
666             ) {
667 1         11 $grade = 'discard';
668 1         10 $msg = 'Makefile out-of-date';
669             }
670              
671             # store results
672 127         2948 $result->{grade} = $grade;
673 127         625 $result->{grade_msg} = $msg;
674              
675 127         643 return;
676             }
677              
678             #--------------------------------------------------------------------------#
679             # _expand_result - add expensive information like prerequisites and
680             # toolchain that should only be generated if a report will actually
681             # be sent
682             #--------------------------------------------------------------------------#
683              
684             sub _expand_result {
685 354     354   1579 my $result = shift;
686 354 100       3619 return if $result->{expanded}++; # only do this once
687 167         2064 $result->{prereq_pm} = _prereq_report( $result->{dist} );
688             {
689             # mirror PERL5OPT as in record_command
690 167 100       801 local $ENV{PERL5OPT} = _get_perl5opt() if _is_PL($result->{command});
  167         2103  
691 167         1352 $result->{env_vars} = _env_report();
692             }
693 167         2012 $result->{special_vars} = _special_vars_report();
694 167         1356 $result->{toolchain_versions} = _toolchain_report( $result );
695 167         4147 $result->{perl_version} = CPAN::Reporter::History::_format_perl_version();
696 167         2037 return;
697             }
698              
699             #--------------------------------------------------------------------------#
700             # _env_report
701             #--------------------------------------------------------------------------#
702              
703             # Entries bracketed with "/" are taken to be a regex; otherwise literal
704             my @env_vars= qw(
705             /HARNESS/
706             /LC_/
707             /PERL/
708             /_TEST/
709             CCFLAGS
710             COMSPEC
711             INCLUDE
712             INSTALL_BASE
713             LANG
714             LANGUAGE
715             LD_LIBRARY_PATH
716             LDFLAGS
717             LIB
718             NON_INTERACTIVE
719             NUMBER_OF_PROCESSORS
720             PATH
721             PREFIX
722             PROCESSOR_IDENTIFIER
723             SHELL
724             TERM
725             TEMP
726             TMPDIR
727             );
728              
729             sub _env_report {
730 176     176   27979 my @vars_found;
731 176         2385 for my $var ( @env_vars ) {
732 3872 100       16400 if ( $var =~ m{^/(.+)/$} ) {
733 704         10395 push @vars_found, grep { /$1/ } keys %ENV;
  24772         81040  
734             }
735             else {
736 3168 100       11164 push @vars_found, $var if exists $ENV{$var};
737             }
738             }
739              
740 176         1136 my $report = "";
741 176         2096 for my $var ( sort @vars_found ) {
742 2497         6117 my $value = $ENV{$var};
743 2497 50       5073 $value = '[undef]' if ! defined $value;
744 2497         7304 $report .= " $var = $value\n";
745             }
746 176         1649 return $report;
747             }
748              
749             #--------------------------------------------------------------------------#
750             # _file_copy_quiet
751             #
752             # manual file copy -- quietly return undef on failure
753             #--------------------------------------------------------------------------#
754              
755             sub _file_copy_quiet {
756 33     33   118 my ($source, $target) = @_;
757             # ensure we have a target directory
758 33 50       9913 mkpath( dirname($target) ) or return;
759             # read source
760 33         242 local *FH;
761 33 50       1395 open FH, "<$source" or return; ## no critic
762 33         120 my $pm_guts = do { local $/; };
  33         223  
  33         1386  
763 33         483 close FH;
764             # write target
765 33 50       2448 open FH, ">$target" or return; ## no critic
766 33         351 print FH $pm_guts;
767 33         1695 close FH;
768 33         285 return 1;
769             }
770              
771             #--------------------------------------------------------------------------#
772             # _get_perl5opt
773             #--------------------------------------------------------------------------#
774              
775             sub _get_perl5opt {
776 76   50 76   1077 my $perl5opt = $ENV{PERL5OPT} || q{};
777 76 50       379 if ( $Autoflush_Lib ) {
778 76 50       306 $perl5opt .= q{ } if length $perl5opt;
779 76 50       607 $perl5opt .= "-I$Autoflush_Lib " if $] >= 5.008;
780 76         242 $perl5opt .= "-MDevel::Autoflush";
781             }
782 76         1374 return $perl5opt;
783             }
784              
785             #--------------------------------------------------------------------------#
786             # _has_recursive_make
787             #
788             # Ignore Makefile.PL in t directories
789             #--------------------------------------------------------------------------#
790              
791             sub _has_recursive_make {
792 67     67   333 my $PL_count = 0;
793             File::Find::find(
794             sub {
795 2373 100   2373   135121 if ( $_ eq 't' ) {
    100          
796 63         708 $File::Find::prune = 1;
797             }
798             elsif ( $_ eq 'Makefile.PL' ) {
799 73         596 $PL_count++;
800             }
801             },
802 67         17322 File::Spec->curdir()
803             );
804 67         1028 return $PL_count > 1;
805             }
806              
807             #--------------------------------------------------------------------------#
808             # _has_test_target
809             #--------------------------------------------------------------------------#
810              
811             sub _has_test_target {
812 47 50   47   1609 my $fh = IO::File->new("Makefile") or return;
813 47         37527 return scalar grep { /^test[ ]*:/ } <$fh>;
  41836         74691  
814             }
815              
816             #--------------------------------------------------------------------------#
817             # _init_result -- create and return a hash of values for use in
818             # report evaluation and dispatch
819             #
820             # takes same argument format as grade_*()
821             #--------------------------------------------------------------------------#
822              
823             sub _init_result {
824 173     173   14139 my ($phase, $dist, $system_command, $output, $exit_value) = @_;
825              
826 173 50 33     3426 unless ( defined $output && defined $exit_value ) {
827 0         0 my $missing;
828 0 0 0     0 if ( ! defined $output && ! defined $exit_value ) {
    0 0        
829 0         0 $missing = "exit value and output"
830             }
831             elsif ( defined $output && !defined $exit_value ) {
832 0         0 $missing = "exit value"
833             }
834             else {
835 0         0 $missing = "output";
836             }
837 0         0 $CPAN::Frontend->mywarn(
838             "CPAN::Reporter: had errors capturing $missing. Tests abandoned"
839             );
840 0         0 return;
841             }
842              
843 173 100       4421 if ( $dist->pretty_id =~ m{\w+/Perl6/} ) {
844 3         63 $CPAN::Frontend->mywarn(
845             "CPAN::Reporter: Won't report a Perl6 distribution."
846             );
847 3         165 return;
848             }
849              
850 170 100       5956 my $result = {
851             phase => $phase,
852             dist => $dist,
853             command => $system_command,
854             is_make => _is_make( $system_command ),
855             output => ref $output eq 'ARRAY' ? $output : [ split /\n/, $output ],
856             exit_value => $exit_value,
857             # Note: pretty_id is like "DAGOLDEN/CPAN-Reporter-0.40.tar.gz"
858             dist_basename => basename($dist->pretty_id),
859             dist_name => $dist->base_id,
860             };
861              
862             # Used in messages to user
863 170 100       53296 $result->{PL_file} = $result->{is_make} ? "Makefile.PL" : "Build.PL";
864 170 100       8510 $result->{make_cmd} = $result->{is_make} ? $Config{make} : "Build";
865              
866             # CPAN might fail to find an author object for some strange dists
867 170         2080 my $author = $dist->author;
868 170 50       2396 $result->{author} = defined $author ? $author->fullname : "Author";
869 170 50       4258 $result->{author_id} = defined $author ? $author->id : "" ;
870              
871 170         3691 return $result;
872             }
873              
874             #--------------------------------------------------------------------------#
875             # _is_make
876             #--------------------------------------------------------------------------#
877              
878             sub _is_make {
879 190     190   14837 my $command = shift;
880 190 100       5114 return $command =~ m{\b(?:\S*make|Makefile.PL)\b}ims ? 1 : 0;
881             }
882              
883             #--------------------------------------------------------------------------#
884             # _is_PL
885             #--------------------------------------------------------------------------#
886              
887             sub _is_PL {
888 353     353   2112 my $command = shift;
889 353 100       8154 return $command =~ m{\b(?:Makefile|Build)\.PL\b}ims ? 1 : 0;
890             }
891              
892             #--------------------------------------------------------------------------#
893             # _max_length
894             #--------------------------------------------------------------------------#
895              
896             sub _max_length {
897 352     352   3252 my ($first, @rest) = @_;
898 352         1545 my $max = length $first;
899 352         1776 for my $term ( @rest ) {
900 7040 100       15428 $max = length $term if length $term > $max;
901             }
902 352         2666 return $max;
903             }
904              
905              
906             #--------------------------------------------------------------------------#
907             # _parse_tap_harness
908             #
909             # As of Test::Harness 2.99_02, the final line is provided by TAP::Harness
910             # as "Result: STATUS" where STATUS is "PASS", "FAIL" or "NOTESTS"
911             #--------------------------------------------------------------------------#
912              
913              
914             sub _parse_tap_harness {
915 294     294   1045 my ($line) = @_;
916 294 100       3134 if ( $line =~ m{^Result:\s+([A-Z]+)} ) {
    100          
917 74 100       971 if ( $1 eq 'PASS' ) {
    100          
    50          
918 20         181 return ('pass', 'All tests successful');
919             }
920             elsif ( $1 eq 'FAIL' ) {
921 51         636 return ('fail', 'One or more tests failed');
922             }
923             elsif ( $1 eq 'NOTESTS' ) {
924 3         64 return ('unknown', 'No tests were run');
925             }
926             }
927             elsif ( $line =~ m{Bailout called\.\s+Further testing stopped}ms ) {
928 2         18 return ( 'fail', 'Bailed out of tests');
929             }
930 218         757 return;
931             }
932              
933             #--------------------------------------------------------------------------#
934             # _parse_test_harness
935             #
936             # Output strings taken from Test::Harness::
937             # _show_results() -- for versions < 2.57_03
938             # get_results() -- for versions >= 2.57_03
939             #--------------------------------------------------------------------------#
940              
941             sub _parse_test_harness {
942 0     0   0 my ($line) = @_;
943 0 0       0 if ( $line =~ m{^All tests successful}ms ) {
    0          
    0          
    0          
    0          
944 0         0 return ( 'pass', 'All tests successful' );
945             }
946             elsif ( $line =~ m{^FAILED--no tests were run}ms ) {
947 0         0 return ( 'unknown', 'No tests were run' );
948             }
949             elsif ( $line =~ m{^FAILED--.*--no output}ms ) {
950 0         0 return ( 'unknown', 'No tests were run');
951             }
952             elsif ( $line =~ m{FAILED--Further testing stopped}ms ) {
953 0         0 return ( 'fail', 'Bailed out of tests');
954             }
955             elsif ( $line =~ m{^Failed }ms ) { # must be lowercase
956 0         0 return ( 'fail', 'One or more tests failed');
957             }
958             else {
959 0         0 return;
960             }
961             }
962              
963             #--------------------------------------------------------------------------#
964             # _prereq_report
965             #--------------------------------------------------------------------------#
966              
967             my @prereq_sections = qw(
968             requires build_requires configure_requires opt_requires opt_build_requires
969             );
970              
971             sub _prereq_report {
972 179     179   166913 my $dist = shift;
973 179         713 my (%need, %have, %prereq_met, $report);
974              
975             # Extract requires/build_requires from CPAN dist
976 179         1476 my $prereq_pm = $dist->prereq_pm;
977              
978 179 50       2992 if ( ref $prereq_pm eq 'HASH' ) {
979             # CPAN 1.94 returns hash with requires/build_requires # so no need to support old style
980 179         1748 foreach (values %$prereq_pm) {
981 711 50 66     5215 if (defined && ref ne 'HASH') {
982 0         0 require Data::Dumper;
983 0         0 warn "Data error detecting prerequisites. Please report it to CPAN::Reporter bug tracker:";
984 0         0 warn Data::Dumper::Dumper($prereq_pm);
985 0         0 die "Stopping";
986             }
987             }
988              
989 179         2146 for my $sec ( @prereq_sections ) {
990 895 100       1894 $need{$sec} = $prereq_pm->{$sec} if keys %{ $prereq_pm->{$sec} };
  895         7537  
991             }
992             }
993              
994             # Extract configure_requires from META.yml if it exists
995 179 100 66     2789 if ( $dist->{build_dir} && -d $dist->{build_dir} ) {
996 72         2563 my $meta_yml = File::Spec->catfile($dist->{build_dir}, 'META.yml');
997 72 100       1682 if ( -f $meta_yml ) {
998 4         33 my @yaml = eval { Parse::CPAN::Meta::LoadFile($meta_yml) };
  4         82  
999 4 100       12750 if ( $@ ) {
1000 2         100 $CPAN::Frontend->mywarn(
1001             "CPAN::Reporter: error parsing META.yml\n"
1002             );
1003             }
1004 4 100 66     124 if ( ref $yaml[0] eq 'HASH' &&
1005             ref $yaml[0]{configure_requires} eq 'HASH'
1006             ) {
1007 2         29 $need{configure_requires} = $yaml[0]{configure_requires};
1008             }
1009             }
1010             }
1011              
1012             # see what prereqs are satisfied in subprocess
1013 179         980 for my $section ( @prereq_sections ) {
1014 895 100       4650 next unless ref $need{$section} eq 'HASH';
1015 137         471 my @prereq_list = %{ $need{$section} };
  137         1290  
1016 137 50       868 next unless @prereq_list;
1017 137         1633 my $prereq_results = _version_finder( @prereq_list );
1018 137         845 for my $mod ( keys %{$prereq_results} ) {
  137         1326  
1019 185         2652 $have{$section}{$mod} = $prereq_results->{$mod}{have};
1020 185         3153 $prereq_met{$section}{$mod} = $prereq_results->{$mod}{met};
1021             }
1022             }
1023              
1024             # find formatting widths
1025 179         1338 my ($name_width, $need_width, $have_width) = (6, 4, 4);
1026 179         871 for my $section ( @prereq_sections ) {
1027 895         1932 for my $module ( keys %{ $need{$section} } ) {
  895         6801  
1028 185         753 my $name_length = length $module;
1029 185         1141 my $need_length = length $need{$section}{$module};
1030 185         622 my $have_length = length $have{$section}{$module};
1031 185 100       825 $name_width = $name_length if $name_length > $name_width;
1032 185 100       801 $need_width = $need_length if $need_length > $need_width;
1033 185 100       961 $have_width = $have_length if $have_length > $have_width;
1034             }
1035             }
1036              
1037 179         2226 my $format_str =
1038             " \%1s \%-${name_width}s \%-${need_width}s \%-${have_width}s\n";
1039              
1040             # generate the report
1041 179         1027 for my $section ( @prereq_sections ) {
1042 895 100       1675 if ( keys %{ $need{$section} } ) {
  895         3197  
1043 137         921 $report .= "$section:\n\n";
1044 137         1681 $report .= sprintf( $format_str, " ", qw/Module Need Have/ );
1045 137         1552 $report .= sprintf( $format_str, " ",
1046             "-" x $name_width,
1047             "-" x $need_width,
1048             "-" x $have_width );
1049 137         590 for my $module (sort {lc $a cmp lc $b} keys %{ $need{$section} } ) {
  137         300  
  137         1099  
1050 185         812 my $need = $need{$section}{$module};
1051 185         653 my $have = $have{$section}{$module};
1052 185 100       1377 my $bad = $prereq_met{$section}{$module} ? " " : "!";
1053 185         1364 $report .=
1054             sprintf( $format_str, $bad, $module, $need, $have);
1055             }
1056 137         627 $report .= "\n";
1057             }
1058             }
1059              
1060 179   100     6042 return $report || " No requirements found\n";
1061             }
1062              
1063             #--------------------------------------------------------------------------#
1064             # _print_grade_msg -
1065             #--------------------------------------------------------------------------#
1066              
1067             sub _print_grade_msg {
1068 138     138   1532 my ($phase, $result) = @_;
1069 138         946 my ($grade, $msg) = ($result->{grade}, $result->{grade_msg});
1070 138         7079 $CPAN::Frontend->myprint( "CPAN::Reporter: $phase result is '$grade'");
1071 138 50 33     10865 $CPAN::Frontend->myprint(", $msg") if defined $msg && length $msg;
1072 138         3268 $CPAN::Frontend->myprint(".\n");
1073 138         2297 return;
1074             }
1075              
1076             #--------------------------------------------------------------------------#
1077             # _prompt
1078             #
1079             # Note: always returns lowercase
1080             #--------------------------------------------------------------------------#
1081              
1082             sub _prompt {
1083 328     328   42210 my ($config, $option, $grade, $extra) = @_;
1084 328   50     2884 $extra ||= q{};
1085              
1086 328         1284 my %spec = CPAN::Reporter::Config::_config_spec();
1087              
1088             my $dispatch = CPAN::Reporter::Config::_validate_grade_action_pair(
1089 328   50     2628 $option, join(q{ }, "default:no", $config->{$option} || '')
1090             );
1091 328   66     1996 my $action = $dispatch->{$grade} || $dispatch->{default};
1092 328         1695 my $intro = $spec{$option}{prompt} . $extra . " (yes/no)";
1093 328         669 my $prompt;
1094 328 100       2388 if ( $action =~ m{^ask/yes}i ) {
    100          
1095 10         47 $prompt = CPAN::Shell::colorable_makemaker_prompt( $intro, "yes" );
1096             }
1097             elsif ( $action =~ m{^ask(/no)?}i ) {
1098 72         726 $prompt = CPAN::Shell::colorable_makemaker_prompt( $intro, "no" );
1099             }
1100             else {
1101 246         644 $prompt = $action;
1102             }
1103 328         14124 return lc $prompt;
1104             }
1105              
1106             #--------------------------------------------------------------------------#
1107             # _report_text
1108             #--------------------------------------------------------------------------#
1109              
1110             my %intro_para = (
1111             'pass' => <<'HERE',
1112             Thank you for uploading your work to CPAN. Congratulations!
1113             All tests were successful.
1114             HERE
1115              
1116             'fail' => <<'HERE',
1117             Thank you for uploading your work to CPAN. However, there was a problem
1118             testing your distribution.
1119              
1120             If you think this report is invalid, please consult the CPAN Testers Wiki
1121             for suggestions on how to avoid getting FAIL reports for missing library
1122             or binary dependencies, unsupported operating systems, and so on:
1123              
1124             http://wiki.cpantesters.org/wiki/CPANAuthorNotes
1125             HERE
1126              
1127             'unknown' => <<'HERE',
1128             Thank you for uploading your work to CPAN. However, attempting to
1129             test your distribution gave an inconclusive result.
1130              
1131             This could be because your distribution had an error during the make/build
1132             stage, did not define tests, tests could not be found, because your tests were
1133             interrupted before they finished, or because the results of the tests could not
1134             be parsed. You may wish to consult the CPAN Testers Wiki:
1135              
1136             http://wiki.cpantesters.org/wiki/CPANAuthorNotes
1137             HERE
1138              
1139             'na' => <<'HERE',
1140             Thank you for uploading your work to CPAN. While attempting to build or test
1141             this distribution, the distribution signaled that support is not available
1142             either for this operating system or this version of Perl. Nevertheless, any
1143             diagnostic output produced is provided below for reference. If this is not
1144             what you expect, you may wish to consult the CPAN Testers Wiki:
1145              
1146             http://wiki.cpantesters.org/wiki/CPANAuthorNotes
1147             HERE
1148              
1149             );
1150              
1151             sub _comment_text {
1152              
1153             # We assemble the completed comment as a series of "parts" which
1154             # will get joined together
1155 117     117   252 my @comment_parts;
1156              
1157             # All automated testing gets a preamble
1158 117 100       635 if ($ENV{AUTOMATED_TESTING}) {
1159 111         428 push @comment_parts,
1160             "this report is from an automated smoke testing program\n"
1161             . "and was not reviewed by a human for accuracy"
1162             }
1163              
1164             # If a comment file is provided, read it and add it to the comment
1165 117         477 my $confdir = CPAN::Reporter::Config::_get_config_dir();
1166 117         1380 my $comment_file = File::Spec->catfile($confdir, 'comment.txt');
1167 117 100 66     4331 if ( -d $confdir && -f $comment_file && -r $comment_file ) {
      66        
1168 2 50   1   180 open my $fh, '<:encoding(UTF-8)', $comment_file or die($!);
  1         9  
  1         7  
  1         35  
1169 2         2207 my $text;
1170 2         9 do {
1171 2         15 local $/ = undef; # No record (line) seperator on input
1172 2 50       84 defined( $text = <$fh> ) or die($!);
1173             };
1174 2         71 chomp($text);
1175 2         10 push @comment_parts, $text;
1176 2         34 close $fh;
1177             }
1178              
1179             # If we have an empty comment so far, add a default value
1180 117 100       744 if (scalar(@comment_parts) == 0) {
1181 5         52 push @comment_parts, 'none provided';
1182             }
1183              
1184             # Join the parts seperated by a blank line
1185 117         814 return join "\n\n", @comment_parts;
1186             }
1187              
1188             sub _report_text {
1189 117     117   403 my $data = shift;
1190 117         283 my $test_log = join(q{},@{$data->{output}});
  117         1213  
1191 117 50       670 if ( length $test_log > MAX_OUTPUT_LENGTH ) {
1192 0         0 my $max_k = int(MAX_OUTPUT_LENGTH/1000) . "K";
1193 0         0 $test_log = substr( $test_log, 0, MAX_OUTPUT_LENGTH/2 ) . "\n\n"
1194             . "[Output truncated because it exceeded $max_k]\n\n"
1195             . substr( $test_log, -(MAX_OUTPUT_LENGTH/2) );
1196             }
1197              
1198 117         551 my $comment_body = _comment_text();
1199              
1200             # generate report
1201 117         3236 my $output = << "ENDREPORT";
1202             Dear $data->{author},
1203              
1204             This is a computer-generated report for $data->{dist_name}
1205             on perl $data->{perl_version}, created by CPAN-Reporter-$CPAN::Reporter::VERSION\.
1206              
1207             $intro_para{ $data->{grade} }
1208             Sections of this report:
1209              
1210             * Tester comments
1211             * Program output
1212             * Prerequisites
1213             * Environment and other context
1214              
1215             ------------------------------
1216             TESTER COMMENTS
1217             ------------------------------
1218              
1219             Additional comments from tester:
1220              
1221             $comment_body
1222              
1223             ------------------------------
1224             PROGRAM OUTPUT
1225             ------------------------------
1226              
1227             Output from '$data->{command}':
1228              
1229             $test_log
1230             ------------------------------
1231             PREREQUISITES
1232             ------------------------------
1233              
1234             Prerequisite modules loaded:
1235              
1236             $data->{prereq_pm}
1237             ------------------------------
1238             ENVIRONMENT AND OTHER CONTEXT
1239             ------------------------------
1240              
1241             Environment variables:
1242              
1243             $data->{env_vars}
1244             Perl special variables (and OS-specific diagnostics, for MSWin32):
1245              
1246             $data->{special_vars}
1247             Perl module toolchain versions installed:
1248              
1249             $data->{toolchain_versions}
1250             ENDREPORT
1251              
1252 117         1009 return $output;
1253             }
1254              
1255             #--------------------------------------------------------------------------#
1256             # _special_vars_report
1257             #--------------------------------------------------------------------------#
1258              
1259             sub _special_vars_report {
1260 176     176   16633 my $special_vars = << "HERE";
1261             \$^X = $^X
1262             \$UID/\$EUID = $< / $>
1263             \$GID = $(
1264             \$EGID = $)
1265             HERE
1266 176 50 33     1623 if ( $^O eq 'MSWin32' && eval "require Win32" ) { ## no critic
1267 0         0 my @getosversion = Win32::GetOSVersion();
1268 0         0 my $getosversion = join(", ", @getosversion);
1269 0         0 $special_vars .= " Win32::GetOSName = " . Win32::GetOSName() . "\n";
1270 0         0 $special_vars .= " Win32::GetOSVersion = $getosversion\n";
1271 0         0 $special_vars .= " Win32::FsType = " . Win32::FsType() . "\n";
1272 0         0 $special_vars .= " Win32::IsAdminUser = " . Win32::IsAdminUser() . "\n";
1273             }
1274 176         1304 return $special_vars;
1275             }
1276              
1277             #--------------------------------------------------------------------------#
1278             # _split_redirect
1279             #--------------------------------------------------------------------------#
1280              
1281             sub _split_redirect {
1282 186     186   792 my $command = shift;
1283 186         1608 my ($cmd, $prefix) = ($command =~ m{\A(.+?)(\|.*)\z});
1284 186 100       854 if (defined $cmd) {
1285 1         19 return ($cmd, $prefix);
1286             }
1287             else { # didn't match a redirection
1288 185         803 return $command
1289             }
1290             }
1291              
1292             #--------------------------------------------------------------------------#
1293             # _temp_filename -- stand-in for File::Temp for backwards compatibility
1294             #
1295             # takes an optional prefix, adds 8 random chars and returns
1296             # an absolute pathname
1297             #
1298             # NOTE -- manual unlink required
1299             #--------------------------------------------------------------------------#
1300              
1301             # @CHARS from File::Temp
1302             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
1303             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
1304             0 1 2 3 4 5 6 7 8 9 _
1305             /);
1306              
1307             sub _temp_filename {
1308 500     500   3485 my ($prefix) = @_;
1309 500 50       2315 $prefix = q{} unless defined $prefix;
1310 500         10470 $prefix .= $CHARS[ int( rand(@CHARS) ) ] for 0 .. 7;
1311 500         27556 return File::Spec->catfile(File::Spec->tmpdir(), $prefix);
1312             }
1313              
1314             #--------------------------------------------------------------------------#
1315             # _timeout_wrapper
1316             # Timeout technique adapted from App::cpanminus (thank you Miyagawa!)
1317             #--------------------------------------------------------------------------#
1318              
1319             sub _timeout_wrapper {
1320 15     15   88 my ($cmd, $timeout) = @_;
1321              
1322             # protect shell quotes
1323 15         68 $cmd = quotemeta($cmd);
1324              
1325 15         154 my $wrapper = sprintf << 'HERE', $timeout, $cmd, $cmd;
1326             use strict;
1327             my ($pid, $exitcode);
1328             eval {
1329             $pid = fork;
1330             if ($pid) {
1331             local $SIG{CHLD};
1332             local $SIG{ALRM} = sub {die 'Timeout'};
1333             alarm %s;
1334             my $wstat = waitpid $pid, 0;
1335             alarm 0;
1336             $exitcode = $wstat == -1 ? -1 : $?;
1337             } elsif ( $pid == 0 ) {
1338             setpgrp(0,0); # new process group
1339             exec "%s";
1340             }
1341             else {
1342             die "Cannot fork: $!\n" unless defined $pid;
1343             }
1344             };
1345             if ($pid && $@ =~ /Timeout/){
1346             kill -9 => $pid; # and send to our child's whole process group
1347             waitpid $pid, 0;
1348             $exitcode = 9; # force result to look like SIGKILL
1349             }
1350             elsif ($@) {
1351             die $@;
1352             }
1353             print "(%s exited with $exitcode)\n";
1354             HERE
1355 15         75 return $wrapper;
1356             }
1357              
1358             #--------------------------------------------------------------------------#
1359             # _timeout_wrapper_win32
1360             #--------------------------------------------------------------------------#
1361              
1362             sub _timeout_wrapper_win32 {
1363 0     0   0 my ($cmd, $timeout) = @_;
1364              
1365 0   0     0 $timeout ||= 0; # just in case upstream doesn't guarantee it
1366              
1367 0         0 eval "use Win32::Job ();";
1368 0 0       0 if ($@) {
1369 0         0 $CPAN::Frontend->mywarn( << 'HERE' );
1370             CPAN::Reporter: you need Win32::Job for inactivity_timeout support.
1371             Continuing without timeout...
1372             HERE
1373 0         0 return;
1374             }
1375              
1376 0         0 my ($program) = split " ", $cmd;
1377 0 0       0 if (! File::Spec->file_name_is_absolute( $program ) ) {
1378 0         0 my $exe = $program . ".exe";
1379 0         0 my ($path) = grep { -e File::Spec->catfile($_,$exe) }
1380 0         0 split /$Config{path_sep}/, $ENV{PATH};
1381 0 0       0 if (! $path) {
1382 0         0 $CPAN::Frontend->mywarn( << "HERE" );
1383             CPAN::Reporter: can't locate $exe in the PATH.
1384             Continuing without timeout...
1385             HERE
1386 0         0 return;
1387             }
1388 0         0 $program = File::Spec->catfile($path,$exe);
1389             }
1390              
1391             # protect shell quotes and other things
1392 0         0 $_ = quotemeta($_) for ($program, $cmd);
1393              
1394 0         0 my $wrapper = sprintf << 'HERE', $program, $cmd, $timeout;
1395             use strict;
1396             use Win32::Job;
1397             my $executable = "%s";
1398             my $cmd_line = "%s";
1399             my $timeout = %s;
1400              
1401             my $job = Win32::Job->new() or die $^E;
1402             my $ppid = $job->spawn($executable, $cmd_line);
1403             $job->run($timeout);
1404             my $status = $job->status;
1405             my $exitcode = $status->{$ppid}{exitcode};
1406             if ( $exitcode == 293 ) {
1407             $exitcode = 9; # map Win32::Job kill (293) to SIGKILL (9)
1408             }
1409             elsif ( $exitcode & 255 ) {
1410             $exitcode = $exitcode << 8; # how perl expects it
1411             }
1412             print "($cmd_line exited with $exitcode)\n";
1413             HERE
1414 0         0 return $wrapper;
1415             }
1416              
1417             #--------------------------------------------------------------------------#-
1418             # _toolchain_report
1419             #--------------------------------------------------------------------------#
1420              
1421             my @toolchain_mods= qw(
1422             CPAN
1423             CPAN::Meta
1424             Cwd
1425             ExtUtils::CBuilder
1426             ExtUtils::Command
1427             ExtUtils::Install
1428             ExtUtils::MakeMaker
1429             ExtUtils::Manifest
1430             ExtUtils::ParseXS
1431             File::Spec
1432             JSON
1433             JSON::PP
1434             Module::Build
1435             Module::Signature
1436             Parse::CPAN::Meta
1437             Test::Harness
1438             Test::More
1439             Test2
1440             YAML
1441             YAML::Syck
1442             version
1443             );
1444              
1445             sub _toolchain_report {
1446 176     176   3414 my ($result) = @_;
1447              
1448 176         983 my $installed = _version_finder( map { $_ => 0 } @toolchain_mods );
  3696         12075  
1449 176         7577 $result->{toolchain} = $installed;
1450              
1451 176         4838 my $mod_width = _max_length( keys %$installed );
1452             my $ver_width = _max_length(
1453 176         1694 map { $installed->{$_}{have} } keys %$installed
  3696         8082  
1454             );
1455              
1456 176         2498 my $format = " \%-${mod_width}s \%-${ver_width}s\n";
1457              
1458 176         1098 my $report = "";
1459 176         1967 $report .= sprintf( $format, "Module", "Have" );
1460 176         2031 $report .= sprintf( $format, "-" x $mod_width, "-" x $ver_width );
1461              
1462 176         3989 for my $var ( sort keys %$installed ) {
1463             $report .= sprintf(" \%-${mod_width}s \%-${ver_width}s\n",
1464 3696         12732 $var, $installed->{$var}{have} );
1465             }
1466              
1467 176         4012 return $report;
1468             }
1469              
1470              
1471             #--------------------------------------------------------------------------#
1472             # _validate_metabase_args
1473             #
1474             # This is a kludge to make metabase transport args a little less
1475             # clunky for novice users
1476             #--------------------------------------------------------------------------#
1477              
1478             sub _validate_metabase_args {
1479 116     116   1013 my @transport_args = @_;
1480 116         298 shift @transport_args; # drop leading 'Metabase'
1481 116         319 my (%args, $error);
1482              
1483 116 50       694 if ( @transport_args % 2 != 0 ) {
1484 0         0 $error = << "TRANSPORT_ARGS";
1485              
1486             CPAN::Reporter: Metabase 'transport' option had odd number of
1487             parameters in the config file. See documentation for proper
1488             configuration format.
1489              
1490             TRANSPORT_ARGS
1491             }
1492             else {
1493 116         1272 %args = @transport_args;
1494              
1495 116         830 for my $key ( qw/uri id_file/ ) {
1496 232 50       1042 if ( ! $args{$key} ) {
1497 0         0 $error = << "TRANSPORT_ARGS";
1498              
1499             CPAN::Reporter: Metabase 'transport' option did not have
1500             a '$key' parameter in the config file. See documentation for
1501             proper configuration format.
1502              
1503             TRANSPORT_ARGS
1504             }
1505             }
1506             }
1507              
1508 116 50       460 if ( $error ) {
1509 0         0 $CPAN::Frontend->mywarn( $error );
1510 0         0 return;
1511             }
1512              
1513 116         635 $args{id_file} = CPAN::Reporter::Config::_normalize_id_file( $args{id_file} );
1514              
1515 116 50       2736 if ( ! -r $args{id_file} ) {
1516 0         0 $CPAN::Frontend->mywarn( <<"TRANSPORT_ARGS" );
1517              
1518             CPAN::Reporter: Could not find Metabase transport 'id_file' parameter
1519             located at '$args{id_file}'.
1520             See documentation for proper configuration of the 'transport' setting.
1521              
1522             TRANSPORT_ARGS
1523 0         0 return;
1524             }
1525              
1526 116         1311 return ('Metabase', %args);
1527             }
1528              
1529              
1530             #--------------------------------------------------------------------------#
1531             # _version_finder
1532             #
1533             # module => version pairs
1534             #
1535             # This is done via an external program to show installed versions exactly
1536             # the way they would be found when test programs are run. This means that
1537             # any updates to PERL5LIB will be reflected in the results.
1538             #
1539             # File-finding logic taken from CPAN::Module::inst_file(). Logic to
1540             # handle newer Module::Build prereq syntax is taken from
1541             # CPAN::Distribution::unsat_prereq()
1542             #
1543             #--------------------------------------------------------------------------#
1544              
1545             my $version_finder = $INC{'CPAN/Reporter/PrereqCheck.pm'};
1546              
1547             sub _version_finder {
1548 314     314   17353 my %prereqs = @_;
1549              
1550 314         12963 my $perl = Probe::Perl->find_perl_interpreter();
1551 314         18254 my @prereq_results;
1552              
1553 314         3295 my $prereq_input = _temp_filename( 'CPAN-Reporter-PI-' );
1554 314 50       10805 my $fh = IO::File->new( $prereq_input, "w" )
1555             or die "Could not create temporary '$prereq_input' for prereq analysis: $!";
1556 314         124711 $fh->print( map { "$_ $prereqs{$_}\n" } keys %prereqs );
  3895         17063  
1557 314         9330 $fh->close;
1558              
1559 314     314   54704 my $prereq_result = capture { system( $perl, $version_finder, '<', $prereq_input ) };
  314         123483425  
1560              
1561 314         672624 unlink $prereq_input;
1562              
1563 314         2699 my %result;
1564 314         5465 for my $line ( split "\n", $prereq_result ) {
1565 3895 50       12377 next unless length $line;
1566 3895         17515 my ($mod, $met, $have) = split " ", $line;
1567 3895 50 33     26181 unless ( defined($mod) && defined($met) && defined($have) ) {
      33        
1568 0         0 $CPAN::Frontend->mywarn(
1569             "Error parsing output from CPAN::Reporter::PrereqCheck:\n" .
1570             $line
1571             );
1572 0         0 next;
1573             }
1574 3895         40142 $result{$mod}{have} = $have;
1575 3895         15135 $result{$mod}{met} = $met;
1576             }
1577 314         13677 return \%result;
1578             }
1579              
1580             1;
1581              
1582             # ABSTRACT: Adds CPAN Testers reporting to CPAN.pm
1583              
1584             =pod
1585              
1586             =encoding UTF-8
1587              
1588             =head1 NAME
1589              
1590             CPAN::Reporter - Adds CPAN Testers reporting to CPAN.pm
1591              
1592             =head1 VERSION
1593              
1594             version 1.2019
1595              
1596             =head1 SYNOPSIS
1597              
1598             From the CPAN shell:
1599              
1600             cpan> install Task::CPAN::Reporter
1601             cpan> reload cpan
1602             cpan> o conf init test_report
1603              
1604             Installing L will pull in additional dependencies
1605             that new CPAN Testers will need.
1606              
1607             Advanced CPAN Testers with custom L setups
1608             may wish to install only CPAN::Reporter, which has fewer dependencies.
1609              
1610             =head1 DESCRIPTION
1611              
1612             The CPAN Testers project captures and analyzes detailed results from building
1613             and testing CPAN distributions on multiple operating systems and multiple
1614             versions of Perl. This provides valuable feedback to module authors and
1615             potential users to identify bugs or platform compatibility issues and improves
1616             the overall quality and value of CPAN.
1617              
1618             One way individuals can contribute is to send a report for each module that
1619             they test or install. CPAN::Reporter is an add-on for the CPAN.pm module to
1620             send the results of building and testing modules to the CPAN Testers project.
1621             Full support for CPAN::Reporter is available in CPAN.pm as of version 1.92.
1622              
1623             =for Pod::Coverage configure
1624             grade_PL
1625             grade_make
1626             grade_test
1627             record_command
1628             test
1629              
1630             =head1 GETTING STARTED
1631              
1632             =head2 Installation
1633              
1634             The first step in using CPAN::Reporter is to install it using whatever
1635             version of CPAN.pm is already installed. CPAN.pm will be upgraded as
1636             a dependency if necessary.
1637              
1638             cpan> install CPAN::Reporter
1639              
1640             If CPAN.pm was upgraded, it needs to be reloaded.
1641              
1642             cpan> reload cpan
1643              
1644             =head2 Configuration
1645              
1646             If upgrading from a very old version of CPAN.pm, users may be prompted to renew
1647             their configuration settings, including the 'test_report' option to enable
1648             CPAN::Reporter.
1649              
1650             If not prompted automatically, users should manually initialize CPAN::Reporter
1651             support. After enabling CPAN::Reporter, CPAN.pm will automatically continue
1652             with interactive configuration of CPAN::Reporter options.
1653              
1654             cpan> o conf init test_report
1655              
1656             Users will need to enter an email address in one of the following formats:
1657              
1658             johndoe@example.com
1659             John Doe
1660             "John Q. Public"
1661              
1662             Users that are new to CPAN::Reporter should accept the recommended values
1663             for other configuration options.
1664              
1665             Users will be prompted to create a I file that uniquely
1666             identifies their test reports. See L below for details.
1667              
1668             After completing interactive configuration, be sure to commit (save) the CPAN
1669             configuration changes.
1670              
1671             cpan> o conf commit
1672              
1673             See L for advanced configuration settings.
1674              
1675             =head3 The Metabase
1676              
1677             CPAN::Reporter sends test reports to a server known as the Metabase. This
1678             requires an active Internet connection and a profile file. To create the
1679             profile, users will need to run C<<< metabase-profile >>> from a terminal window and
1680             fill the information at the prompts. This will create a file called
1681             C<<< metabase_id.json >>> in the current directory. That file should be moved to the
1682             C<<< .cpanreporter >>> directory inside the user's home directory.
1683              
1684             Users with an existing metabase profile file (e.g. from another machine),
1685             should copy it into the C<<< .cpanreporter >>> directory instead of creating
1686             a new one. Profile files may be located outside the C<<< .cpanreporter >>>
1687             directory by following instructions in L.
1688              
1689             =head3 Default Test Comments
1690              
1691             This module puts default text into the "TESTER COMMENTS" section, typically,
1692             "none provided" if doing interactive testing, or, if doing smoke testing that
1693             sets CE$ENV{AUTOMATED_TESTING}E to a true value, "this report is from an
1694             automated smoke testing program and was not reviewed by a human for
1695             accuracy." If CECPAN::ReporterE is configured to allow editing of the
1696             report, this can be edited during submission.
1697              
1698             If you wish to override the default comment, you can create a file named
1699             CEcomment.txtE in the configuration directory (typically C<<< .cpanreporter >>>
1700             under the user's home directory), with the default comment you would
1701             like to appear.
1702              
1703             Note that if your test is an automated smoke
1704             test (CE$ENV{AUTOMATED_TESTING}E is set to a true value), the smoke
1705             test notice ("this report is from an automated smoke testing program and
1706             was not reviewed by a human for accuracy") is included along with a blank
1707             line before your CEcomment.txtE, so that it is always possible to
1708             distinguish automated tests from non-automated tests that use this
1709             module.
1710              
1711             =head2 Using CPAN::Reporter
1712              
1713             Once CPAN::Reporter is enabled and configured, test or install modules with
1714             CPAN.pm as usual.
1715              
1716             For example, to test the File::Marker module:
1717              
1718             cpan> test File::Marker
1719              
1720             If a distribution's tests fail, users will be prompted to edit the report to
1721             add additional information that might help the author understand the failure.
1722              
1723             =head1 UNDERSTANDING TEST GRADES
1724              
1725             CPAN::Reporter will assign one of the following grades to the report:
1726              
1727             =over
1728              
1729             =item *
1730              
1731             C<<< pass >>> -- distribution built and tested correctly
1732              
1733             =item *
1734              
1735             C<<< fail >>> -- distribution failed to test correctly
1736              
1737             =item *
1738              
1739             C<<< unknown >>> -- distribution failed to build, had no test suite or outcome was
1740             inconclusive
1741              
1742             =item *
1743              
1744             C<<< na >>> --- distribution is not applicable to this platform andEor
1745             version of Perl
1746              
1747             =back
1748              
1749             In returning results of the test suite to CPAN.pm, "pass" and "unknown" are
1750             considered successful attempts to "make test" or "Build test" and will not
1751             prevent installation. "fail" and "na" are considered to be failures and
1752             CPAN.pm will not install unless forced.
1753              
1754             An error from Makefile.PLEBuild.PL or makeEBuild will also be graded as
1755             "unknown" and a failure will be signaled to CPAN.pm.
1756              
1757             If prerequisites specified in C<<< Makefile.PL >>> or C<<< Build.PL >>> are not available,
1758             no report will be generated and a failure will be signaled to CPAN.pm.
1759              
1760             =head1 PRIVACY WARNING
1761              
1762             CPAN::Reporter includes information in the test report about environment
1763             variables and special Perl variables that could be affecting test results in
1764             order to help module authors interpret the results of the tests. This includes
1765             information about paths, terminal, locale, userEgroup ID, installed toolchain
1766             modules (e.g. ExtUtils::MakeMaker) and so on.
1767              
1768             These have been intentionally limited to items that should not cause harmful
1769             personal information to be revealed -- it does I include your entire
1770             environment. Nevertheless, please do not use CPAN::Reporter if you are
1771             concerned about the disclosure of this information as part of your test report.
1772              
1773             Users wishing to review this information may choose to edit the report
1774             prior to sending it.
1775              
1776             =head1 BUGS
1777              
1778             Using command_timeout on Linux may cause problems. See
1779             L
1780              
1781             Please report any bugs or feature using the CPAN Request Tracker.
1782             Bugs can be submitted through the web interface at
1783             L
1784              
1785             When submitting a bug or request, please include a test-file or a patch to an
1786             existing test-file that illustrates the bug or desired feature.
1787              
1788             =head1 SEE ALSO
1789              
1790             Information about CPAN::Testers:
1791              
1792             =over
1793              
1794             =item *
1795              
1796             L -- overview of CPAN Testers architecture stack
1797              
1798             =item *
1799              
1800             L -- project home with all reports
1801              
1802             =item *
1803              
1804             L -- documentation and wiki
1805              
1806             =back
1807              
1808             Additional Documentation:
1809              
1810             =over
1811              
1812             =item *
1813              
1814             L -- advanced configuration settings
1815              
1816             =item *
1817              
1818             L -- hints and tips
1819              
1820             =back
1821              
1822             =for :stopwords cpan testmatrix url bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
1823              
1824             =head1 SUPPORT
1825              
1826             =head2 Bugs / Feature Requests
1827              
1828             Please report any bugs or feature requests through the issue tracker
1829             at L.
1830             You will be notified automatically of any progress on your issue.
1831              
1832             =head2 Source Code
1833              
1834             This is open source software. The code repository is available for
1835             public review and contribution under the terms of the license.
1836              
1837             L
1838              
1839             git clone https://github.com/cpan-testers/CPAN-Reporter.git
1840              
1841             =head1 AUTHOR
1842              
1843             David Golden
1844              
1845             =head1 CONTRIBUTORS
1846              
1847             =for stopwords Alexandr Ciornii Breno G. de Oliveira Christian Walde David Cantrell Ed J Graham Knop James E Keenan J. Maslak José Joaquín Atria Kent Fredric Matthew Musgrove Patrice Clement Reini Urban Scott Wiersdorf Slaven Rezic
1848              
1849             =over 4
1850              
1851             =item *
1852              
1853             Alexandr Ciornii
1854              
1855             =item *
1856              
1857             Breno G. de Oliveira
1858              
1859             =item *
1860              
1861             Christian Walde
1862              
1863             =item *
1864              
1865             David Cantrell
1866              
1867             =item *
1868              
1869             Ed J
1870              
1871             =item *
1872              
1873             Graham Knop
1874              
1875             =item *
1876              
1877             James E Keenan
1878              
1879             =item *
1880              
1881             J. Maslak
1882              
1883             =item *
1884              
1885             José Joaquín Atria
1886              
1887             =item *
1888              
1889             Kent Fredric
1890              
1891             =item *
1892              
1893             Matthew Musgrove
1894              
1895             =item *
1896              
1897             Patrice Clement
1898              
1899             =item *
1900              
1901             Reini Urban
1902              
1903             =item *
1904              
1905             Scott Wiersdorf
1906              
1907             =item *
1908              
1909             Slaven Rezic
1910              
1911             =back
1912              
1913             =head1 COPYRIGHT AND LICENSE
1914              
1915             This software is Copyright (c) 2023 by David Golden.
1916              
1917             This is free software, licensed under:
1918              
1919             The Apache License, Version 2.0, January 2004
1920              
1921             =cut
1922              
1923             __END__