File Coverage

blib/lib/Test/Chimps/Server.pm
Criterion Covered Total %
statement 86 229 37.5
branch 3 46 6.5
condition 1 11 9.0
subroutine 26 40 65.0
pod 2 2 100.0
total 118 328 35.9


line stmt bran cond sub pod time code
1             package Test::Chimps::Server;
2              
3 1     1   35837 use warnings;
  1         3  
  1         41  
4 1     1   6 use strict;
  1         1  
  1         34  
5              
6 1     1   863 use Test::Chimps::ReportCollection;
  1         5  
  1         14  
7 1     1   801 use Test::Chimps::Report;
  1         6  
  1         11  
8 1     1   933 use Test::Chimps::Server::Lister;
  1         10  
  1         10  
9              
10 1     1   1226 use Algorithm::TokenBucket;
  1         11655  
  1         23  
11 1     1   1813 use CGI::Carp qw;
  1         4008  
  1         9  
12 1     1   15922 use CGI;
  1         14657  
  1         8  
13 1     1   66 use Digest::MD5 qw;
  1         2  
  1         78  
14 1     1   6 use Fcntl qw<:DEFAULT :flock>;
  1         3  
  1         515  
15 1     1   7 use File::Basename;
  1         2  
  1         116  
16 1     1   6 use File::Path;
  1         1  
  1         62  
17 1     1   7 use File::Spec;
  1         2  
  1         10  
18 1     1   1237 use Jifty::DBI::Handle;
  1         59706  
  1         22  
19 1     1   1110 use Jifty::DBI::SchemaGenerator;
  1         24024  
  1         10  
20 1     1   43 use Params::Validate qw<:all>;
  1         2  
  1         238  
21 1     1   5 use Storable qw;
  1         2  
  1         83  
22 1     1   1193 use TAP::Formatter::HTML;
  1         84850  
  1         15  
23 1     1   980 use TAP::Parser::Aggregator;
  1         7231  
  1         15  
24 1     1   1015 use TAP::Harness::Archive;
  1         233332  
  1         20  
25 1     1   1931 use YAML::Syck;
  1         2835  
  1         69  
26 1     1   8 use DateTime;
  1         2  
  1         12  
27              
28 1     1   32 use constant PROTO_VERSION => 1.0;
  1         2  
  1         62  
29              
30             =head1 NAME
31              
32             Test::Chimps::Server - Accept smoke report uploads and display smoke reports
33              
34             =head1 SYNOPSIS
35              
36             This module simplifies the process of running a smoke server. It
37             is meant to be used with Test::Chimps::Client.
38              
39             use Test::Chimps::Server;
40              
41             my $server = Test::Chimps::Server->new(base_dir => '/var/www/smokes');
42              
43             $server->handle_request;
44              
45             =head1 METHODS
46              
47             =head2 new ARGS
48              
49             Creates a new Server object. ARGS is a hash whose valid keys are:
50              
51             =over 4
52              
53             =item * base_dir
54              
55             Mandatory. Base directory where report data will be stored.
56              
57             =item * bucket_file
58              
59             Name of bucket database file (see L). Defaults
60             to 'bucket.dat'.
61              
62             =item * burst_rate
63              
64             Burst upload rate allowed (see L). Defaults to
65             5.
66              
67             =item * database
68              
69             Name of the database
70              
71             =item * database_driver
72              
73             Database driver to use
74              
75             =item * database_user
76              
77             User to connect ot the database as
78              
79             =item * database_password
80              
81             Password to connect to the database with
82              
83             =item * list_template
84              
85             Template filename under base_dir/template_dir to use for listing
86             smoke reports. Defaults to 'list.tmpl'.
87              
88             =item * lister
89              
90             An instance of L to use to list smoke
91             reports. You do not have to use this option unless you are
92             subclassing C.
93              
94             =item * max_rate
95              
96             Maximum upload rate allowed (see L). Defaults
97             to 1/30.
98              
99             =item * max_size
100              
101             Maximum size of HTTP POST that will be accepted. Defaults to 3
102             MiB.
103              
104             =item * max_reports_per_subcategory
105              
106             Maximum number of smokes allowed per category. Defaults to 5.
107              
108             =item * template_dir
109              
110             Directory under base_dir where html templates will be stored.
111             Defaults to 'templates'.
112              
113             =item * variables_validation_spec
114              
115             A hash reference of the form accepted by Params::Validate. If
116             supplied, this will be used to validate the report variables
117             submitted to the server.
118              
119             =back
120              
121             =cut
122              
123 1     1   5 use base qw/Class::Accessor/;
  1         2  
  1         2965  
