File Coverage

blib/lib/CPAN/Testers/ParseReport.pm
Criterion Covered Total %
statement 451 725 62.2
branch 262 494 53.0
condition 75 148 50.6
subroutine 25 30 83.3
pod 4 4 100.0
total 817 1401 58.3


line stmt bran cond sub pod time code
1             package CPAN::Testers::ParseReport;
2              
3 2     2   191453 use warnings;
  2         16  
  2         69  
4 2     2   11 use strict;
  2         4  
  2         40  
5              
6 2     2   1230 use Compress::Zlib ();
  2         125356  
  2         61  
7 2     2   1294 use DateTime::Format::Strptime;
  2         1117197  
  2         11  
8 2     2   189 use File::Basename qw(basename);
  2         4  
  2         173  
9 2     2   15 use File::Path qw(mkpath);
  2         4  
  2         126  
10 2     2   1059 use HTML::Entities qw(decode_entities);
  2         11426  
  2         171  
11 2     2   1318 use LWP::UserAgent;
  2         78200  
  2         85  
12 2     2   1174 use List::AllUtils qw(uniq max min sum);
  2         21293  
  2         227  
13 2     2   966 use MIME::QuotedPrint ();
  2         2565  
  2         46  
14 2     2   14 use Time::Local ();
  2         4  
  2         32  
15 2     2   974 use Time::HiRes;
  2         2804  
  2         9  
16 2     2   249 use utf8;
  2         5  
  2         21  
17              
18             our $default_transport = "http_cpantesters";
19             our $default_cturl = "http://static.cpantesters.org/distro";
20             our $Signal = 0;
21              
22             =encoding utf-8
23              
24             =head1 NAME
25              
26             CPAN::Testers::ParseReport - parse reports to www.cpantesters.org from various sources
27              
28             =cut
29              
30 2     2   995 use version; our $VERSION = qv('0.4.5');
  2         3669  
  2         11  
