| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package App::cpanminus::reporter; | 
| 2 |  |  |  |  |  |  |  | 
| 3 | 21 |  |  | 21 |  | 1527980 | use warnings; | 
|  | 21 |  |  |  |  | 226 |  | 
|  | 21 |  |  |  |  | 720 |  | 
| 4 | 21 |  |  | 21 |  | 128 | use strict; | 
|  | 21 |  |  |  |  | 41 |  | 
|  | 21 |  |  |  |  | 959 |  | 
| 5 |  |  |  |  |  |  |  | 
| 6 |  |  |  |  |  |  | our $VERSION = '0.21'; | 
| 7 |  |  |  |  |  |  |  | 
| 8 | 21 |  |  | 21 |  | 190 | use Carp (); | 
|  | 21 |  |  |  |  | 56 |  | 
|  | 21 |  |  |  |  | 686 |  | 
| 9 | 21 |  |  | 21 |  | 169 | use File::Spec     3.19; | 
|  | 21 |  |  |  |  | 818 |  | 
|  | 21 |  |  |  |  | 584 |  | 
| 10 | 21 |  |  | 21 |  | 9526 | use File::HomeDir::Tiny (); | 
|  | 21 |  |  |  |  | 6380 |  | 
|  | 21 |  |  |  |  | 563 |  | 
| 11 | 21 |  |  | 21 |  | 11082 | use Test::Reporter 1.54; | 
|  | 21 |  |  |  |  | 736061 |  | 
|  | 21 |  |  |  |  | 901 |  | 
| 12 | 21 |  |  | 21 |  | 13212 | use CPAN::Testers::Common::Client 0.13; | 
|  | 21 |  |  |  |  | 4308699 |  | 
|  | 21 |  |  |  |  | 886 |  | 
| 13 | 21 |  |  | 21 |  | 242 | use CPAN::Testers::Common::Client::Config; | 
|  | 21 |  |  |  |  | 58 |  | 
|  | 21 |  |  |  |  | 489 |  | 
| 14 | 21 |  |  | 21 |  | 11535 | use Parse::CPAN::Meta; | 
|  | 21 |  |  |  |  | 35458 |  | 
|  | 21 |  |  |  |  | 1265 |  | 
| 15 | 21 |  |  | 21 |  | 14750 | use CPAN::Meta::Converter; | 
|  | 21 |  |  |  |  | 446611 |  | 
|  | 21 |  |  |  |  | 1053 |  | 
| 16 | 21 |  |  | 21 |  | 12118 | use Try::Tiny; | 
|  | 21 |  |  |  |  | 45020 |  | 
|  | 21 |  |  |  |  | 1364 |  | 
| 17 | 21 |  |  | 21 |  | 12547 | use URI; | 
|  | 21 |  |  |  |  | 102197 |  | 
|  | 21 |  |  |  |  | 755 |  | 
| 18 | 21 |  |  | 21 |  | 10245 | use Metabase::Resource; | 
|  | 21 |  |  |  |  | 27946 |  | 
|  | 21 |  |  |  |  | 813 |  | 
| 19 | 21 |  |  | 21 |  | 202 | use Capture::Tiny qw(capture); | 
|  | 21 |  |  |  |  | 77 |  | 
|  | 21 |  |  |  |  | 1512 |  | 
| 20 | 21 |  |  | 21 |  | 10204 | use IO::Prompt::Tiny (); | 
|  | 21 |  |  |  |  | 11914 |  | 
|  | 21 |  |  |  |  | 42482 |  | 
| 21 |  |  |  |  |  |  |  | 
| 22 |  |  |  |  |  |  | sub new { | 
| 23 | 21 |  |  | 21 | 0 | 3396 | my ($class, %params) = @_; | 
| 24 | 21 |  |  |  |  | 86 | my $self = bless {}, $class; | 
| 25 |  |  |  |  |  |  |  | 
| 26 |  |  |  |  |  |  | $self->config( | 
| 27 |  |  |  |  |  |  | CPAN::Testers::Common::Client::Config->new( | 
| 28 | 0 |  |  | 0 |  | 0 | prompt => sub { local %ENV; IO::Prompt::Tiny::prompt(@_) }, | 
|  | 0 |  |  |  |  | 0 |  | 
| 29 |  |  |  |  |  |  | ) | 
| 30 | 21 |  |  |  |  | 399 | ); | 
| 31 |  |  |  |  |  |  |  | 
| 32 | 21 | 50 |  |  |  | 121 | if ($params{cpanm}) { | 
| 33 | 0 |  |  |  |  | 0 | my $cpanm = $self->_cpanm( $params{cpanm} ); | 
| 34 | 0 |  |  |  |  | 0 | $params{only} =~ s/-\d+(\.\d+)*$//; # strip version from cpanm's "only" data | 
| 35 |  |  |  |  |  |  |  | 
| 36 |  |  |  |  |  |  | # FIXME: cpanm doesn't provide an accessor here, so | 
| 37 |  |  |  |  |  |  | # we break encapsulation in order to make sure we | 
| 38 |  |  |  |  |  |  | # always have the right paths. | 
| 39 | 0 |  |  |  |  | 0 | $params{build_dir}     = $cpanm->{home}; | 
| 40 | 0 |  |  |  |  | 0 | $params{build_logfile} = $cpanm->{log}; | 
| 41 |  |  |  |  |  |  | } | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | $self->build_dir( | 
| 44 |  |  |  |  |  |  | $params{build_dir} | 
| 45 | 21 |  | 33 |  |  | 690 | || File::Spec->catdir( File::HomeDir::Tiny::home(), '.cpanm' ) | 
| 46 |  |  |  |  |  |  | ); | 
| 47 |  |  |  |  |  |  |  | 
| 48 |  |  |  |  |  |  | $self->build_logfile( | 
| 49 |  |  |  |  |  |  | $params{build_logfile} | 
| 50 | 21 |  | 66 |  |  | 173 | || File::Spec->catfile( $self->build_dir, 'build.log' ) | 
| 51 |  |  |  |  |  |  | ); | 
| 52 |  |  |  |  |  |  |  | 
| 53 | 21 |  | 50 |  |  | 211 | $self->max_age($params{max_age} || 30); | 
| 54 |  |  |  |  |  |  |  | 
| 55 | 21 |  |  |  |  | 83 | foreach my $option ( qw(quiet verbose force exclude only dry-run skip-history ignore-versions all) ) { | 
| 56 | 189 |  |  |  |  | 324 | my $method = $option; | 
| 57 | 189 |  |  |  |  | 405 | $method =~ s/\-/_/g; | 
| 58 | 189 | 100 |  |  |  | 597 | $self->$method( $params{$option} ) if exists $params{$option}; | 
| 59 |  |  |  |  |  |  | } | 
| 60 |  |  |  |  |  |  |  | 
| 61 | 21 |  |  |  |  | 169 | return $self; | 
| 62 |  |  |  |  |  |  | } | 
| 63 |  |  |  |  |  |  |  | 
| 64 | 0 |  |  | 0 | 0 | 0 | sub setup { shift->config->setup } | 
| 65 |  |  |  |  |  |  |  | 
| 66 |  |  |  |  |  |  | ## basic accessors ## | 
| 67 |  |  |  |  |  |  |  | 
| 68 |  |  |  |  |  |  | sub author { | 
| 69 | 78 |  |  | 78 | 0 | 236 | my ($self, $author) = @_; | 
| 70 | 78 | 100 |  |  |  | 235 | $self->{_author} = $author if $author; | 
| 71 | 78 |  |  |  |  | 262 | return $self->{_author}; | 
| 72 |  |  |  |  |  |  | } | 
| 73 |  |  |  |  |  |  |  | 
| 74 |  |  |  |  |  |  | sub distfile { | 
| 75 | 76 |  |  | 76 | 0 | 1657 | my ($self, $distfile) = @_; | 
| 76 | 76 | 100 |  |  |  | 278 | $self->{_distfile} = $distfile if $distfile; | 
| 77 | 76 |  |  |  |  | 201 | return $self->{_distfile}; | 
| 78 |  |  |  |  |  |  | } | 
| 79 |  |  |  |  |  |  |  | 
| 80 |  |  |  |  |  |  | sub config { | 
| 81 | 28 |  |  | 28 | 0 | 1844 | my ($self, $config) = @_; | 
| 82 | 28 | 100 |  |  |  | 176 | $self->{_config} = $config if $config; | 
| 83 | 28 |  |  |  |  | 106 | return $self->{_config}; | 
| 84 |  |  |  |  |  |  | } | 
| 85 |  |  |  |  |  |  |  | 
| 86 |  |  |  |  |  |  | sub verbose { | 
| 87 | 257 |  |  | 257 | 0 | 604 | my ($self, $verbose) = @_; | 
| 88 | 257 | 100 |  |  |  | 573 | $self->{_verbose} = $verbose if $verbose; | 
| 89 | 257 |  |  |  |  | 792 | return $self->{_verbose}; | 
| 90 |  |  |  |  |  |  | } | 
| 91 |  |  |  |  |  |  |  | 
| 92 |  |  |  |  |  |  | sub all { | 
| 93 | 18 |  |  | 18 | 0 | 69 | my ($self, $all) = @_; | 
| 94 | 18 | 50 |  |  |  | 54 | $self->{_all} = $all if $all; | 
| 95 | 18 |  |  |  |  | 117 | return $self->{_all}; | 
| 96 |  |  |  |  |  |  | } | 
| 97 |  |  |  |  |  |  |  | 
| 98 |  |  |  |  |  |  | sub max_age { | 
| 99 | 39 |  |  | 39 | 0 | 101 | my ($self, $max_age) = @_; | 
| 100 | 39 | 100 |  |  |  | 124 | $self->{_max_age} = $max_age if $max_age; | 
| 101 | 39 |  |  |  |  | 78 | return $self->{_max_age}; | 
| 102 |  |  |  |  |  |  | } | 
| 103 |  |  |  |  |  |  |  | 
| 104 |  |  |  |  |  |  | sub force { | 
| 105 | 41 |  |  | 41 | 0 | 116 | my ($self, $force) = @_; | 
| 106 | 41 | 100 |  |  |  | 120 | $self->{_force} = $force if $force; | 
| 107 | 41 |  |  |  |  | 160 | return $self->{_force}; | 
| 108 |  |  |  |  |  |  | } | 
| 109 |  |  |  |  |  |  |  | 
| 110 |  |  |  |  |  |  | sub ignore_versions { | 
| 111 | 88 |  |  | 88 | 0 | 247 | my ($self, $ignore_versions) = @_; | 
| 112 | 88 | 100 |  |  |  | 278 | $self->{_ignore_versions} = $ignore_versions if $ignore_versions; | 
| 113 | 88 |  |  |  |  | 332 | return $self->{_ignore_versions}; | 
| 114 |  |  |  |  |  |  | } | 
| 115 |  |  |  |  |  |  |  | 
| 116 |  |  |  |  |  |  | sub quiet { | 
| 117 | 11 |  |  | 11 | 0 | 34 | my ($self, $quiet) = @_; | 
| 118 | 11 | 100 |  |  |  | 27 | if ($quiet) { | 
| 119 | 3 |  |  |  |  | 17 | $self->verbose(0); | 
| 120 | 3 |  |  |  |  | 13 | $self->{_quiet} = 1; | 
| 121 |  |  |  |  |  |  | } | 
| 122 | 11 |  |  |  |  | 44 | return $self->{_quiet}; | 
| 123 |  |  |  |  |  |  | } | 
| 124 |  |  |  |  |  |  |  | 
| 125 |  |  |  |  |  |  | sub dry_run { | 
| 126 | 2 |  |  | 2 | 0 | 21 | my ($self, $dry_run) = @_; | 
| 127 | 2 | 50 |  |  |  | 12 | $self->{_dry_run} = $dry_run if $dry_run; | 
| 128 | 2 |  |  |  |  | 15 | $self->{_dry_run}; | 
| 129 |  |  |  |  |  |  | } | 
| 130 |  |  |  |  |  |  |  | 
| 131 |  |  |  |  |  |  | sub skip_history { | 
| 132 | 6 |  |  | 6 | 0 | 36 | my ($self, $skip) = @_; | 
| 133 | 6 | 100 |  |  |  | 19 | $self->{_skip_history} = $skip if $skip; | 
| 134 | 6 |  |  |  |  | 28 | $self->{_skip_history}; | 
| 135 |  |  |  |  |  |  | } | 
| 136 |  |  |  |  |  |  |  | 
| 137 |  |  |  |  |  |  | sub only { | 
| 138 | 74 |  |  | 74 | 0 | 650 | my ($self, $only) = @_; | 
| 139 | 74 | 100 |  |  |  | 187 | if ($only) { | 
| 140 | 1 |  |  |  |  | 5 | $only =~ s/::/-/g; | 
| 141 | 1 |  |  |  |  | 10 | my @modules = split /\s*,\s*/, $only; | 
| 142 | 1 |  |  |  |  | 5 | foreach (@modules) { $_ =~ s/(\S+)-[\d.]+$/$1/ }; | 
|  | 3 |  |  |  |  | 10 |  | 
| 143 |  |  |  |  |  |  |  | 
| 144 | 1 |  |  |  |  | 15 | $self->{_only} = { map { $_ => 0 } @modules }; | 
|  | 3 |  |  |  |  | 11 |  | 
| 145 |  |  |  |  |  |  | } | 
| 146 | 74 |  |  |  |  | 396 | return $self->{_only}; | 
| 147 |  |  |  |  |  |  | } | 
| 148 |  |  |  |  |  |  |  | 
| 149 |  |  |  |  |  |  | sub exclude { | 
| 150 | 74 |  |  | 74 | 0 | 199 | my ($self, $exclude) = @_; | 
| 151 | 74 | 100 |  |  |  | 193 | if ($exclude) { | 
| 152 | 1 |  |  |  |  | 6 | $exclude =~ s/::/-/g; | 
| 153 | 1 |  |  |  |  | 9 | my @modules = split /\s*,\s*/, $exclude; | 
| 154 | 1 |  |  |  |  | 4 | foreach (@modules) { $_ =~ s/(\S+)-[\d.]+$/$1/ }; | 
|  | 3 |  |  |  |  | 10 |  | 
| 155 |  |  |  |  |  |  |  | 
| 156 | 1 |  |  |  |  | 3 | $self->{_exclude} = { map { $_ => 0 } @modules }; | 
|  | 3 |  |  |  |  | 12 |  | 
| 157 |  |  |  |  |  |  | } | 
| 158 | 74 |  |  |  |  | 395 | return $self->{_exclude}; | 
| 159 |  |  |  |  |  |  | } | 
| 160 |  |  |  |  |  |  |  | 
| 161 |  |  |  |  |  |  | sub build_dir { | 
| 162 | 26 |  |  | 26 | 0 | 1678 | my ($self, $dir) = @_; | 
| 163 | 26 | 100 |  |  |  | 135 | $self->{_build_dir} = $dir if $dir; | 
| 164 | 26 |  |  |  |  | 120 | return $self->{_build_dir}; | 
| 165 |  |  |  |  |  |  | } | 
| 166 |  |  |  |  |  |  |  | 
| 167 |  |  |  |  |  |  | sub build_logfile { | 
| 168 | 40 |  |  | 40 | 0 | 113 | my ($self, $file) = @_; | 
| 169 | 40 | 100 |  |  |  | 125 | $self->{_build_logfile} = $file if $file; | 
| 170 | 40 |  |  |  |  | 103 | return $self->{_build_logfile}; | 
| 171 |  |  |  |  |  |  | } | 
| 172 |  |  |  |  |  |  |  | 
| 173 |  |  |  |  |  |  | sub _cpanm { | 
| 174 | 0 |  |  | 0 |  | 0 | my ($self, $cpanm) = @_; | 
| 175 | 0 | 0 |  |  |  | 0 | $self->{_cpanm_object} = $cpanm if $cpanm; | 
| 176 | 0 |  |  |  |  | 0 | return $self->{_cpanm_object}; | 
| 177 |  |  |  |  |  |  | } | 
| 178 |  |  |  |  |  |  |  | 
| 179 |  |  |  |  |  |  | sub _check_cpantesters_config_data { | 
| 180 | 0 |  |  | 0 |  | 0 | my $self     = shift; | 
| 181 | 0 |  |  |  |  | 0 | my $config   = $self->config; | 
| 182 | 0 |  |  |  |  | 0 | my $filename = $config->get_config_filename; | 
| 183 |  |  |  |  |  |  |  | 
| 184 | 0 | 0 |  |  |  | 0 | if (-e $filename) { | 
| 185 | 0 | 0 |  |  |  | 0 | if (!$config->read) { | 
| 186 | 0 |  |  |  |  | 0 | print "Error reading CPAN Testers configuration file '$filename'. Aborting."; | 
| 187 | 0 |  |  |  |  | 0 | return; | 
| 188 |  |  |  |  |  |  | } | 
| 189 |  |  |  |  |  |  | } | 
| 190 |  |  |  |  |  |  | else { | 
| 191 | 0 |  |  |  |  | 0 | my $answer = IO::Prompt::Tiny::prompt("CPAN Testers configuration file '$filename' not found. Would you like to set it up now? (y/n)", 'y'); | 
| 192 |  |  |  |  |  |  |  | 
| 193 | 0 | 0 |  |  |  | 0 | if ( $answer =~ /^y/i ) { | 
| 194 | 0 |  |  |  |  | 0 | $config->setup; | 
| 195 |  |  |  |  |  |  | } | 
| 196 |  |  |  |  |  |  | else { | 
| 197 | 0 |  |  |  |  | 0 | print "The CPAN Testers configuration file is required. Aborting.\n"; | 
| 198 | 0 |  |  |  |  | 0 | return; | 
| 199 |  |  |  |  |  |  | } | 
| 200 |  |  |  |  |  |  | } | 
| 201 | 0 |  |  |  |  | 0 | return 1; | 
| 202 |  |  |  |  |  |  | } | 
| 203 |  |  |  |  |  |  |  | 
| 204 |  |  |  |  |  |  | # Returns 1 if log is fresh enough, 0 if it is too old. | 
| 205 |  |  |  |  |  |  | sub _check_build_log { | 
| 206 | 18 |  |  | 18 |  | 49 | my ($self, $build_logfile) = @_; | 
| 207 |  |  |  |  |  |  |  | 
| 208 | 18 |  |  |  |  | 50 | my $max_age = $self->max_age; | 
| 209 |  |  |  |  |  |  |  | 
| 210 |  |  |  |  |  |  | # as a safety mechanism, we only let people parse build.log files | 
| 211 |  |  |  |  |  |  | # if they were generated up to 30 minutes (1800 seconds) ago, | 
| 212 |  |  |  |  |  |  | # unless the user asks us to --force it. | 
| 213 | 18 |  |  |  |  | 454 | my $mtime = (stat $build_logfile)[9]; | 
| 214 | 18 |  |  |  |  | 145 | my $age_in_minutes = int((time - $mtime) / 60); | 
| 215 | 18 | 0 | 33 |  |  | 71 | if ( !$self->force && $mtime && $age_in_minutes > $max_age ) { | 
|  |  |  | 33 |  |  |  |  | 
| 216 | 0 | 0 |  |  |  | 0 | if($self->all) { | 
| 217 | 0 |  |  |  |  | 0 | print "Skipping $build_logfile, too old (modified $age_in_minutes minutes ago > $max_age)." | 
| 218 |  |  |  |  |  |  | } else { | 
| 219 | 0 |  |  |  |  | 0 | print <<"EOMESSAGE"; | 
| 220 |  |  |  |  |  |  | $build_logfile is too old (created $age_in_minutes minutes ago). | 
| 221 |  |  |  |  |  |  |  | 
| 222 |  |  |  |  |  |  | As a standalone tool, it is important that you run cpanm-reporter as | 
| 223 |  |  |  |  |  |  | soon as you finish cpanm, otherwise your system data may have changed, | 
| 224 |  |  |  |  |  |  | from new libraries to a completely different perl binary. | 
| 225 |  |  |  |  |  |  |  | 
| 226 |  |  |  |  |  |  | Because of that, this app will *NOT* parse build.log files which are | 
| 227 |  |  |  |  |  |  | too old (by default: which are last modified more than 30 minutes ago). | 
| 228 |  |  |  |  |  |  |  | 
| 229 |  |  |  |  |  |  | You can override this behaviour by touching the file, passing | 
| 230 |  |  |  |  |  |  | --max-age option or --force flag, but please take good care to avoid | 
| 231 |  |  |  |  |  |  | sending bogus reports. | 
| 232 |  |  |  |  |  |  | EOMESSAGE | 
| 233 |  |  |  |  |  |  | } | 
| 234 | 0 |  |  |  |  | 0 | return; | 
| 235 |  |  |  |  |  |  | } | 
| 236 | 18 |  |  |  |  | 81 | return 1; | 
| 237 |  |  |  |  |  |  | } | 
| 238 |  |  |  |  |  |  |  | 
| 239 |  |  |  |  |  |  | sub _get_logfiles { | 
| 240 | 18 |  |  | 18 |  | 59 | my ($self) = @_; | 
| 241 | 18 |  |  |  |  | 37 | my @files; | 
| 242 | 18 | 50 |  |  |  | 84 | if ($self->all) { | 
| 243 | 0 |  |  |  |  | 0 | my $workdir = File::Spec->catdir($self->build_dir, 'work'); | 
| 244 | 0 | 0 |  |  |  | 0 | if (-e $workdir) { | 
| 245 | 0 | 0 |  |  |  | 0 | opendir my $dh, $workdir or return (); | 
| 246 | 0 | 0 |  |  |  | 0 | my @children = grep { $_ ne '.' && $_ ne '..' } readdir $dh; | 
|  | 0 |  |  |  |  | 0 |  | 
| 247 | 0 |  |  |  |  | 0 | closedir $dh; | 
| 248 | 0 |  |  |  |  | 0 | foreach my $child (@children) { | 
| 249 | 0 |  |  |  |  | 0 | my $logfile = File::Spec->catfile($workdir, $child, 'build.log'); | 
| 250 | 0 | 0 | 0 |  |  | 0 | if (-e $logfile && !-d _) { | 
| 251 | 0 |  |  |  |  | 0 | push @files, $logfile; | 
| 252 |  |  |  |  |  |  | } | 
| 253 |  |  |  |  |  |  | } | 
| 254 |  |  |  |  |  |  | } | 
| 255 |  |  |  |  |  |  | else { | 
| 256 | 0 |  |  |  |  | 0 | print <<"EOMSG"; | 
| 257 |  |  |  |  |  |  | Can not find cpanm work directory (tried $workdir). | 
| 258 |  |  |  |  |  |  | Please specify top cpanm dir as --build-dir, or do not | 
| 259 |  |  |  |  |  |  | specify --build-dir if it is in ~/.cpanm. | 
| 260 |  |  |  |  |  |  | EOMSG | 
| 261 |  |  |  |  |  |  | } | 
| 262 |  |  |  |  |  |  | } | 
| 263 |  |  |  |  |  |  | else { | 
| 264 | 18 |  |  |  |  | 62 | push @files, $self->build_logfile; | 
| 265 |  |  |  |  |  |  | } | 
| 266 | 18 |  |  |  |  | 70 | return @files; | 
| 267 |  |  |  |  |  |  | } | 
| 268 |  |  |  |  |  |  |  | 
| 269 |  |  |  |  |  |  | sub run { | 
| 270 | 18 |  |  | 18 | 0 | 1344 | my $self = shift; | 
| 271 | 18 | 50 |  |  |  | 64 | return unless $self->_check_cpantesters_config_data; | 
| 272 | 18 |  |  |  |  | 153 | foreach my $logfile ($self->_get_logfiles) { | 
| 273 | 18 |  |  |  |  | 72 | $self->process_logfile($logfile); | 
| 274 |  |  |  |  |  |  | } | 
| 275 | 18 |  |  |  |  | 1461 | return; | 
| 276 |  |  |  |  |  |  | } | 
| 277 |  |  |  |  |  |  |  | 
| 278 |  |  |  |  |  |  | sub process_logfile { | 
| 279 | 18 |  |  | 18 | 0 | 53 | my ($self, $logfile) = @_; | 
| 280 |  |  |  |  |  |  |  | 
| 281 | 18 | 50 |  |  |  | 64 | return unless $self->_check_build_log($logfile); | 
| 282 |  |  |  |  |  |  |  | 
| 283 | 18 | 50 |  |  |  | 833 | open my $fh, '<', $logfile | 
| 284 |  |  |  |  |  |  | or Carp::croak "error opening build log file '$logfile' for reading: $!"; | 
| 285 |  |  |  |  |  |  |  | 
| 286 | 18 |  |  |  |  | 1013 | my $header = <$fh>; | 
| 287 | 18 | 50 |  |  |  | 230 | if ($header =~ /^cpanm \(App::cpanminus\) (\d+\.\d+) on perl (\d+\.\d+)/) { | 
| 288 | 18 |  |  |  |  | 135 | $self->{_cpanminus_version} = $1; | 
| 289 | 18 |  |  |  |  | 134 | $self->{_perl_version} = $2; | 
| 290 |  |  |  |  |  |  | } | 
| 291 |  |  |  |  |  |  | else { | 
| 292 | 0 |  |  |  |  | 0 | Carp::croak( | 
| 293 |  |  |  |  |  |  | 'Unable to find cpanminus/perl versions on build.log. ' | 
| 294 |  |  |  |  |  |  | . 'Please update App::cpanminus. If you think this is a mistake, ' | 
| 295 |  |  |  |  |  |  | . 'please send us a bug report with your version of App::cpanminus, ' | 
| 296 |  |  |  |  |  |  | . 'App::cpanminus::reporter, perl -V and your failing build.log file.' | 
| 297 |  |  |  |  |  |  | ); | 
| 298 |  |  |  |  |  |  | } | 
| 299 |  |  |  |  |  |  |  | 
| 300 | 18 |  |  |  |  | 45 | my $found = 0; | 
| 301 | 18 |  |  |  |  | 37 | my $parser; | 
| 302 |  |  |  |  |  |  |  | 
| 303 |  |  |  |  |  |  | # we could go over 100 levels deep on the dependency track | 
| 304 | 21 |  |  | 21 |  | 216 | no warnings 'recursion'; | 
|  | 21 |  |  |  |  | 78 |  | 
|  | 21 |  |  |  |  | 49285 |  | 
| 305 |  |  |  |  |  |  | $parser = sub { | 
| 306 | 90 |  |  | 90 |  | 206 | my ($dist, $resource) = @_; | 
| 307 | 90 | 100 |  |  |  | 531 | (my $dist_vstring = $dist) =~ s/\-(\d+(?:\.\d)+)$/-v$1/ if $dist; | 
| 308 | 90 |  |  |  |  | 194 | my @test_output = (); | 
| 309 | 90 |  |  |  |  | 122 | my $recording; | 
| 310 | 90 |  |  |  |  | 155 | my $has_tests = 0; | 
| 311 | 90 |  |  |  |  | 146 | my $found_na; | 
| 312 |  |  |  |  |  |  | my $fetched; | 
| 313 |  |  |  |  |  |  |  | 
| 314 | 90 |  |  |  |  | 629 | while (<$fh>) { | 
| 315 | 7398 | 100 | 100 |  |  | 38774 | if ( /^Fetching (\S+)/ ) { | 
|  |  | 100 |  |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
| 316 | 71 | 50 |  |  |  | 215 | next if /CHECKSUMS$/; | 
| 317 | 71 |  |  |  |  | 239 | $fetched = $1; | 
| 318 | 71 | 100 |  |  |  | 195 | $resource = $fetched unless $resource; | 
| 319 |  |  |  |  |  |  | } | 
| 320 |  |  |  |  |  |  | elsif ( /^Entering (\S+)/ ) { | 
| 321 | 72 |  |  |  |  | 207 | my $dep = $1; | 
| 322 | 72 |  |  |  |  | 152 | $found = 1; | 
| 323 | 72 | 50 | 66 |  |  | 285 | if ($recording && $recording eq 'test') { | 
| 324 | 0 |  |  |  |  | 0 | Carp::croak 'Parsing error. This should not happen. Please send us a report!'; | 
| 325 |  |  |  |  |  |  | } | 
| 326 |  |  |  |  |  |  | else { | 
| 327 | 72 | 50 | 0 |  |  | 254 | print "entering $dep, " . ($fetched || '(local)') . "\n" if $self->verbose; | 
| 328 | 72 |  |  |  |  | 486 | $parser->($dep, $fetched); | 
| 329 | 72 | 50 | 0 |  |  | 226 | print "left $dep, " . ($fetched || '(local)') . "\n" if $self->verbose; | 
| 330 | 72 |  |  |  |  | 671 | next; | 
| 331 |  |  |  |  |  |  | } | 
| 332 |  |  |  |  |  |  | } | 
| 333 |  |  |  |  |  |  | elsif ( /^Running (?:Build|Makefile)\.PL/ ) { | 
| 334 | 73 |  |  |  |  | 152 | $recording = 'configure'; | 
| 335 |  |  |  |  |  |  | } | 
| 336 |  |  |  |  |  |  | elsif ( $dist and /^Building .*(?:$dist|$dist_vstring)/) { | 
| 337 | 68 | 50 |  |  |  | 252 | print "recording $dist\n" if $self->verbose; | 
| 338 | 68 | 100 |  |  |  | 365 | $has_tests = 1 if /and testing/; | 
| 339 |  |  |  |  |  |  | # if we got here, we need to flush the test output | 
| 340 |  |  |  |  |  |  | # (so far filled with 'configure' output) and start | 
| 341 |  |  |  |  |  |  | # recording the actual tests. | 
| 342 | 68 |  |  |  |  | 330 | @test_output = (); | 
| 343 | 68 |  |  |  |  | 138 | $recording = 'test'; | 
| 344 |  |  |  |  |  |  | } | 
| 345 |  |  |  |  |  |  |  | 
| 346 | 7326 | 100 |  |  |  | 16714 | push @test_output, $_ if $recording; | 
| 347 |  |  |  |  |  |  |  | 
| 348 | 7326 |  |  |  |  | 9445 | my $result; | 
| 349 | 7326 | 100 |  |  |  | 11964 | if ($recording) { | 
| 350 | 5856 | 100 | 100 |  |  | 42829 | if (   /^Result: (PASS|NA|FAIL|UNKNOWN|NOTESTS)/ | 
|  |  | 100 | 100 |  |  |  |  | 
|  |  | 100 | 100 |  |  |  |  | 
|  |  |  | 66 |  |  |  |  | 
|  |  |  | 66 |  |  |  |  | 
| 351 |  |  |  |  |  |  | || ($recording eq 'test' && /^-> (FAIL|OK)/) | 
| 352 |  |  |  |  |  |  | ) { | 
| 353 | 69 |  |  |  |  | 210 | $result = $1; | 
| 354 | 69 | 50 | 66 |  |  | 525 | if ($result eq 'FAIL' && $recording eq 'configure') { | 
|  |  | 100 | 66 |  |  |  |  | 
|  |  | 100 | 100 |  |  |  |  | 
|  |  | 50 |  |  |  |  |  | 
| 355 | 0 |  |  |  |  | 0 | $result = 'NA'; | 
| 356 |  |  |  |  |  |  | } | 
| 357 |  |  |  |  |  |  | elsif ($result eq 'FAIL' && @test_output > 1 && $test_output[-2] =~ /make.*?[1-9]/) { | 
| 358 |  |  |  |  |  |  | # [dn]make error returning non-zero status should be graded UNKNOWN | 
| 359 | 1 |  |  |  |  | 4 | $result = 'UNKNOWN'; | 
| 360 |  |  |  |  |  |  | } | 
| 361 |  |  |  |  |  |  | elsif ($result eq 'OK') { | 
| 362 | 4 | 100 |  |  |  | 17 | $result = $has_tests ? 'PASS' : 'UNKNOWN'; | 
| 363 |  |  |  |  |  |  | } | 
| 364 |  |  |  |  |  |  | elsif ($result eq 'NOTESTS') { | 
| 365 | 0 |  |  |  |  | 0 | $result = 'UNKNOWN'; | 
| 366 |  |  |  |  |  |  | } | 
| 367 |  |  |  |  |  |  | } | 
| 368 |  |  |  |  |  |  | elsif ( $recording eq 'configure' && /^-> N\/A/ ) { | 
| 369 | 2 |  |  |  |  | 5 | $found_na = 1; | 
| 370 |  |  |  |  |  |  | } | 
| 371 |  |  |  |  |  |  | elsif (  $recording eq 'configure' | 
| 372 |  |  |  |  |  |  | # https://github.com/miyagawa/cpanminus/blob/devel/lib/App/cpanminus/script.pm#L2269 | 
| 373 |  |  |  |  |  |  | && ( /Configure failed for (?:$dist|$dist_vstring)/ | 
| 374 |  |  |  |  |  |  | || /proper Makefile.PL\/Build.PL/ | 
| 375 |  |  |  |  |  |  | || /configure the distribution/ | 
| 376 |  |  |  |  |  |  | ) | 
| 377 |  |  |  |  |  |  | ) { | 
| 378 | 2 | 50 |  |  |  | 9 | $result = $found_na ? 'NA' : 'UNKNOWN'; | 
| 379 |  |  |  |  |  |  | } | 
| 380 |  |  |  |  |  |  | } | 
| 381 | 7326 | 100 |  |  |  | 22634 | if ($result) { | 
| 382 | 71 |  |  |  |  | 150 | my $dist_without_version = $dist; | 
| 383 | 71 |  |  |  |  | 470 | $dist_without_version =~ s/(\S+)-[\d.]+$/$1/; | 
| 384 |  |  |  |  |  |  |  | 
| 385 | 71 | 100 | 33 |  |  | 439 | if (@test_output <= 2) { | 
|  |  | 50 | 33 |  |  |  |  | 
|  |  | 50 | 66 |  |  |  |  | 
|  |  | 50 | 66 |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
| 386 | 1 |  |  |  |  | 70 | print "No test output found for '$dist'. Skipping...\n" | 
| 387 |  |  |  |  |  |  | . "To send test reports, please make sure *NOT* to pass '-v' to cpanm or your build.log will contain no output to send.\n"; | 
| 388 |  |  |  |  |  |  | } | 
| 389 |  |  |  |  |  |  | elsif (!$resource) { | 
| 390 | 0 |  |  |  |  | 0 | print "Skipping report for local installation of '$dist'.\n"; | 
| 391 |  |  |  |  |  |  | } | 
| 392 |  |  |  |  |  |  | elsif ( defined $self->exclude && exists $self->exclude->{$dist_without_version} ) { | 
| 393 | 0 | 0 |  |  |  | 0 | print "Skipping $dist as it's in the 'exclude' list...\n" if $self->verbose; | 
| 394 |  |  |  |  |  |  | } | 
| 395 |  |  |  |  |  |  | elsif ( defined $self->only && !exists $self->only->{$dist_without_version} ) { | 
| 396 | 0 | 0 |  |  |  | 0 | print "Skipping $dist as it isn't in the 'only' list...\n" if $self->verbose; | 
| 397 |  |  |  |  |  |  | } | 
| 398 |  |  |  |  |  |  | elsif ( !$self->ignore_versions && defined $self->{_perl_version} && ( $self->{_perl_version} ne $] ) ) { | 
| 399 | 1 | 50 |  |  |  | 19 | print "Skipping $dist as its build Perl version ($self->{_perl_version}) differs from the currently running perl ($])...\n" if $self->verbose; | 
| 400 |  |  |  |  |  |  | } | 
| 401 |  |  |  |  |  |  | else { | 
| 402 | 69 |  |  |  |  | 413 | my $report = $self->make_report($resource, $dist, $result, @test_output); | 
| 403 |  |  |  |  |  |  | } | 
| 404 | 71 |  |  |  |  | 46537 | return; | 
| 405 |  |  |  |  |  |  | } | 
| 406 |  |  |  |  |  |  | } | 
| 407 | 18 |  |  |  |  | 150 | }; | 
| 408 |  |  |  |  |  |  |  | 
| 409 | 18 | 50 |  |  |  | 120 | print "Parsing $logfile...\n" if $self->verbose; | 
| 410 | 18 |  |  |  |  | 80 | $parser->(); | 
| 411 | 18 | 50 | 66 |  |  | 180 | print "No reports found.\n" if !$found and $self->verbose; | 
| 412 | 18 | 50 |  |  |  | 91 | print "Finished.\n" if $self->verbose; | 
| 413 |  |  |  |  |  |  |  | 
| 414 | 18 |  |  |  |  | 253 | close $fh; | 
| 415 | 18 |  |  |  |  | 154 | return; | 
| 416 |  |  |  |  |  |  | } | 
| 417 |  |  |  |  |  |  |  | 
| 418 |  |  |  |  |  |  | sub get_author { | 
| 419 | 61 |  |  | 61 | 0 | 199 | my ($self, $path) = @_; | 
| 420 | 61 | 100 |  |  |  | 178 | if ($path->scheme eq 'file') { | 
| 421 | 1 |  |  |  |  | 63 | return $self->_get_author_from_file($path); | 
| 422 |  |  |  |  |  |  | } | 
| 423 |  |  |  |  |  |  | else { | 
| 424 | 60 |  |  |  |  | 1000 | return $self->_get_author_from_metabase($path->path); | 
| 425 |  |  |  |  |  |  | } | 
| 426 |  |  |  |  |  |  | } | 
| 427 |  |  |  |  |  |  |  | 
| 428 |  |  |  |  |  |  | sub _get_author_from_file { | 
| 429 | 1 |  |  | 1 |  | 5 | my ($self, $path) = @_; | 
| 430 |  |  |  |  |  |  |  | 
| 431 | 1 |  |  |  |  | 84 | my $directories = (File::Spec->splitpath($path))[1]; | 
| 432 | 1 |  |  |  |  | 104 | my @path = File::Spec->splitdir($directories); | 
| 433 | 1 | 50 |  |  |  | 16 | pop @path if $path[-1] eq ''; | 
| 434 |  |  |  |  |  |  |  | 
| 435 | 1 | 50 | 33 |  |  | 88 | if ( @path >= 3                               # R/RJ/RJBS | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 33 |  |  |  |  | 
| 436 |  |  |  |  |  |  | && $path[-1] =~ /\A[A-Z\-]+\z/           # RJBS | 
| 437 |  |  |  |  |  |  | && substr($path[-1], 0, 2) eq $path[-2]  # RJ | 
| 438 |  |  |  |  |  |  | && substr($path[-1], 0, 1) eq $path[-3]  # R | 
| 439 |  |  |  |  |  |  | ) { | 
| 440 | 1 |  |  |  |  | 21 | return $path[-1]; | 
| 441 |  |  |  |  |  |  | } | 
| 442 |  |  |  |  |  |  | else { | 
| 443 | 0 | 0 |  |  |  | 0 | print "DEBUG: path '$path' doesn't look valid" if $self->verbose; | 
| 444 | 0 |  |  |  |  | 0 | return; | 
| 445 |  |  |  |  |  |  | } | 
| 446 |  |  |  |  |  |  | } | 
| 447 |  |  |  |  |  |  |  | 
| 448 |  |  |  |  |  |  | sub _get_author_from_metabase { | 
| 449 | 60 |  |  | 60 |  | 789 | my ($self, $path) = @_; | 
| 450 | 60 |  |  |  |  | 90 | my $metadata; | 
| 451 |  |  |  |  |  |  |  | 
| 452 |  |  |  |  |  |  | try { | 
| 453 | 60 |  |  | 60 |  | 3184 | $metadata = Metabase::Resource->new( q[cpan:///distfile/] . $path )->metadata; | 
| 454 |  |  |  |  |  |  | } | 
| 455 |  |  |  |  |  |  | catch { | 
| 456 | 0 | 0 |  | 0 |  | 0 | print "DEBUG: $_" if $self->verbose; | 
| 457 | 60 |  |  |  |  | 496 | }; | 
| 458 | 60 | 50 |  |  |  | 62349 | return unless $metadata; | 
| 459 |  |  |  |  |  |  |  | 
| 460 | 60 |  |  |  |  | 280 | return $metadata->{cpan_id}; | 
| 461 |  |  |  |  |  |  | } | 
| 462 |  |  |  |  |  |  |  | 
| 463 |  |  |  |  |  |  |  | 
| 464 |  |  |  |  |  |  | # returns false in case of error (so, skip!) | 
| 465 |  |  |  |  |  |  | sub parse_uri { | 
| 466 | 61 |  |  | 61 | 0 | 6301 | my ($self, $resource) = @_; | 
| 467 |  |  |  |  |  |  |  | 
| 468 | 61 |  |  |  |  | 274 | my $uri = URI->new( $resource ); | 
| 469 | 61 |  |  |  |  | 90670 | my $scheme = lc $uri->scheme; | 
| 470 | 61 |  |  |  |  | 2023 | my %eligible_schemes = map {$_ => 1} (qw| http https ftp cpan file |); | 
|  | 305 |  |  |  |  | 841 |  | 
| 471 | 61 | 50 |  |  |  | 233 | if (! $eligible_schemes{$scheme}) { | 
| 472 | 0 | 0 |  |  |  | 0 | print "invalid scheme '$scheme' for resource '$resource'. Skipping...\n" | 
| 473 |  |  |  |  |  |  | unless $self->quiet; | 
| 474 | 0 |  |  |  |  | 0 | return; | 
| 475 |  |  |  |  |  |  | } | 
| 476 |  |  |  |  |  |  |  | 
| 477 | 61 |  |  |  |  | 222 | my $author = $self->get_author( $uri ); | 
| 478 |  |  |  |  |  |  |  | 
| 479 | 61 | 50 |  |  |  | 183 | unless (defined $author) { | 
| 480 | 0 | 0 |  |  |  | 0 | print "error fetching author for resource '$resource'. Skipping...\n" | 
| 481 |  |  |  |  |  |  | unless $self->quiet; | 
| 482 | 0 |  |  |  |  | 0 | return; | 
| 483 |  |  |  |  |  |  | } | 
| 484 |  |  |  |  |  |  |  | 
| 485 |  |  |  |  |  |  | # the 'LOCAL' user is reserved and should never send reports. | 
| 486 | 61 | 100 |  |  |  | 180 | if ($author eq 'LOCAL') { | 
| 487 | 1 | 50 |  |  |  | 4 | print "'LOCAL' user is reserved. Skipping resource '$resource'\n" | 
| 488 |  |  |  |  |  |  | unless $self->quiet; | 
| 489 | 1 |  |  |  |  | 16 | return; | 
| 490 |  |  |  |  |  |  | } | 
| 491 |  |  |  |  |  |  |  | 
| 492 | 60 |  |  |  |  | 261 | $self->author($author); | 
| 493 |  |  |  |  |  |  |  | 
| 494 | 60 |  |  |  |  | 211 | $self->distfile(substr("$uri", index("$uri", $author))); | 
| 495 |  |  |  |  |  |  |  | 
| 496 | 60 |  |  |  |  | 337 | return 1; | 
| 497 |  |  |  |  |  |  | } | 
| 498 |  |  |  |  |  |  |  | 
| 499 |  |  |  |  |  |  | sub make_report { | 
| 500 | 4 |  |  | 4 | 0 | 33 | my ($self, $resource, $dist, $result, @test_output) = @_; | 
| 501 |  |  |  |  |  |  |  | 
| 502 | 4 | 100 |  |  |  | 23 | if ( index($dist, 'Local-') == 0 ) { | 
| 503 | 1 | 50 |  |  |  | 3 | print "'Local::' namespace is reserved. Skipping resource '$resource'\n" | 
| 504 |  |  |  |  |  |  | unless $self->quiet; | 
| 505 | 1 |  |  |  |  | 5 | return; | 
| 506 |  |  |  |  |  |  | } | 
| 507 | 3 | 100 |  |  |  | 12 | return unless $self->parse_uri($resource); | 
| 508 |  |  |  |  |  |  |  | 
| 509 | 2 |  |  |  |  | 14 | my $author = $self->author; | 
| 510 |  |  |  |  |  |  |  | 
| 511 | 2 |  | 100 |  |  | 17 | my $cpanm_version = $self->{_cpanminus_version} || 'unknown cpanm version'; | 
| 512 | 2 |  |  |  |  | 10 | my $meta = $self->get_meta_for( $dist ); | 
| 513 |  |  |  |  |  |  | my $client = CPAN::Testers::Common::Client->new( | 
| 514 |  |  |  |  |  |  | author      => $self->author, | 
| 515 |  |  |  |  |  |  | distname    => $dist, | 
| 516 |  |  |  |  |  |  | grade       => $result, | 
| 517 |  |  |  |  |  |  | via         => "App::cpanminus::reporter $VERSION ($cpanm_version)", | 
| 518 |  |  |  |  |  |  | test_output => join( '', @test_output ), | 
| 519 |  |  |  |  |  |  | prereqs     => ($meta && ref $meta) ? $meta->{prereqs} : undef, | 
| 520 | 2 | 50 | 33 |  |  | 13 | ); | 
| 521 |  |  |  |  |  |  |  | 
| 522 | 2 | 50 | 33 |  |  | 137 | if (!$self->skip_history && $client->is_duplicate) { | 
| 523 | 0 | 0 |  |  |  | 0 | print "($resource, $author, $dist, $result) was already sent. Skipping...\n" | 
| 524 |  |  |  |  |  |  | if $self->verbose; | 
| 525 | 0 |  |  |  |  | 0 | return; | 
| 526 |  |  |  |  |  |  | } | 
| 527 |  |  |  |  |  |  | else { | 
| 528 | 2 | 50 |  |  |  | 9 | print "sending: ($resource, $author, $dist, $result)\n" unless $self->quiet; | 
| 529 |  |  |  |  |  |  | } | 
| 530 |  |  |  |  |  |  |  | 
| 531 | 2 |  |  |  |  | 10 | my $reporter = Test::Reporter->new( | 
| 532 |  |  |  |  |  |  | transport      => $self->config->transport_name, | 
| 533 |  |  |  |  |  |  | transport_args => $self->config->transport_args, | 
| 534 |  |  |  |  |  |  | grade          => $client->grade, | 
| 535 |  |  |  |  |  |  | distribution   => $dist, | 
| 536 |  |  |  |  |  |  | distfile       => $self->distfile, | 
| 537 |  |  |  |  |  |  | from           => $self->config->email_from, | 
| 538 |  |  |  |  |  |  | comments       => $client->email, | 
| 539 |  |  |  |  |  |  | via            => $client->via, | 
| 540 |  |  |  |  |  |  | ); | 
| 541 |  |  |  |  |  |  |  | 
| 542 | 2 | 50 |  |  |  | 708903 | if ($self->dry_run) { | 
| 543 | 0 | 0 |  |  |  | 0 | print "not sending (dry run)\n" unless $self->quiet; | 
| 544 | 0 |  |  |  |  | 0 | return; | 
| 545 |  |  |  |  |  |  | } | 
| 546 |  |  |  |  |  |  |  | 
| 547 |  |  |  |  |  |  | try { | 
| 548 | 2 | 50 |  | 2 |  | 381 | $reporter->send() || die $reporter->errstr(); | 
| 549 |  |  |  |  |  |  | } | 
| 550 |  |  |  |  |  |  | catch { | 
| 551 | 0 | 0 |  | 0 |  | 0 | print "Error while sending this report, continuing with the next one ($_)...\n" unless $self->quiet; | 
| 552 | 0 | 0 |  |  |  | 0 | print "DEBUG: @_" if $self->verbose; | 
| 553 |  |  |  |  |  |  | } finally{ | 
| 554 | 2 | 50 |  | 2 |  | 734 | $client->record_history unless $self->skip_history; | 
| 555 | 2 |  |  |  |  | 59 | }; | 
| 556 | 2 |  |  |  |  | 225 | return; | 
| 557 |  |  |  |  |  |  | } | 
| 558 |  |  |  |  |  |  |  | 
| 559 |  |  |  |  |  |  | sub get_meta_for { | 
| 560 | 2 |  |  | 2 | 0 | 5 | my ($self, $dist) = @_; | 
| 561 | 2 |  |  |  |  | 9 | my $distdir = File::Spec->catdir( $self->build_dir, 'latest-build', $dist ); | 
| 562 |  |  |  |  |  |  |  | 
| 563 | 2 |  |  |  |  | 11 | foreach my $meta_file ( qw( MYMETA.json MYMETA.yml META.json META.yml ) ) { | 
| 564 | 8 |  |  |  |  | 76 | my $meta_path = File::Spec->catfile( $distdir, $meta_file ); | 
| 565 | 8 | 50 |  |  |  | 122 | if (-e $meta_path) { | 
| 566 | 0 |  |  |  |  | 0 | my $meta = eval { Parse::CPAN::Meta->load_file( $meta_path ) }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 567 | 0 | 0 |  |  |  | 0 | next if $@; | 
| 568 |  |  |  |  |  |  |  | 
| 569 | 0 | 0 | 0 |  |  | 0 | if (!$meta->{'meta-spec'} or $meta->{'meta-spec'}{version} < 2) { | 
| 570 | 0 |  |  |  |  | 0 | $meta = CPAN::Meta::Converter->new( $meta )->convert( version => 2 ); | 
| 571 |  |  |  |  |  |  | } | 
| 572 | 0 |  |  |  |  | 0 | return $meta; | 
| 573 |  |  |  |  |  |  | } | 
| 574 |  |  |  |  |  |  | } | 
| 575 | 2 |  |  |  |  | 10 | return; | 
| 576 |  |  |  |  |  |  | } | 
| 577 |  |  |  |  |  |  |  | 
| 578 |  |  |  |  |  |  |  | 
| 579 |  |  |  |  |  |  | 42; | 
| 580 |  |  |  |  |  |  | __END__ |