File Coverage

blib/lib/CPAN/Testers/Schema.pm
Criterion Covered Total %
statement 112 133 84.2
branch 32 42 76.1
condition 2 13 15.3
subroutine 9 12 75.0
pod 3 3 100.0
total 158 203 77.8


line stmt bran cond sub pod time code
1             package CPAN::Testers::Schema;
2             our $VERSION = '0.025';
3             # ABSTRACT: Schema for CPANTesters database processed from test reports
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod my $schema = CPAN::Testers::Schema->connect( $dsn, $user, $pass );
8             #pod my $rs = $schema->resultset( 'Stats' )->search( { dist => 'Test-Simple' } );
9             #pod for my $row ( $rs->all ) {
10             #pod if ( $row->state eq 'fail' ) {
11             #pod say sprintf "Fail report from %s: http://cpantesters.org/cpan/report/%s",
12             #pod $row->tester, $row->guid;
13             #pod }
14             #pod }
15             #pod
16             #pod =head1 DESCRIPTION
17             #pod
18             #pod This is a L<DBIx::Class> Schema for the CPANTesters statistics database.
19             #pod This database is generated by processing the incoming data from L<the
20             #pod CPANTesters Metabase|http://metabase.cpantesters.org>, and extracting
21             #pod the useful fields like distribution, version, platform, and others (see
22             #pod L<CPAN::Testers::Schema::Result::Stats> for a full list).
23             #pod
24             #pod This is its own distribution so that it can be shared by the backend
25             #pod processing, data APIs, and the frontend web application.
26             #pod
27             #pod =head1 SEE ALSO
28             #pod
29             #pod L<CPAN::Testers::Schema::Result::Stats>, L<DBIx::Class>
30             #pod
31             #pod =cut
32              
33 13     13   1055622 use CPAN::Testers::Schema::Base;
  13         30  
  13         70  
34 13     13   4729 use File::Share qw( dist_dir );
  13         281249  
  13         658  
35 13     13   8303 use Path::Tiny qw( path );
  13         116680  
  13         712  
36 13     13   93 use List::Util qw( uniq );
  13         24  
  13         637  
37 13     13   68 use base 'DBIx::Class::Schema';
  13         24  
  13         6975  
38 13     13   619306 use Mojo::UserAgent;
  13         4487336  
  13         104  
39 13     13   6614 use DateTime::Format::ISO8601;
  13         5360278  
  13         22499  