124              
125             __PACKAGE__->mk_ro_accessors(
126             qw/base_dir bucket_file max_rate max_size
127             max_reports_per_subcategory database database_driver database_user database_password
128             template_dir list_template lister
129             variables_validation_spec handle/
130             );
131              
132             sub new {
133 1     1 1 55 my $class = shift;
134 1         4 my $obj = bless {}, $class;
135 1         5 $obj->_init(@_);
136 1         7 return $obj;
137             }
138              
139             sub _init {
140 1     1   4 my $self = shift;
141             my %args = validate_with(
142             params => \@_,
143             called => 'The Test::Chimps::Server constructor',
144             spec => {
145             base_dir => {
146             type => SCALAR,
147             optional => 0
148             },
149             bucket_file => {
150             type => SCALAR,
151             default => 'bucket.dat',
152             optional => 1
153             },
154             burst_rate => {
155             type => SCALAR,
156             optional => 1,
157             default => 5,
158             callbacks => {
159 0     0   0 "greater than or equal to 0" => sub { $_[0] >= 0 }
160             }
161             },
162             database => {
163             type => SCALAR,
164             optional => 1,
165             default => 'chimpsdb/database'
166             },
167             database_driver => {
168             type => SCALAR,
169             optional => 1,
170             default => 'SQLite'
171             },
172             database_user => {
173             type => SCALAR,
174             optional => 1,
175             default => ''
176             },
177             database_password => {
178             type => SCALAR,
179             optional => 1,
180             default => ''
181             },
182             variables_validation_spec => {
183             type => HASHREF,
184             optional => 1
185             },
186             list_template => {
187             type => SCALAR,
188             optional => 1,
189             default => 'list.tmpl'
190             },
191             lister => {
192             type => SCALAR,
193             isa => 'Test::Chimps::Server::Lister',
194             optional => 1
195             },
196             max_rate => {
197             type => SCALAR,
198             default => 1 / 30,
199             optional => 1,
200             callbacks => {
201 0     0   0 "greater than or equal to 0" => sub { $_[0] >= 0 }
202             }
203             },
204             max_size => {
205             type => SCALAR,
206             default => 2**20 * 3.0,
207             optional => 1,
208             callbacks => {
209 0     0   0 "greater than or equal to 0" => sub { $_[0] >= 0 }
210             }
211             },
212             max_reports_per_subcategory => {
213             type => SCALAR,
214             default => 5,
215             optional => 1,
216             callbacks => {
217 0     0   0 "greater than or equal to 0" => sub { $_[0] >= 0 }
218             }
219             },
220 1         103 template_dir => {
221             type => SCALAR,
222             default => 'templates',
223             optional => 1
224             }
225             }
226             );
227            
228 1         31 foreach my $key (keys %args) {
229 12         39 $self->{$key} = $args{$key};
230             }
231              
232 1 50       9 if (defined $self->variables_validation_spec) {
233 0         0 foreach my $var (keys %{$self->variables_validation_spec}) {
  0         0  
234 0         0 my $column = Test::Chimps::Report->add_column($var);
235 0         0 $column->type("text");
236 0         0 $column->writable(1);
237 0         0 $column->readable(1);
238 0         0 Test::Chimps::Report->_init_methods_for_column($column);
239             }
240             }
241              
242 1         31 $self->{handle} = Jifty::DBI::Handle->new();
243 1         30 eval {
244 1         6 $self->handle->connect(driver => $self->database_driver,
245             database => $self->database,
246             user => $self->database_user,
247             password => $self->database_password,
248             );
249             };
250              
251 1         20200 my $error = $@;
252 1 50 33     30 if ( $error =~ /database .*? does not exist/i
    50          
253             or $error =~ /unknown database/i ) {
254 0 0         if ($self->database_driver ne 'SQLite') {
255 0           warn "Creating database\n";
256 0           my $dbname = $self->database;
257 0 0         $dbname = 'template1' if $self->database_driver =~ /Pg/;
258 0 0         $dbname = '' if $self->database_driver eq 'mysql';
259 0           my $create_handle = Jifty::DBI::Handle->new;
260 0           $create_handle->connect(driver => $self->database_driver,
261             database => $dbname,
262             user => $self->database_user,
263             password => $self->database_password,
264             );
265              
266 0           my $query = "CREATE DATABASE ".$self->database;
267 0 0         $query .= " TEMPLATE template0" if $self->database_driver =~ /Pg/;
268 0           $create_handle->simple_query($query);
269 0           $create_handle->disconnect;
270              
271 0           $self->{handle} = Jifty::DBI::Handle->new();
272 0           $self->handle->connect(driver => $self->database_driver,
273             database => $self->database,
274             user => $self->database_user,
275             password => $self->database_password,
276             );
277             }
278              
279 0           warn "Running create statements\n";
280 0           my $sg = Jifty::DBI::SchemaGenerator->new($self->handle);
281 0           $sg->add_model(Test::Chimps::Report->new(handle => $self->handle));
282 0           $self->handle->simple_query($_) for $sg->create_table_sql_statements;
283             } elsif ($error) {
284 0           die $error;
285             }
286             }
287              
288             =head2 handle_request
289              
290             Handles a single request. This function will either accept a
291             series of reports for upload or display report summaries.
292              
293             =cut
294              
295             sub handle_request {
296 0     0 1   my $self = shift;
297              
298 0           my $cgi = CGI->new;
299 0 0         if ($cgi->param("upload")) {
    0          
300 0           $self->_process_upload($cgi);
301             } elsif ($cgi->param("id")) {
302 0           $self->_process_detail($cgi);
303             } else {
304 0           $self->_process_listing($cgi);
305             }
306             }
307              
308             sub _process_upload {
309 0     0     my $self = shift;
310 0           my $cgi = shift;
311              
312 0           print $cgi->header("text/plain");
313             # $self->_limit_rate($cgi);
314 0           $self->_validate_params($cgi);
315 0           $self->_add_report($cgi);
316              
317 0           print "ok";
318             }
319              
320             sub _limit_rate {
321 0     0     my $self = shift;
322 0           my $cgi = shift;
323              
324 0           my $bucket_file = File::Spec->catfile($self->{base_dir},
325             $self->{bucket_file});
326            
327             # Open the DB and lock it exclusively. See perldoc -q lock.
328 0 0         sysopen my $fh, $bucket_file, O_RDWR|O_CREAT
329             or die "Couldn't open \"$bucket_file\": $!\n";
330 0 0         flock $fh, LOCK_EX
331             or die "Couldn't flock \"$bucket_file\": $!\n";
332              
333 0           my $data = eval { fd_retrieve $fh };
  0            
334 0   0       $data ||= [$self->{max_rate}, $self->{burst_rate}];
335 0           my $bucket = Algorithm::TokenBucket->new(@$data);
336              
337 0           my $exit;
338 0 0         unless($bucket->conform(1)) {
339 0           print "Rate limiting -- please wait a bit and try again, thanks.";
340 0           $exit++;
341             }
342 0           $bucket->count(1);
343              
344 0 0         seek $fh, 0, 0 or die "Couldn't rewind \"$bucket_file\": $!\n";
345 0 0         truncate $fh, 0 or die "Couldn't truncate \"$bucket_file\": $!\n";
346              
347 0 0         store_fd [$bucket->state] => $fh or
348             croak "Couldn't serialize bucket to \"$bucket_file\": $!\n";
349              
350 0 0         exit if $exit;
351             }
352              
353             sub _validate_params {
354 0     0     my $self = shift;
355 0           my $cgi = shift;
356            
357 0 0 0       if(! $cgi->param("version") ||
358             $cgi->param("version") != PROTO_VERSION) {
359 0           print "Protocol versions do not match!";
360 0           exit;
361             }
362              
363 0 0         if(! $cgi->param("archive")) {
364 0           print "No archive given!";
365 0           exit;
366             }
367              
368             }
369              
370             sub _variables_validation_spec {
371 0     0     my $self = shift;
372 0           my $meta = shift;
373 0           my %meta = %{$meta};
  0            
374            
375 0 0         if (defined $self->{variables_validation_spec}) {
376 0           eval {
377 0           validate(@{[%meta]}, $self->{variables_validation_spec});
  0            
378             };
379 0 0 0       if (defined $@ && $@) {
380             # XXX: doesn't dump subroutines because we're using YAML::Syck
381 0           print "This server accepts specific report variables. It's validation ",
382             "string looks like this:\n", Dump($self->{variables_validation_spec}),
383             "\nYour report variables look like this:\n", Dump(\%meta);
384 0           exit;
385             }
386             }
387             }
388              
389             sub _add_report {
390 0     0     my $self = shift;
391 0           my $cgi = shift;
392              
393             # We hate CGI.pm's fake filehandle objects -- move to a real
394             # tempfile
395 0           my $archive = $cgi->upload('archive');
396 0           my $tmpfile = File::Temp->new( SUFFIX => ".tar.gz" );
397 0           print $tmpfile do {local $/; <$archive>};
  0            
  0            
398 0           close $tmpfile;
399              
400 0           my ($start, $end, $meta);
401 0           my $formatter = TAP::Formatter::HTML->new;
402 0           $formatter->verbosity(-3);
403 0           $formatter->js_uris(['/jquery-1.2.6.pack.js','/default_report.js']);
404 0           $formatter->css_uris(['/default_page.css', '/default_report.css']);
405             my $aggregator = TAP::Harness::Archive->aggregator_from_archive( {
406             archive => "$tmpfile",
407             made_parser_callback => sub {
408 0     0     my ($parser, $file, $full_path) = @_;
409 0           my $session = $formatter->open_test( $file, $parser );
410 0           while ( defined( my $result = $parser->next ) ) {
411 0           $session->result($result);
412             }
413 0           $session->close_test;
414             },
415             meta_yaml_callback => sub {
416 0     0     ($meta) = @_;
417 0           $start = $meta->[0]->{start_time};
418 0           $end = $meta->[0]->{stop_time};
419 0           $formatter->prepare(@{$meta->[0]->{file_order}});
  0            
420             }
421 0           } );
422 0           $self->_variables_validation_spec($meta->[0]{extra_properties});
423              
424             # Such a hack, but TAP::Harness::Archive doesn't store the actual benchmark values.
425 0           $aggregator->{start_time} = bless [$start, 0, 0, 0, 0, 0], 'Benchmark';
426 0           $aggregator->{end_time} = bless [$end, 0, 0, 0, 0, 0], 'Benchmark';
427              
428 0           $formatter->summary( $aggregator );
429              
430 0           my %params = (
431 0           %{$meta->[0]{extra_properties}},
432             timestamp => DateTime->from_epoch(epoch => time),
433             total_passed => scalar $aggregator->passed,
434             total_failed => scalar $aggregator->failed,
435             total_ratio => $aggregator->total ? $aggregator->passed / $aggregator->total : 0,
436             total_seen => scalar $aggregator->total,
437             total_skipped => scalar $aggregator->skipped,
438             total_todo => scalar $aggregator->todo,
439             total_unexpectedly_succeeded => scalar $aggregator->todo_passed,
440             duration => $end - $start,
441 0 0         report_html => ${$formatter->html},
442             );
443 0           my $report = Test::Chimps::Report->new( handle => $self->handle );
444 0           my ($id, $msg) = $report->create(%params);
445 0 0         unless ($id) {
446 0           open(FAIL, ">/tmp/report-fail");
447 0           print FAIL Dump(\%params);
448 0           close FAIL;
449 0           croak "Couldn't add report to database: $msg\n";
450             }
451             }
452              
453             sub _process_detail {
454 0     0     my $self = shift;
455 0           my $cgi = shift;
456              
457 0           print $cgi->header;
458            
459 0           my $id = $cgi->param("id");
460              
461 0           my $report = Test::Chimps::Report->new(handle => $self->handle);
462 0           $report->load($id);
463            
464 0           print $report->report_html;
465             }
466              
467             sub _process_listing {
468 0     0     my $self = shift;
469 0           my $cgi = shift;
470              
471 0           print $cgi->header();
472              
473 0           my @projects = map {$_->[0]} @{$self->handle->simple_query("select project from reports group by project")->fetchall_arrayref([0])};
  0            
  0            
474              
475 0           my @reports;
476 0           for my $projectname (@projects) {
477 0           my $report_coll = Test::Chimps::ReportCollection->new(handle => $self->handle);
478 0           $report_coll->limit( column => "project", value => $projectname, case_sensitive => 1 );
479 0           $report_coll->order_by( column => "timestamp", order => "DESC");
480 0           $report_coll->set_page_info( per_page => $self->max_reports_per_subcategory, current_page => 1);
481 0           $report_coll->columns(qw/id
482             project
483             revision
484             timestamp
485             committer
486             duration
487              
488             total_ratio
489             total_seen
490             total_passed
491             total_failed
492             total_todo
493             total_skipped
494             total_unexpectedly_succeeded
495             /);
496 0           while (my $report = $report_coll->next) {
497 0           push @reports, $report;
498             }
499             }
500              
501 0           my $lister;
502 0 0         if (defined $self->lister) {
503 0           $lister = $self->lister;
504             } else {
505 0           $lister = Test::Chimps::Server::Lister->new(
506             list_template => $self->list_template,
507             max_reports_per_subcategory => $self->max_reports_per_subcategory
508             );
509             }
510            
511 0           $lister->output_list(File::Spec->catdir($self->{base_dir},
512             $self->{template_dir}),
513             \@reports,
514             $cgi);
515            
516             }
517              
518             =head1 AUTHOR
519              
520             Zev Benjamin, C<< >>
521              
522             =head1 BUGS
523              
524             Please report any bugs or feature requests to
525             C, or through the web interface at
526             L.
527             I will be notified, and then you'll automatically be notified of progress on
528             your bug as I make changes.
529              
530             =head1 SUPPORT
531              
532             You can find documentation for this module with the perldoc command.
533              
534             perldoc Test::Chimps
535              
536             You can also look for information at:
537              
538             =over 4
539              
540             =item * Mailing list
541              
542             Chimps has a mailman mailing list at
543             L. You can subscribe via the web
544             interface at
545             L.
546              
547             =item * AnnoCPAN: Annotated CPAN documentation
548              
549             L
550              
551             =item * CPAN Ratings
552              
553             L
554              
555             =item * RT: CPAN's request tracker
556              
557             L
558              
559             =item * Search CPAN
560              
561             L
562              
563             =back
564              
565             =head1 ACKNOWLEDGEMENTS
566              
567             Some code in this distribution is based on smokeserv-server.pl from
568             the Pugs distribution.
569              
570             =head1 COPYRIGHT & LICENSE
571              
572             Copyright 2006 Best Practical Solutions.
573             Portions copyright 2005-2006 the Pugs project.
574              
575             This program is free software; you can redistribute it and/or modify it
576             under the same terms as Perl itself.
577              
578             =cut
579            
580             1;