31              
32             =head1 SYNOPSIS
33              
34             The documentation in here is normally not needed because the code is
35             meant to be run from the standalone program C<ctgetreports>.
36              
37             ctgetreports --q mod:Moose Devel-Events
38              
39             =head1 DESCRIPTION
40              
41             This is the core module for CPAN::Testers::ParseReport. If you're not
42             looking to extend or alter the behaviour of this module, you probably
43             want to look at L<ctgetreports> instead.
44              
45             =head1 OPTIONS
46              
47             Options are described in the L<ctgetreports> manpage and are passed
48             through to the functions unaltered.
49              
50             =head1 FUNCTIONS
51              
52             =head2 parse_distro($distro,%options)
53              
54             reads the cpantesters JSON file or the local database for the distro
55             and loops through the reports for the specified or most recent version
56             of that distro found in these data.
57              
58             parse_distro() intentionally has no meaningful return value, different
59             options would require different ones.
60              
61             =head2 $extract = parse_single_report($report,$dumpvars,%options)
62              
63             mirrors and reads this report. $report is of the form
64              
65             { id => <integer>, guid => <guid>, }
66              
67             $dumpvar is a hashreference that gets filled with data.
68              
69             $extract is the result of parse_report() described below.
70              
71             =cut
72              
73             {
74             my $ua;
75             sub _ua {
76 0 0   0   0 return $ua if $ua;
77 0         0 $ua = LWP::UserAgent->new
78             (
79             keep_alive => 1,
80             env_proxy => 1,
81             );
82 0         0 $ua->parse_head(0);
83 0         0 $ua;
84             }
85             }
86             {
87             my $ua;
88             sub _ua_gzip {
89 0 0   0   0 return $ua if $ua;
90 0         0 $ua = LWP::UserAgent->new
91             (
92             keep_alive => 1,
93             env_proxy => 1,
94             );
95 0         0 $ua->parse_head(0);
96 0         0 $ua->default_header('Accept-Encoding' => scalar HTTP::Message::decodable());
97 0         0 $ua;
98             }
99             }
100              
101             {
102             # we called it yaml because it was yaml; now it is json
103 2     2   1853 use JSON::XS;
  2         9891  
  2         19219  
104             my $j = JSON::XS->new->ascii->pretty;
105             sub _slurp {
106 8     8   28 my($file) = @_;
107 8         47 local $/;
108 8 50       397 open my $fh, $file or die "Could not open '$file': $!";
109 8         18225 <$fh>;
110             }
111             sub _yaml_loadfile {
112 8     8   73 $j->decode(_slurp shift);
113             }
114             sub _yaml_dump {
115 4     4   10880 $j->encode(shift);
116             }
117             }
118              
119             sub _download_overview {
120 4     4   24 my($cts_dir, $distro, %Opt) = @_;
121 4   33     29 my $cturl = $Opt{cturl} ||= $default_cturl;
122 4         18 my $ctarget = "$cts_dir/$distro.json";
123 4         14 my $cheaders = "$cts_dir/$distro.headers";
124 4 50       19 if ($Opt{local}) {
125 4 50       69 unless (-e $ctarget) {
126 0         0 die "Alert: No local file '$ctarget' found, cannot continue\n";
127             }
128             } else {
129 0 0 0     0 if (! -e $ctarget or -M $ctarget > .25) {
130 0 0 0     0 if (-e $ctarget && $Opt{verbose}) {
131 0         0 my(@stat) = stat _;
132 0         0 my $timestamp = gmtime $stat[9];
133 0 0       0 print STDERR "(timestamp $timestamp GMT)\n" unless $Opt{quiet};
134             }
135 0 0 0     0 print STDERR "Fetching $ctarget..." if $Opt{verbose} && !$Opt{quiet};
136 0         0 my $firstletter = substr($distro,0,1);
137 0         0 my $uri = "$cturl/$firstletter/$distro.json";
138 0         0 my $resp = _ua->mirror($uri,$ctarget);
139 0 0       0 if ($resp->is_success) {
    0          
140 0 0 0     0 print STDERR "DONE\n" if $Opt{verbose} && !$Opt{quiet};
141 0 0       0 open my $fh, ">", $cheaders or die;
142 0         0 for ($resp->headers->as_string) {
143 0         0 print $fh $_;
144 0 0 0     0 if ($Opt{verbose} && $Opt{verbose}>1) {
145 0 0       0 print STDERR $_ unless $Opt{quiet};
146             }
147             }
148             } elsif (304 == $resp->code) {
149 0 0 0     0 print STDERR "DONE (not modified)\n" if $Opt{verbose} && !$Opt{quiet};
150 0         0 my $atime = my $mtime = time;
151 0         0 utime $atime, $mtime, $cheaders;
152             } else {
153 0         0 die sprintf
154             (
155             "No success downloading %s: %s",
156             $uri,
157             $resp->status_line,
158             );
159             }
160             }
161             }
162 4         25 return $ctarget;
163             }
164              
165             sub _parse_yaml {
166 4     4   19 my($ctarget, %Opt) = @_;
167 4         17 my $arr = _yaml_loadfile($ctarget);
168 4         23 my($selected_release_ul,$selected_release_distrov,$excuse_string);
169 4 50       18 if ($Opt{vdistro}) {
170 0         0 $excuse_string = "selected distro '$Opt{vdistro}'";
171 0         0 $arr = [grep {$_->{distversion} eq $Opt{vdistro}} @$arr];
  0         0  
172 0         0 ($selected_release_distrov) = $arr->[0]{distversion};
173             } else {
174 4         13 $excuse_string = "any distro";
175 4         11 my $last_addition;
176             my %seen;
177 4         51 for my $report (sort { $a->{id} <=> $b->{id} } @$arr) {
  2992         4566  
178 1396 100       2769 unless ($seen{$report->{distversion}}++) {
179 76         127 $last_addition = $report->{distversion};
180             }
181             }
182 4         16 $arr = [grep {$_->{distversion} eq $last_addition} @$arr];
  1396         3475  
183 4         33 ($selected_release_distrov) = $last_addition;
184             }
185 4 50       17 unless ($selected_release_distrov) {
186 0         0 warn "Warning: could not find $excuse_string in '$ctarget'";
187 0         0 return;
188             }
189 4 50       15 print STDERR "SELECTED: $selected_release_distrov\n" unless $Opt{quiet};
190 4         10 my @all;
191 4         15 for my $test (@$arr) {
192 520         819 my $id = $test->{id};
193             push @all, {
194             id => $test->{id},
195             guid => $test->{guid},
196 520         1180 };
197 520 50       1013 return if $Signal;
198             }
199 4         28 @all = sort { $b->{id} <=> $a->{id} } @all;
  516         746  
200 4         691 return \@all;
201             }
202              
203             sub parse_single_report {
204 369     369 1 2317 my($report, $dumpvars, %Opt) = @_;
205 369         1329 my($id) = $report->{id};
206 369         944 my($guid) = $report->{guid};
207 369   33     1053 $Opt{cachedir} ||= "$ENV{HOME}/var/cpantesters";
208             # the name nntp-testers was picked because originally the reports
209             # were available from an NNTP server
210 369         1157 my $nnt_dir = "$Opt{cachedir}/nntp-testers";
211 369         24379 mkpath $nnt_dir;
212 369         1943 my $target = "$nnt_dir/$id";
213 369 50       1381 if ($Opt{local}) {
214 369 50       6065 unless (-e $target) {
215 0         0 die {severity=>0,text=>"Warning: No local file '$target' found, skipping\n"};
216             }
217             } else {
218 0   0     0 $Opt{transport} ||= $default_transport;
219 0         0 my $ttarget;
220 0 0       0 if (-e $target) {
    0          
221 0         0 $ttarget = $target;
222             } elsif (-e "$target.gz") {
223 0         0 $ttarget = "$target.gz";
224             }
225 0 0       0 if ($ttarget) {
226 0         0 my $raw_report;
227 0 0       0 open my $fh, $ttarget or die "Could not open '$ttarget': $!";
228 0 0       0 if (0) {
    0          
229 0         0 } elsif ($Opt{transport} eq "http_cpantesters") {
230 0         0 local $/;
231 0         0 $raw_report = <$fh>;
232             } elsif ($Opt{transport} eq "http_cpantesters_gzip") {
233 0         0 my $gz = Compress::Zlib::gzopen($fh, "rb");
234 0         0 $raw_report = "";
235 0         0 my $buffer;
236 0         0 while (my $bytesread = $gz->gzread($buffer)) {
237 0         0 $raw_report .= $buffer;
238             }
239             }
240 0 0       0 if ($raw_report =~ m{<title>.*(Report not found|Error).*</title>}) {
241 0 0       0 unlink $ttarget or die "Could not unlink '$ttarget': $!";
242             }
243             }
244 0 0       0 if (! -e $target) {
245 0 0 0     0 print STDERR "Fetching $target..." if $Opt{verbose} && !$Opt{quiet};
246 0 0       0 if (0) {
    0          
247 0         0 } elsif ($Opt{transport} eq "http_cpantesters") {
248 0         0 my $mustfetch = 0;
249 0 0       0 if ($Opt{"prefer-local-reports"}) {
250 0 0       0 unless (-e $target) {
251 0         0 $mustfetch = 1;
252             }
253             } else {
254 0         0 $mustfetch = 1;
255             }
256 0 0       0 if ($mustfetch) {
257 0         0 my $resp = _ua->mirror("http://www.cpantesters.org/cpan/report/$guid?raw=1",$target);
258 0 0       0 if ($resp->is_success) {
259 0 0       0 if ($Opt{verbose}) {
260 0         0 my(@stat) = stat $target;
261 0         0 my $timestamp = gmtime $stat[9];
262 0 0       0 print STDERR "(timestamp $timestamp GMT)\n" unless $Opt{quiet};
263 0 0       0 if ($Opt{verbose} > 1) {
264 0 0       0 print STDERR $resp->headers->as_string unless $Opt{quiet};
265             }
266             }
267 0         0 my $headers = "$target.headers";
268 0 0       0 open my $fh, ">", $headers or die {severity=>1,text=>"Could not open >$headers: $!"};
269 0         0 print $fh $resp->headers->as_string;
270             } else {
271 0         0 die {severity=>0,
272             text=>sprintf "HTTP Server Error[%s] for id[%s] guid[%s]", $resp->status_line, $id, $guid};
273             }
274             }
275             } elsif ($Opt{transport} eq "http_cpantesters_gzip") {
276 0         0 my $mustfetch = 0;
277 0 0       0 if ($Opt{"prefer-local-reports"}) {
278 0 0       0 unless (-e "$target.gz") {
279 0         0 $mustfetch = 1;
280             }
281             } else {
282 0         0 $mustfetch = 1;
283             }
284 0 0       0 if ($mustfetch) {
285 0         0 my $resp = _ua_gzip->mirror("http://www.cpantesters.org/cpan/report/$guid?raw=1","$target.gz");
286 0 0       0 if ($resp->is_success) {
287 0 0       0 if ($Opt{verbose}) {
288 0         0 my(@stat) = stat "$target.gz";
289 0         0 my $timestamp = gmtime $stat[9];
290 0 0       0 print STDERR "(timestamp $timestamp GMT)\n" unless $Opt{quiet};
291 0 0       0 if ($Opt{verbose} > 1) {
292 0 0       0 print STDERR $resp->headers->as_string unless $Opt{quiet};
293             }
294             }
295 0         0 my $headers = "$target.headers";
296 0 0       0 open my $fh, ">", $headers or die {severity=>1,text=>"Could not open >$headers: $!"};
297 0         0 print $fh $resp->headers->as_string;
298             } else {
299 0         0 die {severity=>0,
300             text=>sprintf "HTTP Server Error[%s] for id[%s] guid[%s]", $resp->status_line, $id, $guid};
301             }
302             }
303             } else {
304 0         0 die {severity=>1,text=>"Illegal value for --transport: '$Opt{transport}'"};
305             }
306             }
307             }
308 369         2412 parse_report($target, $dumpvars, %Opt);
309             }
310              
311             sub parse_distro {
312 4     4 1 10231 my($distro,%Opt) = @_;
313 4         11 my %dumpvars;
314 4   33     19 $Opt{cachedir} ||= "$ENV{HOME}/var/cpantesters";
315             # the name cpantesters-show was picked because originally
316             # http://www.cpantesters.org/show/ contained html file that we had
317             # to parse.
318 4         16 my $cts_dir = "$Opt{cachedir}/cpantesters-show";
319 4         236 mkpath $cts_dir;
320 4 50       25 if ($Opt{solve}) {
321 0         0 require Statistics::Regression;
322 0 0       0 $Opt{dumpvars} = "." unless defined $Opt{dumpvars};
323             }
324 4 50 33     39 if (!$Opt{vdistro} && $distro =~ /^(.+)-(?i:v?\d+)(?:\.\d+)*\w*$/) {
325 0         0 $Opt{vdistro} = $distro;
326 0         0 $distro = $1;
327             }
328 4         10 my $reports;
329 4 50       16 if (my $ctdb = $Opt{ctdb}) {
330 0         0 require CPAN::WWW::Testers::Generator::Database;
331 0         0 require CPAN::DistnameInfo;
332 0 0       0 my $dbi = CPAN::WWW::Testers::Generator::Database->new(database=>$ctdb) or die "Alert: unknown error while opening database '$ctdb'";
333 0 0       0 unless ($Opt{vdistro}) {
334 0         0 my $sql = "select version from cpanstats where dist=? order by id";
335 0         0 my @rows = $dbi->get_query($sql,$distro);
336 0         0 my($newest,%seen);
337 0         0 for my $row (@rows) {
338 0 0       0 $newest = $row->[0] unless $seen{$row->[0]}++;
339             }
340 0         0 $Opt{vdistro} = "$distro-$newest";
341             }
342 0         0 my $d = CPAN::DistnameInfo->new("FOO/$Opt{vdistro}.tgz");
343 0         0 my $dist = $d->dist;
344 0         0 my $version = $d->version;
345 0         0 my $sql = "select id, guid from cpanstats where dist=? and version=? order by id desc";
346 0         0 my @rows = $dbi->get_query($sql,$dist,$version);
347 0         0 my @all;
348 0         0 for my $row (@rows) {
349 0         0 push @all, {
350             id => $row->[0],
351             guid => $row->[1],
352             };
353             }
354 0         0 $reports = \@all;
355             } else {
356 4         29 my $ctarget = _download_overview($cts_dir, $distro, %Opt);
357 4         25 $reports = _parse_yaml($ctarget,%Opt);
358             }
359 4 50       26 return unless $reports;
360 4         16 my $sampled = 0;
361 4   100     17 my $samplesize = $Opt{sample} || 0;
362 4 100 100     32 $samplesize = 0 if $samplesize && $samplesize >= @$reports;
363             REPEATER: {
364 4         8 my $i = 0;
  13         33  
365 13         27 my %taken;
366 13         35 REPORT: for my $report (@$reports) {
367 1221         1947 $i++;
368 1221 100       2424 if ($samplesize) {
369 961         1376 my $need = $samplesize - $sampled;
370 961 100       1818 next REPORT unless $need;
371 777         1153 my $left = @$reports - $i;
372             # warn sprintf "tot %d i %d sampled %d need %d left %d\n", scalar @$reports, $i, $sampled, $need, $left;
373 777         1734 my $want_this = (rand(1) <= ($need/$left));
374 777 100       2031 next REPORT unless $want_this;
375             }
376 369         836 eval {parse_single_report($report, \%dumpvars, %Opt)};
  369         1948  
377 369 50       8304 if ($@) {
378 0 0       0 if (ref $@) {
379 0 0       0 if ($@->{severity}) {
380 0         0 die $@->{text};
381             } else {
382 0         0 warn $@->{text};
383             }
384             } else {
385 0         0 die $@;
386             }
387             }
388 369         912 $sampled++;
389 369         1942 $taken{$i-1}=undef;
390 369 50       1500 last REPEATER if $Signal;
391             }
392 13 100       190 if ($samplesize) {
393 11         42 PASSFAIL: for my $pf ("pass","fail") {
394 22 100       128 my $minx = $Opt{"min".$pf} or next PASSFAIL;
395 10         54 my $x = $dumpvars{"meta:ok"}{uc $pf}{uc $pf};
396 10 100       68 if ($x < $minx) {
397             # bump samplesize, remove already sampled reports from array, redo
398 9         49 my $bump = int($samplesize * 0.05)+1;
399 9         28 $samplesize += $bump;
400 9         132 for my $k (sort {$b <=> $a} keys %taken) {
  187         334  
401 65         221 splice @$reports, $k, 1;
402             }
403 9         54 redo REPEATER;
404             }
405             }
406             }
407             }
408 4 50       25 if ($Opt{dumpvars}) {
409 4   50     40 my $dumpfile = $Opt{dumpfile} || "ctgetreports.out";
410 4 50       776 open my $fh, ">", $dumpfile or die "Could not open '$dumpfile' for writing: $!";
411 4         44 print $fh _yaml_dump(\%dumpvars);
412 4 50       425 close $fh or die "Could not close '$dumpfile': $!"
413             }
414 4 50       2957 if ($Opt{solve}) {
415 0         0 solve(\%dumpvars,%Opt);
416             }
417             }
418              
419             =head2 $bool = _looks_like_qp($raw_report)
420              
421             We had to acknowledge the fact that some MTAs swallow the MIME-Version
422             header while passing MIME through. So we introduce fallback heuristics
423             that try to determine if a report is written in quoted printable.
424              
425             Note that this subroutine is internal, just documented to have the
426             internals documented.
427              
428             The current implementation counts the number of QP escaped spaces and
429             equal signs.
430              
431             =cut
432              
433             sub _looks_like_qp {
434 14     14   143 my($report) = @_;
435 14         151 my $count_space = () = $report =~ /=20/g;
436 14 100       134 return 1 if $count_space > 12;
437 13         68 my $count_equal = () = $report =~ /=3D/g;
438 13 50       39 return 1 if $count_equal > 12;
439 13 50       56 return 1 if $count_space+$count_equal > 24;
440 13         78 return 0; # waiting for a counter example
441             }
442              
443             =head2 $extract = parse_report($target,$dumpvars,%Opt)
444              
445             Reads one report. $target is the local filename to read (but see below
446             for option 'article'). $dumpvars is a hashref which gets filled with
447             descriptive stats about PASS/FAIL/etc. %Opt are the options as
448             described in the C<ctgetreports> manpage. $extract is a hashref
449             containing the found variables.
450              
451             Note: this parsing is a bit dirty but as it seems good enough I'm not
452             inclined to change it. We parse HTML with regexps only, not an HTML
453             parser. Only the entities are decoded.
454              
455             In %Opt you can use
456              
457             article => $some_full_article_as_scalar
458              
459             to use this function to parse one full article as text. When this is
460             given, the argument $target is not read, but its basename is taken to
461             be the id of the article. (OMG, hackers!)
462              
463             =cut
464             sub parse_report {
465 391     391 1 5363460 my($target,$dumpvars,%Opt) = @_;
466 391         853 our @q;
467 391         18939 my $id = basename($target);
468             # warn "DEBUG: id[$id]";
469 391         1615 my($ok,$about);
470              
471 391         0 my(%extract);
472              
473 391         1615 my($report,$isHTML) = _get_cooked_report($target, \%Opt);
474 391         1022 my @qr = map /^qr:(.+)/, @{$Opt{q}};
  391         2973  
475 391 100 66     3057 if ($Opt{raw} || @qr) {
476 131         364 for my $qr (@qr) {
477 131         15930 my $cqr = eval "qr{$qr}";
478 131 50       833 die "Could not compile regular expression '$qr': $@" if $@;
479 131         1629 my(@matches) = $report =~ $cqr;
480 131         273 my $v;
481 131 100       390 if (@matches) {
482 2 50       8 if (@matches==1) {
483 2         10 $v = $matches[0];
484             } else {
485 0         0 $v = join "", map {"($_)"} @matches;
  0         0  
486             }
487             } else {
488 129         386 $v = "";
489             }
490 131         698 $extract{"qr:$qr"} = $v;
491             }
492             }
493              
494 391         996 my $report_writer;
495 391         1018 my $moduleunpack = {};
496 391         841 my $expect_prereq = 0;
497 391         837 my $expect_toolchain = 0;
498 391         772 my $expecting_toolchain_soon = 0;
499 391         815 my $expect_module_versions_report = 0;
500 391         714 my $expect_characteristics_libperl = 0;
501 391         901 my $fallback_p5 = "";
502              
503 391         841 my $in_summary = 0;
504 391         707 my $in_summary_seen_platform = 0;
505 391         701 my $in_prg_output = 0;
506 391         668 my $in_env_context = 0;
507 391         713 my $in_test_summary = 0;
508 391         664 my $in_characteristics = 0;
509              
510 391         675 my $current_headline;
511 391         886 my @previous_line = ""; # so we can neutralize line breaks
512 391         70341 my @rlines = split /\r?\n/, $report;
513 391         1598 LINE: for (@rlines) {
514 3000 100 100     12882 next LINE unless ($isHTML ? m/<title>((\S+)\s+(\S+))/ : m/^Subject:\s*((\S+)\s+(\S+))/)
    100          
515             || m{^Subject:\s*<strong>((\S+)\s+(\S+))};
516 391         1463 my $s = $1;
517 391 100       1404 $s = $1 if $s =~ m{<strong>(.+)};
518 391 50       2015 if ($s =~ /(\S+)\s+(\S+)/) {
519 391         1171 $ok = $1;
520 391         1035 $about = $2;
521             }
522 391         1440 $extract{"meta:ok"} = $ok;
523 391         987 $extract{"meta:about"} = $about;
524 391         938 last;
525             }
526 391 50       1013 unless ($extract{"meta:about"}) {
527 0         0 $extract{"meta:about"} = $Opt{vdistro};
528 0 0       0 unless ($extract{"meta:ok"}) {
529 0         0 warn "Warning: could not determine state of report";
530             }
531             }
532 391         968 LINE: while (@rlines) {
533 106940         195634 $_ = shift @rlines;
534 106940   66     242498 while (/!$/ and @rlines) {
535 260         799 my $followupline = shift @rlines;
536 260         933 $followupline =~ s/^\s+//; # remo leading space
537 260         1432 $_ .= $followupline;
538             }
539 106940 100       197142 if (/^--------/) {
540 3081 100 100     13138 if ($previous_line[-2] && $previous_line[-2] =~ /^--------/) {
    100 100        
541 1448         2519 $current_headline = $previous_line[-1];
542 1448 100       3473 if ($current_headline =~ /PROGRAM OUTPUT/) {
543 355         624 $in_prg_output = 1;
544             } else {
545 1093         1737 $in_prg_output = 0;
546             }
547 1448 100       3178 if ($current_headline =~ /ENVIRONMENT AND OTHER CONTEXT/) {
548 387         723 $in_env_context = 1;
549             } else {
550 1061         1779 $in_env_context = 0;
551             }
552             } elsif ($previous_line[-1] && $previous_line[-1] =~ /Test Summary Report/) {
553 132         291 $in_test_summary = 1;
554 132         258 $in_prg_output = 0;
555             }
556             }
557 106940 100       191279 if ($extract{"meta:perl"}) {
558 45882 100 66     182408 if ( $in_summary
      100        
559             and !$extract{"conf:git_commit_id"}
560             and /Commit id:\s*([[:xdigit:]]+)/) {
561 2         8 $extract{"conf:git_commit_id"} = $1;
562             }
563             } else {
564 61058         84539 my $p5;
565 61058 100       108470 if (0) {
566 0         0 } elsif (/Summary of my perl5 \((.+)\) configuration:/) {
567 390         1258 $p5 = $1;
568 390         757 $in_summary = 1;
569 390         716 $in_env_context = 0;
570             }
571 61058 100       106465 if ($p5) {
572 390         805 my($r,$v,$s,$p);
573 390 100       3331 if (($r,$v,$s,$p) = $p5 =~ /revision (\S+) version (\S+) subversion (\S+) patch (\S+)/) {
    100          
    50          
574 224         540 $r =~ s/\.0//; # 5.0 6 2!
575 224         942 $extract{"meta:perl"} = "$r.$v.$s\@$p";
576             } elsif (($r,$v,$s) = $p5 =~ /revision (\S+) version (\S+) subversion (\S+)/) {
577 160         474 $r =~ s/\.0//;
578 160         626 $extract{"meta:perl"} = "$r.$v.$s";
579             } elsif (($r,$v,$s) = $p5 =~ /(\d+\S*) patchlevel (\S+) subversion (\S+)/) {
580 6         25 $r =~ s/\.0//;
581 6         29 $extract{"meta:perl"} = "$r.$v.$s";
582             } else {
583 0         0 $extract{"meta:perl"} = $p5;
584             }
585             }
586             }
587 106940 100       196586 unless ($extract{"meta:from"}) {
588 13316 100       44525 if (0) {
589 0 100       0 } elsif ($isHTML ?
    100          
590             m|<div class="h_name">From:</div> <b>(.+?)</b><br/>| :
591             m|^From:\s*(.+)|
592             or
593             m|^From:\s*(.+)|
594             ) {
595 391         1295 my $f = $1;
596 391 100       1112 $f = $1 if $f =~ m{<strong>(.+)</strong>};
597 391         1396 $extract{"meta:from"} = $f;
598             }
599 13316 100       24410 $extract{"meta:from"} =~ s/\.$// if $extract{"meta:from"};
600             }
601 106940 100       186251 unless ($extract{"meta:date"}) {
602 13577 100       43173 if (0) {
603 0 100       0 } elsif ($isHTML ?
    100          
604             m|<div class="h_name">Date:</div> (.+?)<br/>| :
605             m|^Date:\s*(.+)|
606             or
607             m|^Date:\s*(.+)|
608             ) {
609 391         1004 my $date = $1;
610 391 100       1093 $date = $1 if $date =~ m{<strong>(.+)</strong>};
611 391         884 my($dt);
612 391         952 DATEFMT: for my $pat ("%Y-%m-%dT%TZ", # 2010-07-07T14:01:40Z
613             "%a, %d %b %Y %T %z", # Sun, 28 Sep 2008 12:23:12 +0100
614             "%b %d, %Y %R", # July 10,...
615             "%b %d, %Y %R", # July 4,...
616             ) {
617 1143         2424 $dt = eval {
618 1143         8150 my $p = DateTime::Format::Strptime->new
619             (
620             locale => "en",
621             time_zone => "UTC",
622             pattern => $pat,
623             );
624 1143         1843133 $p->parse_datetime($date)
625             };
626 1143 100       448970 last DATEFMT if $dt;
627             }
628 391 50       3292 unless ($dt) {
629 0         0 warn "Could not parse date[$date], setting to epoch 0";
630 0         0 $dt = DateTime->from_epoch( epoch => 0 );
631             }
632 391         3155 $extract{"meta:date"} = $dt->datetime;
633             }
634 13577 100       40840 $extract{"meta:date"} =~ s/\.$// if $extract{"meta:date"};
635             }
636 106940 100       187318 unless ($extract{"meta:writer"}) {
637 19300         46518 for ("$previous_line[-1] $_") {
638 19300 100       66272 if (0) {
    50          
    100          
    100          
639 0         0 } elsif (/CPANPLUS, version (\S+)/) {
640 10         43 $extract{"meta:writer"} = "CPANPLUS $1";
641             } elsif (/created by (App::cpanminus::reporter \S+)/) {
642 0         0 $extract{"meta:writer"} = $1;
643             } elsif (/created (?:automatically )?by (\S+)/) {
644 352         1523 $extract{"meta:writer"} = $1;
645 352 50       2731 if (/\s+on\s+perl\s+([^,]+),/) {
646 352         991 $fallback_p5 = $1;
647             }
648             } elsif (/This report was machine-generated by (\S+) (\S+)/) {
649 29         171 $extract{"meta:writer"} = "$1 $2";
650             }
651 19300 100       46268 $extract{"meta:writer"} =~ s/[\.,]$// if $extract{"meta:writer"};
652             }
653             }
654 106940 100       189063 if ($in_summary) {
655             # we do that first three lines a bit too often
656 46272   100     91726 my $qr = $Opt{dumpvars} || "";
657 46272 100       149280 $qr = qr/$qr/ if $qr;
658 46272 100       99243 unless (@q) {
659 1 50       2 @q = @{$Opt{q}||[]};
  1         5  
660 1 50       8 @q = qw(meta:perl conf:archname conf:usethreads conf:optimize meta:writer meta:from) unless @q;
661             }
662              
663 46272         78162 my %conf_vars = map {($_ => 1)} grep { /^conf:/ } @q;
  138816         276736  
  277632         597763  
664              
665 46272 100 100     242802 if (/^\s+Platform:$/) {
    100 100        
    100          
    100          
666 390         1263 $in_summary_seen_platform=1;
667             } elsif (/^\s*$/ || m|</pre>|) {
668             # if not html, we have reached the end now
669 17771 100       39763 if ($in_characteristics) {
    100          
670 1         3 $in_summary = 0;
671             } elsif ($in_summary_seen_platform) {
672             # some perls have an empty line after the summary line
673 17761         40141 $expect_characteristics_libperl = 1;
674             }
675             } elsif ($in_characteristics) {
676 242 100       856 if (my($date) = /Compiled at (.+)/) {
677 7         31 $date =~ s/\s+\z//;
678             # find: Apr 10 2013 16:59:47
679             # want: 2016-07-05T11:03:04
680 7         13 my($dt);
681 7         23 DATEFMT: for my $pat ("%b %d %Y %T") { # Sep 28 2008 12:23:12
682 7         19 $dt = eval {
683 7         40 my $p = DateTime::Format::Strptime->new
684             (
685             locale => "en",
686             time_zone => "UTC",
687             pattern => $pat,
688             );
689 7         10842 $p->parse_datetime($date)
690             };
691 7 50       5711 last DATEFMT if $dt;
692             }
693 7 50       63 unless ($dt) {
694 0         0 warn "Could not parse date[$date], setting to epoch 0";
695 0         0 $dt = DateTime->from_epoch( epoch => 0 );
696             }
697 7         52 $extract{"meta:perl_compiled_at"} = $dt->datetime;
698             }
699             } elsif ($expect_characteristics_libperl && /Characteristics of this/) {
700 7         22 $in_characteristics = 1;
701             } else {
702 27862         193862 my(%kv) = m!\G,?\s*([^=]+)= # left hand side and equal sign
703             (
704             [^',\s]+(?=.+=) # use64bitint=define use64bitall=define uselongdouble=undef
705             # (lookahead needed for left-over equal sign)
706             |
707             [^',]+$ # libpth=/usr/lib /usr/local/lib
708             |
709             '[^']+?' # cccdlflags='-DPIC -fPIC'
710             |
711             \S+ # useshrplib=false
712             )!xgc;
713 27862         100793 while (my($k,$v) = each %kv) {
714 32573         75461 my $ck = "conf:$k";
715 32573         69802 $ck =~ s/\s+$//;
716 32573         54004 $v =~ s/,$//;
717 32573 100       71813 if ($v =~ /^'(.*)'$/) {
718 6964         17200 $v = $1;
719             }
720 32573         62394 $v =~ s/^\s+//;
721 32573         58521 $v =~ s/\s+$//;
722 32573 100 66     156006 if ($qr && $ck =~ $qr) {
    100          
723 32455         161218 $extract{$ck} = $v;
724             } elsif ($conf_vars{$ck}) {
725 4         19 $extract{$ck} = $v;
726             }
727             }
728             }
729             }
730 106940 100       195391 if ($in_prg_output) {
731 8738 100       15653 unless ($extract{"meta:output_from"}) {
732 1056 100       3430 if (/Output from (.+):$/) {
733 352         1337 $extract{"meta:output_from"} = $1
734             }
735             }
736              
737             # Parsing of Module::Versions::Report text in test output
738 8738 100       19369 if (/Modules in memory:/) {
    100          
739 1         3 $expect_module_versions_report = 1;
740 1         3 next LINE;
741             }
742             elsif ($expect_module_versions_report) {
743 8 100       38 if (/\s+(\S+)(?:\s+(v\d\S+?))?;/) {
    50          
744 7 100       30 $extract{"mod:$1"} = defined $2 ? $2 : 'undef';
745 7         18 next LINE;
746             }
747             elsif (/\[at .+?\]/) {
748             # trailing timestamp
749 1         3 $expect_module_versions_report = 0;
750 1         8 next LINE;
751             }
752             }
753             }
754 106931 100       185195 if ($in_env_context) {
755 9122 100 100     27916 if ($extract{"meta:writer"} =~ /^CPANPLUS\b/
756             ||
757             exists $extract{"env:PERL5_CPANPLUS_IS_VERSION"}
758             ) {
759             (
760 772 100 100     4648 s/Perl:\s+\$\^X/\$^X/
      100        
      100        
761             ||
762             s/EUID:\s+\$>/\$EUID/
763             ||
764             s/UID:\s+\$</\$UID/
765             ||
766             s/EGID:\s+\$\)/\$EGID/
767             ||
768             s/GID:\s+\$\(/\$GID/
769             )
770             }
771 9122 100       36337 if (my($left,$right) = /^\s{4}(\S+)\s*=\s*(.*)$/) {
772 5400 100       14619 if ($left eq '$UID/$EUID') {
    100          
773 351         1979 my($uid,$euid) = split m{\s*/\s*}, $right;
774 351         1007 $extract{'env:$UID'} = $uid;
775 351         880 $extract{'env:$EUID'} = $euid;
776             } elsif ($left =~ /GID/) {
777 774         4997 for my $xgid (uniq split " ", $right) {
778 2174         7675 $extract{"env:$left∋$xgid"} = "true";
779             }
780             } else {
781 4275         12754 $extract{"env:$left"} = $right;
782             }
783             }
784             }
785 106931 100       183993 if ($in_test_summary) {
786 720 100       3586 if (/^(?:Result:|Files=\d)/) {
    100          
    100          
787 132         257 $in_test_summary = 0;
788             } elsif (/^(\S+)\s+\(Wstat:.+?Tests:.+?Failed:\s*(\d+)\)$/) {
789 151         424 my $in_test_summary_current_test = $1; # t/globtest.t or t\globtest.t
790 151         317 my $in_test_summary_current_failed = $2;
791 151         397 $in_test_summary_current_test =~ s|\\|/|g; # only t/globtest.t
792 151         759 $extract{"fail:$in_test_summary_current_test"} = $in_test_summary_current_failed;
793             } elsif (/^\s+Failed tests?:/) {
794             # ignoring the exact combination of tests for now, seems like overkill
795             }
796             }
797 106931         191280 push @previous_line, $_;
798 106931 100 100     289757 if ($expect_prereq || $expect_toolchain) {
799 10980 100       20052 if (/Perl module toolchain versions installed/) {
800             # first time discovered in CPANPLUS 0.89_06
801 11         23 $expecting_toolchain_soon = 1;
802 11         31 $expect_prereq=0;
803 11         38 next LINE;
804             }
805 10969 100       20871 if (exists $moduleunpack->{type}) {
806 8298         13350 my($module,$v,$needwant);
807             # type 1 and 2 are about prereqs, type three about toolchain
808 8298 100       21557 if ($moduleunpack->{type} == 1) {
    100          
    50          
809 1776         2882 (my $leader,$module,$needwant,$v) = eval { unpack $moduleunpack->{tpl}, $_; };
  1776         7459  
810 1776 50       4050 next LINE if $@;
811 1776 100       6964 if ($leader =~ /^-/) {
    100          
    100          
812 347         1165 $moduleunpack = {};
813 347         589 $expect_prereq = 0;
814 347         1078 next LINE;
815             } elsif ($leader =~ /^(
816             buil # build_requires:
817             |conf # configure_requires:
818             )/x) {
819 5         16 next LINE;
820             } elsif ($module =~ /^(
821             - # line drawing
822             )/x) {
823 352         1416 next LINE;
824             }
825             } elsif ($moduleunpack->{type} == 2) {
826 90         147 (my $leader,$module,$v,$needwant) = eval { unpack $moduleunpack->{tpl}, $_; };
  90         367  
827 90 50       209 next LINE if $@;
828 90         189 for ($module,$v,$needwant) {
829 270         615 s/^\s+//;
830 270         704 s/\s+$//;
831             }
832 90 50 33     625 if ($leader =~ /^\*/) {
    100 66        
      100        
833 0         0 $moduleunpack = {};
834 0         0 $expect_prereq = 0;
835 0         0 next LINE;
836             } elsif (!defined $v
837             or !defined $needwant
838             or $v =~ /\s/
839             or $needwant =~ /\s/
840             ) {
841 4         31 ($module,$v,$needwant) = split " ", $_;
842             }
843             } elsif ($moduleunpack->{type} == 3) {
844 6432         10050 (my $leader,$module,$v) = eval { unpack $moduleunpack->{tpl}, $_; };
  6432         21497  
845 6432 50       13171 next LINE if $@;
846 6432 100       16084 if (!$module) {
    100          
847 358         868 $moduleunpack = {};
848 358         595 $expect_toolchain = 0;
849 358         1035 next LINE;
850             } elsif ($module =~ /^-/) {
851 351         1245 next LINE;
852             }
853             }
854 6885         23019 $module =~ s/\s+$//;
855 6885 100       14924 if ($module) {
856 6515         15713 $v =~ s/^\s+//;
857 6515         15738 $v =~ s/\s+$//;
858 6515         16836 my($modulename,$versionlead) = split " ", $module;
859 6515 100 66     21256 if (defined $modulename and defined $versionlead) {
860 26         51 $module = $modulename;
861 26         58 $v = "$versionlead$v";
862             }
863 6515 100       12742 if ($v eq "Have") {
864 5         15 next LINE;
865             }
866 6510         18729 $extract{"mod:$module"} = $v;
867 6510 100       14083 if (defined $needwant) {
868 787         1838 $needwant =~ s/^\s+//;
869 787         2156 $needwant =~ s/\s+$//;
870 787         2894 $extract{"prereq:$module"} = $needwant;
871             }
872             }
873             }
874 9551 100       28205 if (/(\s+)(Module\s+)(Need\s+)Have/) {
    100          
875 347         701 $in_env_context = 0;
876 347         3028 $moduleunpack = {
877             tpl => 'a'.length($1).'a'.length($2).'a'.length($3).'a*',
878             type => 1,
879             };
880             } elsif (/(\s+)(Module Name\s+)(Have)(\s+)Want/) {
881 7         18 $in_env_context = 0;
882 7         25 my $adjust_1 = 0;
883 7         32 my $adjust_2 = -length($4);
884 7         22 my $adjust_3 = length($4);
885             # I think they do not really try to align, usually we
886             # get away with split
887 7         71 $moduleunpack = {
888             tpl => 'a'.length($1).'a'.(length($2)+$adjust_2).'a'.(length($3)+$adjust_3).'a*',
889             type => 2,
890             };
891             }
892             }
893 105502 100       212332 if (/PREREQUISITES|Prerequisite modules loaded/) {
894 713         1361 $in_env_context = 0;
895 713         1153 $expect_prereq=1;
896             }
897 105502 100       186336 if ($expecting_toolchain_soon) {
898 709 100       2850 if (/(\s+)(Module(?:\sName)?\s+) Have/) {
899 358         588 $in_env_context = 0;
900 358         663 $expect_toolchain=1;
901 358         652 $expecting_toolchain_soon=0;
902 358         2230 $moduleunpack = {
903             tpl => 'a'.length($1).'a'.length($2).'a*',
904             type => 3,
905             };
906             }
907             }
908 105502 100       250236 if (/toolchain versions installed/) {
909 347         790 $in_env_context = 0;
910 347         847 $expecting_toolchain_soon=1;
911             }
912             } # LINE
913 391 100 100     2158 if (! $extract{"mod:CPANPLUS"} && $extract{"meta:writer"} =~ /^CPANPLUS\s(\d+(\.\d+))$/) {
914 1         5 $extract{"mod:CPANPLUS"} = $1;
915             }
916 391 100 66     1279 if (! $extract{"meta:perl"} && $fallback_p5) {
917 1         6 my($p5,$patch) = split /\s+patch\s+/, $fallback_p5;
918 1         4 $extract{"meta:perl"} = $p5;
919 1 50       4 $extract{"conf:git_describe"} = $patch if defined $patch;
920             }
921 391         1136 $extract{id} = $id;
922 391 50       1197 if (my $filtercbbody = $Opt{filtercb}) {
923 0         0 my $filtercb = eval('sub {'.$filtercbbody.'}');
924 0         0 $filtercb->(\%extract);
925             }
926 391 100       1072 if ($Opt{solve}) {
927 1 0 33     5 if ($extract{"conf:osvers"} && $extract{"conf:archname"}) {
928 0         0 $extract{"conf:archname+osvers"} = join " ", @extract{"conf:archname","conf:osvers"};
929             }
930 1 50 33     9 if ($extract{"meta:perl"} && $extract{"conf:osname"}) {
931 0         0 $extract{"meta:osname+perl"} = join " ", @extract{"conf:osname","meta:perl"};
932             }
933 1   50     7 my $data = $dumpvars->{"==DATA=="} ||= [];
934 1         5 push @$data, \%extract;
935             }
936             # ---- %extract finished ----
937 391         974 my $diag = "";
938 391 100       1190 if (my $qr = $Opt{dumpvars}) {
939 389         1541 $qr = qr/$qr/;
940 389         3472 while (my($k,$v) = each %extract) {
941 48986 50       146011 if ($k =~ $qr) {
942 48986         210929 $dumpvars->{$k}{$v}{$extract{"meta:ok"}}++;
943             }
944             }
945             }
946 391         1256 for my $want (@q) {
947 2346   100     5741 my $have = $extract{$want} || "";
948 2346         5758 $diag .= " $want\[$have]";
949             }
950 391 50       1606 printf STDERR " %-4s %8s%s\n", $extract{"meta:ok"}, $id, $diag unless $Opt{quiet};
951 391 50       1179 if ($Opt{raw}) {
952 0         0 $report =~ s/\s+\z//;
953 0 0       0 print STDERR $report, "\n================\n" unless $Opt{quiet};
954             }
955 391 50       1108 if ($Opt{interactive}) {
956 0 0       0 eval { require IO::Prompt; 1; } or
  0         0  
  0         0  
957             die "Option '--interactive' requires IO::Prompt installed";
958 0         0 local @ARGV;
959 0         0 local $ARGV;
960 0         0 my $ans = IO::Prompt::prompt
961             (
962             -p => "View $id? [onechar: ynq] ",
963             -d => "y",
964             -u => qr/[ynq]/,
965             -onechar,
966             );
967 0 0       0 print STDERR "\n" unless $Opt{quiet};
968 0 0       0 if ($ans eq "y") {
    0          
969 0         0 my($report) = _get_cooked_report($target, \%Opt);
970 0   0     0 $Opt{pager} ||= "less";
971 0 0       0 open my $lfh, "|-", $Opt{pager} or die "Could not fork '$Opt{pager}': $!";
972 0         0 local $/;
973 0         0 print {$lfh} $report;
  0         0  
974 0 0       0 close $lfh or die "Could not close pager: $!"
975             } elsif ($ans eq "q") {
976 0         0 $Signal++;
977 0         0 return;
978             }
979             }
980 391         20950 return \%extract;
981             }
982              
983             sub _get_cooked_report {
984 391     391   1080 my($target, $Opt_ref) = @_;
985 391         977 my($report, $isHTML);
986 391 100       1605 if ($report = $Opt_ref->{article}) {
987 1         4 $isHTML = $report =~ /^</;
988 1         2 undef $target;
989             }
990 391 100       1239 if ($target) {
991 390         2085 local $/;
992 390         733 my $raw_report;
993 390 100       6231 if (0) {
    50          
994 0         0 } elsif (-e $target) {
995 387 50       22100 open my $fh, '<', $target or die "Could not open '$target': $!";
996 387         28792 $raw_report = <$fh>;
997             } elsif (-e "$target.gz") {
998 3 50       172 open my $fh, "<", "$target.gz" or die "Could not open '$target.gz': $!";
999              
1000             # Opens a gzip (.gz) file for reading or writing. The mode parameter
1001             # is as in fopen ("rb" or "wb") but can also include a compression level
1002             # ("wb9") or a strategy: 'f' for filtered data as in "wb6f", 'h' for
1003             # Huffman only compression as in "wb1h", or 'R' for run-length encoding
1004             # as in "wb1R". (See the description of deflateInit2 for more information
1005             # about the strategy parameter.)
1006              
1007 3         35 my $gz = Compress::Zlib::gzopen($fh, "rb");
1008 3         7543 $raw_report = "";
1009 3         9 my $buffer;
1010 3         15 while (my $bytesread = $gz->gzread($buffer)) {
1011 23         8033 $raw_report .= $buffer;
1012             }
1013             } else {
1014 0         0 die "Could not find '$target' or '$target.gz'";
1015             }
1016 390         4044 $isHTML = $raw_report =~ /^</;
1017 390 100       1505 if ($isHTML) {
1018 374 100       2825 if ($raw_report =~ m{^<\?.+?<html.+?<head.+?<body.+?<pre[^>]*>(.+)</pre>.*</body>.*</html>}s) {
1019 5         199 $raw_report = decode_entities($1);
1020 5         18 $isHTML = 0;
1021             }
1022             }
1023 390 100 100     1444 if ($isHTML) {
    100          
1024 369         11045 $report = decode_entities($raw_report);
1025             } elsif ($raw_report =~ /^MIME-Version: 1.0$/m
1026             ||
1027             _looks_like_qp($raw_report)
1028             ) {
1029             # note(1): minimizing MIME effort; don't know about reports in other formats
1030             # note(2): Net-Generatus-0.40 had an offending report
1031 8         29 $report = eval { MIME::QuotedPrint::decode_qp($raw_report) };
  8         877  
1032 8 50 33     97 if (!$report || $@) {
1033 0         0 warn "WARNING: report '$target' could not be parsed as qp, giving up";
1034 0 0       0 if ($raw_report =~ /Subject:.+Dear.+Perl.+Summary/s) {
1035 0         0 $report = $raw_report;
1036             }
1037             }
1038             } else {
1039 13         206 $report = $raw_report;
1040             }
1041             }
1042 391 100       2400 if ($report =~ /\r\n/) {
1043 1         175 my @rlines = split /\r?\n/, $report;
1044 1         45 $report = join "\n", @rlines;
1045             }
1046 391         2414 ($report, $isHTML);
1047             }
1048              
1049             =head2 solve
1050              
1051             Feeds a couple of potentially interesting data to
1052             Statistics::Regression and sorts the result by R^2 descending. Do not
1053             confuse this with a prove, rather take it as a useful hint. It can
1054             save you minutes of staring at data and provide a quick overview where
1055             one should look closer. Displays the N top candidates, where N
1056             defaults to 3 and can be set with the C<$Opt{solvetop}> variable.
1057             Regressions results with an R^2 of 1.00 are displayed in any case.
1058              
1059             The function is called when the option C<-solve> is given on the
1060             commandline. Several extra config variables are calculated, see source
1061             code for details.
1062              
1063             =cut
1064             {
1065             my %never_solve_on = map {($_ => 1)}
1066             (
1067             'conf:ccflags',
1068             'conf:config_args',
1069             'conf:cppflags',
1070             'conf:lddlflags',
1071             'conf:uname',
1072             'conf:osvers',
1073             'env:$^X',
1074             'env:PATH',
1075             'env:PERL',
1076             'env:PERL5LIB',
1077             'env:PERL5OPT',
1078             'env:PERL5_CPANPLUS_IS_RUNNING',
1079             'env:PERL5_CPAN_IS_RUNNING',
1080             'env:PERL5_CPAN_IS_RUNNING_IN_RECURSION',
1081             'env:PERL5_YACSMOKE_BASE',
1082             'env:PERLBREW_MANPATH',
1083             'env:PERLBREW_PATH',
1084             'env:PERLBREW_PERL',
1085             'env:PERL_CPAN_REPORTER_CONFIG',
1086             'env:PERL_CPAN_REPORTER_DIR',
1087             'meta:ok',
1088             'meta:perl_compiled_at',
1089             );
1090             my %normalize_numeric =
1091             (
1092             id => sub { return shift },
1093              
1094             # here we were treating date as numeric; current
1095             # implementation requires to decide for one normalization, so
1096             # we decided 2012-02 for a sampling focussing on recentness;
1097              
1098             #'meta:date' => sub {
1099             # my $v = shift;
1100             # my($Y,$M,$D,$h,$m,$s) = $v =~ /(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)/;
1101             # unless (defined $M) {
1102             # die "illegal value[$v] for a date";
1103             # }
1104             # Time::Local::timegm($s,$m,$h,$D,$M-1,$Y);
1105             #},
1106             );
1107             my %normalize_value =
1108             (
1109             'meta:perl' => sub {
1110             my($perlatpatchlevel) = shift;
1111             my $perl = $perlatpatchlevel;
1112             $perl =~ s/\@.*//;
1113             $perl;
1114             },
1115             'meta:date' => sub {
1116             my $v = shift;
1117             my($Y,$M,$D,$h,$m,$s) = $v =~ /(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)/;
1118             unless (defined $M) {
1119             die "illegal value[$v] for a date";
1120             }
1121             my $epoch = Time::Local::timegm($s,$m,$h,$D,$M-1,$Y);
1122             my $Y_epoch = time - 2*365.25*86400;
1123             my $ret;
1124             if ($epoch < $Y_epoch) {
1125             $ret = $Y;
1126             } else {
1127             my @gmtime = gmtime $Y_epoch; $gmtime[5] += 1900;
1128             if ($Y == $gmtime[5]) {
1129             $ret = $Y;
1130             } else {
1131             my $M_epoch = time - 9*7*86400;
1132             if ($epoch < $M_epoch) {
1133             $ret = "$Y-$M";
1134             } else {
1135             my @gmtime = gmtime $M_epoch; $gmtime[5] += 1900; $gmtime[4]++;
1136             if ($Y == $gmtime[5] && $M == $gmtime[4]) {
1137             $ret = "$Y-$M";
1138             } else {
1139             $ret = "$Y-$M-$D";
1140             }
1141             }
1142             }
1143             }
1144             return $ret;
1145             },
1146             );
1147             sub solve {
1148 0     0 1   my($V,%Opt) = @_;
1149 0           require Statistics::Regression;
1150 0           my @regression;
1151             my $ycb;
1152 0 0         if (my $ycbbody = $Opt{ycb}) {
1153 0           $ycb = eval('sub {'.$ycbbody.'}');
1154 0 0         die if $@;
1155             } else {
1156             $ycb = sub {
1157 0     0     my $rec = shift;
1158 0           my $y;
1159 0 0         if ($rec->{"meta:ok"} eq "PASS") {
    0          
1160 0           $y = 1;
1161             } elsif ($rec->{"meta:ok"} eq "FAIL") {
1162 0           $y = 0;
1163             }
1164 0           return $y
1165 0           };
1166             }
1167 0           VAR: for my $variable (sort keys %$V) {
1168 0 0         next if $variable eq "==DATA==";
1169 0 0         if ($never_solve_on{$variable}){
1170 0 0         warn "Skipping '$variable'\n" unless $Opt{quiet};
1171 0           next VAR;
1172             }
1173 0           my $value_distribution = $V->{$variable};
1174 0           my $keys = keys %$value_distribution;
1175 0           my @X = qw(const);
1176 0 0         if ($normalize_numeric{$variable}) {
1177 0           push @X, "n_$variable";
1178             } else {
1179 0           my %seen = ();
1180 0           for my $value (sort keys %$value_distribution) {
1181 0           my $pf = $value_distribution->{$value};
1182 0   0       $pf->{PASS} ||= 0;
1183 0   0       $pf->{FAIL} ||= 0;
1184 0 0 0       if ($pf->{PASS} || $pf->{FAIL}) {
1185             my $Xele = sprintf "eq_%s",
1186             (
1187             $normalize_value{$variable} ?
1188 0 0         $normalize_value{$variable}->($value) :
1189             $value
1190             );
1191 0 0         push @X, $Xele unless $seen{$Xele}++;
1192              
1193             }
1194 0 0 0       if (
1195             $pf->{PASS} xor $pf->{FAIL}
1196             ) {
1197 0           my $vl = 40;
1198 0 0         substr($value,$vl) = "..." if length $value > 3+$vl;
1199 0           my $poor_mans_freehand_estimation = 0;
1200 0 0         if ($poor_mans_freehand_estimation) {
1201             warn sprintf
1202             (
1203             "%4d %4d %-23s | %s\n",
1204             $pf->{PASS},
1205             $pf->{FAIL},
1206 0           $variable,
1207             $value,
1208             );
1209             }
1210             }
1211             }
1212             }
1213 0 0         warn "variable[$variable]keys[$keys]X[@X]\n" unless $Opt{quiet};
1214 0 0         next VAR unless @X > 1;
1215 0           my %regdata =
1216             (
1217             X => \@X,
1218             data => [],
1219             );
1220 0           RECORD: for my $rec (@{$V->{"==DATA=="}}) {
  0            
1221 0           my $y = $ycb->($rec);
1222 0 0         next RECORD unless defined $y;
1223 0           my %obs;
1224 0           $obs{Y} = $y;
1225 0           @obs{@X} = (0) x @X;
1226 0           $obs{const} = 1;
1227 0           for my $x (@X) {
1228 0 0         if ($x =~ /^eq_(.+)/) {
    0          
1229 0           my $read_v = $1;
1230 0 0 0       if (exists $rec->{$variable}
1231             && defined $rec->{$variable}
1232             ) {
1233             my $use_v = (
1234             $normalize_value{$variable} ?
1235             $normalize_value{$variable}->($rec->{$variable}) :
1236 0 0         $rec->{$variable}
1237             );
1238 0 0         if ($use_v eq $read_v) {
1239 0           $obs{$x} = 1;
1240             }
1241             }
1242             # warn "DEBUG: y[$y]x[$x]obs[$obs{$x}]\n";
1243             } elsif ($x =~ /^n_(.+)/) {
1244 0           my $v = $1;
1245 0           $obs{$x} = eval { $normalize_numeric{$v}->($rec->{$v}); };
  0            
1246 0 0         if ($@) {
1247 0           warn "Warning: error during parsing v[$v] in record[$rec->{id}]: $@; continuing with undef value";
1248             }
1249             }
1250             }
1251 0           push @{$regdata{data}}, \%obs;
  0            
1252             }
1253 0           my $start = Time::HiRes::time;
1254 0           _run_regression ($variable, \%regdata, \@regression, \%Opt);
1255 0           my $end = Time::HiRes::time;
1256             warn sprintf "regressiontimings[%s]start[%s]end[%s]diff[%s]\n",
1257 0 0         $variable, $start, $end, $end-$start unless $Opt{quiet};
1258             }
1259 0   0       my $top = min ($Opt{solvetop} || 3, scalar @regression);
1260 0 0         my $max_rsq = sum map {1==$_->rsq ? 1 : 0} @regression;
  0            
1261 0 0 0       $top = $max_rsq if $max_rsq && $max_rsq > $top;
1262 0           my $score = 0;
1263 0           printf
1264             (
1265             "State after regression testing: %d results, showing top %d\n\n",
1266             scalar @regression,
1267             $top,
1268             );
1269 0           for my $reg (sort {
1270 0 0         $b->rsq <=> $a->rsq
1271             ||
1272             $a->k <=> $b->k
1273             } @regression) {
1274 0           printf "(%d)\n", ++$score;
1275 0           eval { $reg->print; };
  0            
1276 0 0         if ($@) {
1277 0           printf "\n\nOops, Statistics::Regression died during ->print() with error message[$@]\n\n";
1278             }
1279 0 0         last if --$top <= 0;
1280             }
1281             }
1282             }
1283              
1284             # $variable is the name we pass through to S:R constructor
1285             # $regdata is hash and has the arrays "X" and "data" (observations)
1286             # X goes to S:R constructor
1287             # each observation has a Y which we pass to S:R in an include() call
1288             # $regression is the collector array of results
1289             # $opt are the options from outside, used to see if we are "verbose"
1290             sub _run_regression {
1291 0     0     my($variable,$regdata,$regression,$opt) = @_;
1292 0           my @X = @{$regdata->{X}};
  0            
1293             # my $splo = $regdata->{"spliced-out"} = []; # maybe can be used to
1294             # hold the reference
1295             # group
1296 0           while (@X > 1) {
1297 0           my $reg = Statistics::Regression->new($variable,\@X);
1298 0           for my $obs (@{$regdata->{data}}) {
  0            
1299 0           my $y = delete $obs->{Y};
1300 0           $reg->include($y, $obs);
1301 0           $obs->{Y} = $y;
1302             }
1303 0           eval {$reg->theta;
  0            
1304 0           my @e = $reg->standarderrors;
1305 0 0         die "found standarderrors == 0" if grep { 0 == $_ } @e;
  0            
1306 0           $reg->rsq;};
1307 0 0         if ($@) {
1308 0 0 0       if ($opt->{verbose} && $opt->{verbose}>=2) {
1309             warn _yaml_dump
1310             ({error=>"could not determine some regression parameters",
1311             variable=>$variable,
1312             k=>$reg->k,
1313             n=>$reg->n,
1314 0           X=>$regdata->{"X"},
1315             errorstr => $@,
1316             });
1317             }
1318             # reduce k in case that linear dependencies disturbed us;
1319             # often called reference group; I'm tempted to collect and
1320             # make visible
1321 0           splice @X, 1, 1;
1322             } else {
1323             # $reg->print;
1324 0           push @$regression, $reg;
1325 0           return;
1326             }
1327             }
1328             }
1329              
1330             =head1 AUTHOR
1331              
1332             Andreas König
1333              
1334             =head1 BUGS
1335              
1336             Please report any bugs or feature requests through the web
1337             interface at
1338             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CPAN-Testers-ParseReport>.
1339             I will be notified, and then you'll automatically be notified of
1340             progress on your bug as I make changes.
1341              
1342             =head1 SUPPORT
1343              
1344             You can find documentation for this module with the perldoc command.
1345              
1346             perldoc CPAN::Testers::ParseReport
1347              
1348              
1349             You can also look for information at:
1350              
1351             =over 4
1352              
1353             =item * RT: CPAN's request tracker
1354              
1355             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=CPAN-Testers-ParseReport>
1356              
1357             =item * AnnoCPAN: Annotated CPAN documentation
1358              
1359             L<http://annocpan.org/dist/CPAN-Testers-ParseReport>
1360              
1361             =item * CPAN Ratings
1362              
1363             L<http://cpanratings.perl.org/d/CPAN-Testers-ParseReport>
1364              
1365             =item * Search CPAN
1366              
1367             L<http://search.cpan.org/dist/CPAN-Testers-ParseReport>
1368              
1369             =back
1370              
1371              
1372             =head1 ACKNOWLEDGEMENTS
1373              
1374             Thanks to RJBS for module-starter.
1375              
1376             =head1 COPYRIGHT & LICENSE
1377              
1378             Copyright 2008,2009,2010,2011,2012,2013,2014,2015,2016 Andreas König.
1379              
1380             This program is free software; you can redistribute it and/or modify it
1381             under the same terms as Perl itself.
1382              
1383              
1384             =cut
1385              
1386             1; # End of CPAN::Testers::ParseReport