40              
41             __PACKAGE__->load_namespaces;
42             __PACKAGE__->load_components(qw/Schema::Versioned/);
43             __PACKAGE__->upgrade_directory( dist_dir( 'CPAN-Testers-Schema' ) );
44              
45             #pod =method connect_from_config
46             #pod
47             #pod my $schema = CPAN::Testers::Schema->connect_from_config( %extra_conf );
48             #pod
49             #pod Connect to the MySQL database using a local MySQL configuration file
50             #pod in C<$HOME/.cpanstats.cnf>. This configuration file should look like:
51             #pod
52             #pod [client]
53             #pod host = ""
54             #pod database = cpanstats
55             #pod user = my_usr
56             #pod password = my_pwd
57             #pod
58             #pod See L<DBD::mysql/mysql_read_default_file>.
59             #pod
60             #pod C<%extra_conf> will be added to the L<DBIx::Class::Schema/connect>
61             #pod method in the C<%dbi_attributes> hashref (see
62             #pod L<DBIx::Class::Storage::DBI/connect_info>).
63             #pod
64             #pod =cut
65              
66             # Convenience connect method
67 0     0 1 0 sub connect_from_config ( $class, %config ) {
  0         0  
  0         0  
  0         0  
68 0         0 my $schema = $class->connect(
69             "DBI:mysql:mysql_read_default_file=$ENV{HOME}/.cpanstats.cnf;".
70             "mysql_read_default_group=application;mysql_enable_utf8=1",
71             undef, # user
72             undef, # pass
73             {
74             AutoCommit => 1,
75             RaiseError => 1,
76             mysql_enable_utf8 => 1,
77             quote_char => '`',
78             name_sep => '.',
79             %config,
80             },
81             );
82 0         0 return $schema;
83             }
84              
85             #pod =method ordered_schema_versions
86             #pod
87             #pod Get the available schema versions by reading the files in the share
88             #pod directory. These versions can then be upgraded to using the
89             #pod L<cpantesters-schema> script.
90             #pod
91             #pod =cut
92              
93 0     0 1 0 sub ordered_schema_versions( $self ) {
  0         0  
  0         0  
94             my @versions =
95             uniq sort
96 0         0 map { /[\d.]+-([\d.]+)/ }
97 0         0 grep { /CPAN-Testers-Schema-[\d.]+-[\d.]+-MySQL[.]sql/ }
  0         0  
98             path( dist_dir( 'CPAN-Testers-Schema' ) )->children;
99 0         0 return '0.000', @versions;
100             }
101              
102             #pod =method populate_from_api
103             #pod
104             #pod $schema->populate_from_api( \%search, @tables );
105             #pod
106             #pod Populate the given tables from the CPAN Testers API (L<http://api.cpantesters.org>).
107             #pod C<%search> has the following keys:
108             #pod
109             #pod =over
110             #pod
111             #pod =item dist
112             #pod
113             #pod A distribution to populate
114             #pod
115             #pod =item version
116             #pod
117             #pod A distribution version to populate
118             #pod
119             #pod =item author
120             #pod
121             #pod Populate an author's data
122             #pod
123             #pod =back
124             #pod
125             #pod The available C<@tables> are:
126             #pod
127             #pod =over
128             #pod
129             #pod =item * upload
130             #pod
131             #pod =item * release
132             #pod
133             #pod =item * summary
134             #pod
135             #pod =item * report
136             #pod
137             #pod =back
138             #pod
139             #pod =cut
140              
141 7     7 1 2460912 sub populate_from_api( $self, $search, @tables ) {
  7         21  
  7         14  
  7         17  
  7         12  
142 7   33     30 my $ua = $self->{_ua} ||= Mojo::UserAgent->new;
143 7         41 $ua->inactivity_timeout( 120 );
144 7   50     72 my $base_url = $self->{_url} ||= 'http://api.cpantesters.org/v3';
145 7         59 my $dtf = DateTime::Format::ISO8601->new();
146              
147             # Establish dependencies
148 7         481 my @order = qw( upload summary release report );
149 7         24 my $match_tables = join '|', @order;
150 7 50       18 if ( my @unknown = grep { !/^(?:$match_tables)$/ } @tables ) {
  7         128  
151 0         0 die 'Unknown table(s): ', join ', ', @unknown;
152             }
153              
154 7         21 my %tables = map {; $_ => 1 } @tables;
  7         32  
155             # release depends on data in uploads and summary
156 7 100       28 if ( $tables{ release } ) {
157 2         12 @tables{qw( upload summary )} = ( 1, 1 );
158             }
159             # In order to link the report from the dist via the API, we need
160             # to get the summaries first
161 7 100       27 if ( $tables{ report } ) {
162 1         5 @tables{qw( summary )} = ( 1 );
163             }
164             # summary depends on data in uploads
165 7 100       24 if ( $tables{ summary } ) {
166 5         17 @tables{qw( upload )} = ( 1 );
167             }
168              
169             # ; use Data::Dumper;
170             # ; say "Fetching tables: " . Dumper \%tables;
171              
172 7         21 for my $table ( @order ) {
173 28 100       115 next unless $tables{ $table };
174 15         29 my $url = $base_url;
175 15 100       49 if ( $table eq 'upload' ) {
176 7         20 $url .= '/upload';
177 7 100       28 if ( $search->{dist} ) {
    50          
178 6         20 $url .= '/dist/' . $search->{dist};
179             }
180             elsif ( $search->{author} ) {
181 1         5 $url .= '/author/' . $search->{author};
182             }
183 7         43 my $tx = $ua->get( $url );
184 7 50       64057 if ( my $err = $tx->error ) {
185 0   0     0 die sprintf q{Error fetching table '%s': (%s) %s}, $table, $err->{code} // 'XXX', $err->{message};
186             }
187             my @rows = map {
188 7         124 $_->{released} = $dtf->parse_datetime( $_->{released} )->epoch;
  7         2110  
189 7         4338 $_->{type} = 'cpan';
190 7         24 $_;
191             } $tx->res->json->@*;
192 7         50 $self->resultset( 'Upload' )->update_or_create( $_ ) for @rows;
193             }
194              
195 15 100       49154 if ( $table eq 'summary' ) {
196 5         13 $url .= '/summary';
197 5 50       17 if ( $search->{dist} ) {
198 5         16 $url .= '/' . $search->{dist};
199 5 100       16 if ( $search->{version} ) {
200 3         9 $url .= '/' . $search->{version};
201             }
202             }
203 5         20 my $tx = $ua->get( $url );
204 5 50       34219 if ( my $err = $tx->error ) {
205 0   0     0 die sprintf q{Error fetching table '%s': (%s) %s}, $table, $err->{code} // 'XXX', $err->{message};
206             }
207             my @rows = map {
208 5         86 my $dt = $dtf->parse_datetime( delete $_->{date} );
  5         2007  
209 5         2022 $_->{postdate} = $dt->strftime( '%Y%m' );
210 5         260 $_->{fulldate} = $dt->strftime( '%Y%m%d%H%M' );
211 5         324 $_->{state} = delete $_->{grade};
212 5         15 $_->{type} = 2;
213 5         13 $_->{tester} = delete $_->{reporter};
214             $_->{uploadid} = $self->resultset( 'Upload' )
215 5         20 ->search({ $_->%{qw( dist version )} })
216             ->first->id;
217 5         13267 $_;
218             } $tx->res->json->@*;
219             # ; use Data::Dumper;
220             # ; say "Populate summary: " . Dumper \@rows;
221 5         716 for my $perl ( uniq map { $_->{perl} } @rows ) {
  5         30  
222 5         18 $self->resultset( 'PerlVersion' )->find_or_create({
223             version => $perl,
224             });
225             }
226 5         7263 $self->resultset( 'Stats' )->update_or_create( $_, { key => 'guid' } ) for @rows;
227             }
228              
229 15 100       31830 if ( $table eq 'release' ) {
230 2         4 $url .= '/release';
231 2 50       7 if ( $search->{dist} ) {
    0          
232 2         7 $url .= '/dist/' . $search->{dist};
233 2 100       9 if ( $search->{version} ) {
234 1         3 $url .= '/' . $search->{version};
235             }
236             }
237             elsif ( $search->{author} ) {
238 0         0 $url .= '/author/' . $search->{author};
239             }
240 2         10 my $tx = $ua->get( $url );
241 2 50       14343 if ( my $err = $tx->error ) {
242 0   0     0 die sprintf q{Error fetching table '%s': (%s) %s}, $table, $err->{code} // 'XXX', $err->{message};
243             }
244 2 100       44 my @results = $search->{version} ? ( $tx->res->json ) : $tx->res->json->@*;
245             my @rows = map {
246 2         628 delete $_->{author}; # Author is from Upload
  2         7  
247             my $stats_rs = $self->resultset( 'Stats' )
248 2         10 ->search({ $_->%{qw( dist version )} });
249 2         924 $_->{id} = $stats_rs->get_column( 'id' )->max;
250 2         7403 $_->{guid} = $stats_rs->find( $_->{id} )->guid;
251             my $upload = $self->resultset( 'Upload' )
252 2         6884 ->search({ $_->%{qw( dist version )} })
253             ->first;
254 2         4876 $_->{oncpan} = $upload->type eq 'cpan';
255 2         307 $_->{uploadid} = $upload->id;
256             # XXX These are just wrong
257 2         79 $_->{distmat} = 1;
258 2         4 $_->{perlmat} = 1;
259 2         6 $_->{patched} = 1;
260 2         6 $_;
261             } @results;
262             # ; use Data::Dumper;
263             # ; say "Populate release: " . Dumper \@rows;
264 2         67 $self->resultset( 'Release' )->update_or_create( $_ ) for @rows;
265             }
266              
267 15 100       17390 if ( $table eq 'report' ) {
268 1         3 $url .= '/report';
269              
270             # There is no direct API to get reports by dist/version, BUT
271             # we already have summaries loaded in the database so we can
272             # get the GUIDs out of there.
273 1         3 Mojo::Promise->map(
274             { concurrency => 8 },
275 1     1   1 sub( $summary ) {
  1         2887  
276 1         28 my $report_url = join '/', $url, $summary->guid;
277             #; say "Getting report $report_url";
278             return $ua->get_p( $report_url )->then(
279             # Success
280             sub {
281 1         9798 my ( $tx ) = @_;
282 1 50       5 if ( my $err = $tx->error ) {
283 0   0     0 die sprintf q{Error fetching table '%s': (%s) %s}, $table, $err->{code} // 'XXX', $err->{message};
284             }
285 1         19 my $report = $tx->res->json;
286             #; say "Writing $report->{id}";
287             $self->resultset( 'TestReport' )->update_or_create({
288             id => $report->{id},
289 1         636 report => $report,
290             });
291             },
292             # Failure
293             sub {
294 0         0 warn 'Problem fetching report: ' . join ' ', @_;
295             },
296 1         18 );
297             },
298             $self->resultset( 'Stats' )->search( $search )->all,
299             )->then(
300             undef,
301 0     0   0 sub { warn 'Problem fetching reports: ' . join ' ', @_ },
302 1         10 )->wait;
303             }
304             }
305             }
306              
307             1;
308              
309             __END__
310              
311             =pod
312              
313             =head1 NAME
314              
315             CPAN::Testers::Schema - Schema for CPANTesters database processed from test reports
316              
317             =head1 VERSION
318              
319             version 0.025
320              
321             =head1 SYNOPSIS
322              
323             my $schema = CPAN::Testers::Schema->connect( $dsn, $user, $pass );
324             my $rs = $schema->resultset( 'Stats' )->search( { dist => 'Test-Simple' } );
325             for my $row ( $rs->all ) {
326             if ( $row->state eq 'fail' ) {
327             say sprintf "Fail report from %s: http://cpantesters.org/cpan/report/%s",
328             $row->tester, $row->guid;
329             }
330             }
331              
332             =head1 DESCRIPTION
333              
334             This is a L<DBIx::Class> Schema for the CPANTesters statistics database.
335             This database is generated by processing the incoming data from L<the
336             CPANTesters Metabase|http://metabase.cpantesters.org>, and extracting
337             the useful fields like distribution, version, platform, and others (see
338             L<CPAN::Testers::Schema::Result::Stats> for a full list).
339              
340             This is its own distribution so that it can be shared by the backend
341             processing, data APIs, and the frontend web application.
342              
343             =head1 METHODS
344              
345             =head2 connect_from_config
346              
347             my $schema = CPAN::Testers::Schema->connect_from_config( %extra_conf );
348              
349             Connect to the MySQL database using a local MySQL configuration file
350             in C<$HOME/.cpanstats.cnf>. This configuration file should look like:
351              
352             [client]
353             host = ""
354             database = cpanstats
355             user = my_usr
356             password = my_pwd
357              
358             See L<DBD::mysql/mysql_read_default_file>.
359              
360             C<%extra_conf> will be added to the L<DBIx::Class::Schema/connect>
361             method in the C<%dbi_attributes> hashref (see
362             L<DBIx::Class::Storage::DBI/connect_info>).
363              
364             =head2 ordered_schema_versions
365              
366             Get the available schema versions by reading the files in the share
367             directory. These versions can then be upgraded to using the
368             L<cpantesters-schema> script.
369              
370             =head2 populate_from_api
371              
372             $schema->populate_from_api( \%search, @tables );
373              
374             Populate the given tables from the CPAN Testers API (L<http://api.cpantesters.org>).
375             C<%search> has the following keys:
376              
377             =over
378              
379             =item dist
380              
381             A distribution to populate
382              
383             =item version
384              
385             A distribution version to populate
386              
387             =item author
388              
389             Populate an author's data
390              
391             =back
392              
393             The available C<@tables> are:
394              
395             =over
396              
397             =item * upload
398              
399             =item * release
400              
401             =item * summary
402              
403             =item * report
404              
405             =back
406              
407             =head1 SEE ALSO
408              
409             L<CPAN::Testers::Schema::Result::Stats>, L<DBIx::Class>
410              
411             =head1 AUTHORS
412              
413             =over 4
414              
415             =item *
416              
417             Oriol Soriano <oriolsoriano@gmail.com>
418              
419             =item *
420              
421             Doug Bell <preaction@cpan.org>
422              
423             =back
424              
425             =head1 CONTRIBUTORS
426              
427             =for stopwords Breno G. de Oliveira Joel Berger Mohammad S Anwar Nick Tonkin Paul Cochrane
428              
429             =over 4
430              
431             =item *
432              
433             Breno G. de Oliveira <garu@cpan.org>
434              
435             =item *
436              
437             Joel Berger <joel.a.berger@gmail.com>
438              
439             =item *
440              
441             Mohammad S Anwar <mohammad.anwar@yahoo.com>
442              
443             =item *
444              
445             Nick Tonkin <1nickt@users.noreply.github.com>
446              
447             =item *
448              
449             Paul Cochrane <paul@liekut.de>
450              
451             =back
452              
453             =head1 COPYRIGHT AND LICENSE
454              
455             This software is copyright (c) 2018 by Oriol Soriano, Doug Bell.
456              
457             This is free software; you can redistribute it and/or modify it under
458             the same terms as the Perl 5 programming language system itself.
459              
460             =cut