2 # This CGI script presents the results of the build_farm build
4 # Copyright (C) Andrew Tridgell <tridge@samba.org> 2001-2005
5 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2001
6 # Copyright (C) Vance Lankhaar <vance@samba.org> 2002-2005
7 # Copyright (C) Martin Pool <mbp@samba.org> 2001
8 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24 # TODO: Allow filtering of the "Recent builds" list to show
25 # e.g. only broken builds or only builds that you care about.
30 use FindBin qw($RealBin);
33 use data qw(@compilers %hosts @hosts %trees @pseudo_trees $OLDAGE $DEADAGE);
38 use CGI qw/:standard/;
41 my $WEBDIR = "$RealBin";
42 my $BASEDIR = "$WEBDIR/..";
44 my $db = new data("$BASEDIR/data");
48 # this is automatically filled in
51 ###############################################
52 # URL so I can refer to myself in links
53 my $myself = $req->url();
55 ####################################################################
56 # setup for gzipped output of a web page if possible.
57 # based on cvsweb.pl method
58 # as a side effect this function adds the final line ot the HTTP headers
61 my $paths = ['/usr/bin/gzip', '/bin/gzip'];
63 my $Browser = $ENV{'HTTP_USER_AGENT'} || "";
65 # newer browsers accept gzip content encoding
66 # and state this in a header
67 # (netscape did always but didn't state it)
68 # It has been reported that these
69 # braindamaged MS-Internet Exploders claim that they
70 # accept gzip .. but don't in fact and
71 # display garbage then :-/
72 # Turn off gzip if running under mod_perl. piping does
73 # not work as expected inside the server. One can probably
74 # achieve the same result using Apache::GZIPFilter.
75 my $maycompress = ((defined($ENV{'HTTP_ACCEPT_ENCODING'}) and $ENV{'HTTP_ACCEPT_ENCODING'} =~ m|gzip|
76 || $Browser =~ m%^Mozilla/3%)
77 && ($Browser !~ m/MSIE/)
78 && !defined($ENV{'MOD_PERL'}));
80 if (!$maycompress) { print "\r\n"; return; }
82 for my $p (@{$paths}) {
83 if (stat($p)) { $GZIPBIN = $p; }
86 my $fh = do {local(*FH);};
88 if (stat($GZIPBIN) && open($fh, "|$GZIPBIN -1 -c")) {
89 print header(-content_type => "text/html", -content_encoding => "gzip", -vary => "Accept-Encoding");
90 $| = 1; $| = 0; # Flush header output
97 ################################################
101 print start_html(-title => 'samba.org build farm',
102 -script => {-language=> 'JAVASCRIPT', -src=>"/build_farm.js" },
104 -keywords => "Samba SMB CIFS Build Farm",
105 -description => "Home of the Samba Build Farm, the automated testing facility.",
110 Link({-rel => "stylesheet",
111 -href => "/build_farm.css",
114 Link({-rel => "stylesheet",
115 -href => "http://master.samba.org/samba/style/common.css",
118 Link({-rel=>"shortcut icon",
119 -href=>"http://www.samba.org/samba/images/favicon.ico"})
123 print util::FileLoad("$WEBDIR/header2.html");
125 print util::FileLoad("$WEBDIR/header3.html");
128 ################################################
131 print util::FileLoad("$WEBDIR/footer.html");
132 print $req->end_html;
135 ################################################
136 # print an error on fatal errors
140 print $req->h1("ERROR: $msg");
145 ################################################
146 # get a param from the request, after sanitizing it
150 if (!defined $req->param($param)) {
151 return wantarray ? () : undef;
156 @result = $req->param($param);
158 $result[0] = $req->param($param);
161 for (my $i = 0; $i <= $#result; $i++) {
162 $result[$i] =~ s/ /_/g;
166 if ($_ =~ m/[^a-zA-Z0-9\-\_\.]/) {
167 fatal("Parameter $param is invalid");
168 return wantarray ? () : undef;
172 return wantarray ? @result : $result[0];
175 sub build_link($$$$$)
177 my ($host, $tree, $compiler, $rev, $status) = @_;
179 return a({-href=>"$myself?function=View+Build;host=$host;tree=$tree;compiler=$compiler" . ($rev?";revision=$rev":"")}, $status);
182 sub build_status($$$$)
184 my ($host, $tree, $compiler, $rev) = @_;
185 my $status = $db->build_status($host, $tree, $compiler, $rev);
187 return build_link($host, $tree, $compiler, $rev, $status);
191 #############################################
192 # get the overall age of a host
197 for my $compiler (@compilers) {
198 for my $tree (keys %trees) {
199 my $age = $db->build_age_mtime($host, $tree, $compiler, "");
200 if ($age != -1 && ($age < $ret || $ret == -1)) {
208 #############################################
209 # show an age as a string
214 if ($age > $OLDAGE) {
215 return $req->span({-class=>"old"}, util::dhm_time($age));
217 return util::dhm_time($age);
220 ##############################################
221 # translate a status into a set of int representing status
222 sub build_status_vals($) {
223 my $status = util::strip_html(shift);
227 $status =~ s/PANIC/1/g;
229 return split m%/%, $status;
232 ##############################################
240 # either "text" or anything else.
241 my $output_type = shift;
248 # zero broken and panic counters
249 for my $tree (keys %trees) {
250 $broken_count{$tree} = 0;
251 $panic_count{$tree} = 0;
252 $host_count{$tree} = 0;
255 # set up a variable to store the broken builds table's code, so we can output when we want
256 my $broken_table = "";
260 # for the text report, include the current time
261 if ($output_type eq 'text') {
263 print "Build status as of $time\n\n";
266 for my $host (@hosts) {
267 for my $compiler (@compilers) {
268 for my $tree (keys %trees) {
269 my $status = build_status($host, $tree, $compiler, "");
270 next if $status =~ /^Unknown Build/;
271 my $age_mtime = $db->build_age_mtime($host, $tree, $compiler, "");
273 if ($age_mtime != -1 && $age_mtime < $DEADAGE) {
274 $host_count{$tree}++;
277 if ($age_mtime < $DEADAGE && $status =~ /status failed/) {
278 $broken_count{$tree}++;
279 if ($status =~ /PANIC/) {
280 $panic_count{$tree}++;
287 if ($output_type eq 'text') {
288 print "Build counts:\n";
289 printf "%-12s %-6s %-6s %-6s\n", "Tree", "Total", "Broken", "Panic";
291 print $req->start_div({-id=>"build-counts", -class=>"build-section"});
292 print $req->h2('Build counts:');
293 print $req->start_table({-class => "real"}),
295 $req->Tr($req->th("Tree"), $req->th("Total"),
296 $req->th("Broken"), $req->th("Panic"),
297 $req->th("Test Coverage"))),
301 for my $tree (sort keys %trees) {
302 if ($output_type eq 'text') {
303 printf "%-12s %-6s %-6s %-6s\n", $tree, $host_count{$tree},
304 $broken_count{$tree}, $panic_count{$tree};
306 print $req->start_Tr;
307 print $req->td(tree_link($tree));
308 print $req->td($host_count{$tree});
309 print $req->td($broken_count{$tree});
310 if ($panic_count{$tree}) {
311 print $req->start_td({-class => "panic"});
313 print $req->start_td;
315 print $panic_count{$tree} . $req->end_td;
316 print $req->td($db->lcov_status($tree));
317 print $req->end_Tr . "\n";
321 if ($output_type eq 'text') {
324 print $req->end_tbody, $req->end_table;
329 ##############################################
330 # return a link to a particular revision
331 sub revision_link($$)
333 my ($revision, $tree) = @_;
335 $revision =~ s/^\s+//g;
336 return "0" if ($revision eq "0");
339 -href=>"$myself?function=diff;tree=$tree;revision=$revision",
340 -title=>"View Diff for $revision"
344 ###############################################
345 # return a link to a particular tree
350 return $req->a({-href=>"$myself?function=Recent+Builds;tree=$tree",
351 -title=>"View recent builds for $tree"}, $tree);
354 ##############################################
355 # Draw the "recent builds" view
356 sub view_recent_builds($$) {
357 my ($tree, $sort_by) = @_;
365 my $sort = { revision => sub {
366 util::strip_html($$b[6]) <=> util::strip_html($$a[6])
368 age => sub { $$a[0] <=> $$b[0] },
369 host => sub { $$a[2] cmp $$b[2] },
370 platform => sub { $$a[1] cmp $$b[1] },
371 compiler => sub { $$a[3] cmp $$b[3] },
373 my (@bstat) = build_status_vals($$b[5]);
374 my (@astat) = build_status_vals($$a[5]);
377 if (defined $bstat[4] && !defined $astat[4]) {
379 } elsif (!defined $bstat[4] && defined $astat[4]) {
382 return ($bstat[0] <=> $astat[0] || # configure
383 $bstat[1] <=> $astat[1] || # compile
384 $bstat[2] <=> $astat[2] || # install
385 $bstat[3] <=> $astat[3] # test
390 util::InArray($tree, [keys %trees]) || fatal("not a build tree");
391 util::InArray($sort_by, [keys %$sort]) || fatal("not a valid sort");
393 for my $host (@hosts) {
394 for my $compiler (@compilers) {
395 my $status = build_status($host, $tree, $compiler, "");
396 my $age_mtime = $db->build_age_mtime($host, $tree, $compiler, "");
397 my $age_ctime = $db->build_age_ctime($host, $tree, $compiler, "");
398 my $revision = $db->build_revision($host, $tree, $compiler, "");
399 push @all_builds, [$age_ctime, $hosts{$host}, $req->a({-href=>"$myself?function=View+Host;host=$host;tree=$tree;compiler=$compiler#$host"}, $host), $compiler, $tree, $status, revision_link($revision, $tree)]
400 unless $age_mtime == -1 or $age_mtime >= $DEADAGE;
404 @all_builds = sort { $sort->{$sort_by}() || $sort->{age}() } @all_builds;
406 my $sorturl = "$myself?tree=$tree;function=Recent+Builds";
408 print $req->start_div({-id=>"recent-builds", -class=>"build-section"}),
409 $req->h2("Recent builds of $tree"),
410 $req->start_table({-class => "real"}),
414 $req->a({-href => "$sorturl;sortby=age",
415 -title => "Sort by build age"}, "Age"),
416 $req->a({-href => "$sorturl;sortby=revision",
417 -title => "Sort by build revision"},
420 $req->a({-href => "$sorturl;sortby=platform",
421 -title => "Sort by platform"}, "Platform"),
422 $req->a({-href => "$sorturl;sortby=host",
423 -title => "Sort by host"}, "Host"),
424 $req->a({-href=>"$sorturl;sortby=compiler",
425 -title=>"Sort by compiler"}, "Compiler"),
426 $req->a({-href=>"$sorturl;sortby=status",
427 -title=>"Sort by build status"}, "Status")]
432 for my $build (@all_builds) {
434 $req->td([util::dhm_time($$build[0]), $$build[6], $$build[4],
435 $$build[1], $$build[2], $$build[3], $$build[5]]));
437 print $req->end_tbody, $req->end_table;
441 ##############################################
442 # Draw the "dead hosts" table
443 sub draw_dead_hosts {
444 my $output_type = shift;
447 # don't output anything if there are no dead hosts
448 return if ($#deadhosts < 0);
450 # don't include in text report
451 return if ($output_type eq 'text');
453 print $req->start_div({-class => "build-section", -id=>"dead-hosts"}),
454 $req->h2('Dead Hosts:'),
455 $req->start_table({-class => "real"}),
456 $req->thead($req->Tr($req->th(["Host", "OS", "Min Age"]))),
459 for my $host (@deadhosts) {
460 my $age_ctime = host_age($host);
461 print $req->tr($req->td([$host, $hosts{$host}, util::dhm_time($age_ctime)]));
464 print $req->end_tbody, $req->end_table;
468 ##############################################
469 # show the available old revisions, if any
470 sub show_oldrevs($$$)
472 my ($tree, $host, $compiler) = @_;
473 my %revs = $db->get_old_revs($tree, $host, $compiler);
474 my @revs = sort { $revs{$b} cmp $revs{$a} } keys %revs;
476 return if ($#revs < 1);
478 my $ret = $req->h2("Older builds:");
480 $ret .= $req->start_table({-class => "real"}).
481 $req->thead($req->Tr($req->th(["Revision", "Status"]))).
486 for my $rev (@revs) {
489 next if ($s eq $lastrev);
491 $ret.=$req->Tr($req->td([revision_link($rev, $tree), build_link($host, $tree, $compiler, $rev, $revs{$rev})]));
493 if ($lastrev ne "") {
494 # Only print table if there was any actual data
495 print $ret . $req->end_tbody, $req->end_table;
499 ##############################################
500 # view one build in detail
501 sub view_build($$$$) {
502 my ($tree, $host, $compiler, $rev) = @_;
503 # ensure the params are valid before using them
504 util::InArray($host, [keys %hosts]) || fatal("unknown host");
505 util::InArray($compiler, \@compilers) || fatal("unknown compiler");
506 util::InArray($tree, [keys %trees]) || fatal("not a build tree");
511 my $age_mtime = $db->build_age_mtime($host, $tree, $compiler, $rev);
512 my $revision = $db->build_revision($host, $tree, $compiler, $rev);
513 my $status = build_status($host, $tree, $compiler, $rev);
515 $rev = int($rev) if $rev;
517 my $log = $db->read_log($tree, $host, $compiler, $rev);
518 my $err = $db->read_err($tree, $host, $compiler, $rev);
521 $log = escapeHTML($log);
523 if ($log =~ /(.*)/) { $uname=$1; }
524 if ($log =~ /CFLAGS=(.*)/) { $cflags=$1; }
525 if ($log =~ /configure options: (.*)/) { $config=$1; }
529 $err = escapeHTML($err);
532 print $req->h2('Host information:');
534 print util::FileLoad("../web/$host.html");
536 print $req->table({-class=>"real"},
538 $req->td(["Host:", $req->a({-href=>"$myself?function=View+Host;host=$host;tree=$tree;compiler=$compiler#$host"}, $host)." - $hosts{$host}"]),
539 $req->td(["Uname:", $uname]),
540 $req->td(["Tree:", tree_link($tree)]),
541 $req->td(["Build Revision:", revision_link($revision, $tree)]),
542 $req->td(["Build age:", $req->div({-class=>"age"}, red_age($age_mtime))]),
543 $req->td(["Status:", $status]),
544 $req->td(["Compiler:", $compiler]),
545 $req->td(["CFLAGS:", $cflags]),
546 $req->td(["configure options:", $config])]));
548 show_oldrevs($tree, $host, $compiler);
550 # check the head of the output for our magic string
551 my $plain_logs = (defined get_param("plain") &&
552 get_param("plain") =~ /^(yes|1|on|true|y)$/i);
555 $rev_var = ";revision=$rev";
558 print $req->start_div({-id=>"log"});
561 print $req->p("Switch to the ".$req->a({-href => "$myself?function=View+Build;host=$host;tree=$tree;compiler=$compiler$rev_var;plain=true", -title=> "Switch to bland, non-javascript, unstyled view"}, "Plain View"));
563 print $req->start_div({-id=>"actionList"});
564 # These can be pretty wide -- perhaps we need to
565 # allow them to wrap in some way?
567 print $req->h2("No error log available");
569 print $req->h2("Error log:");
570 print make_collapsible_html('action', "Error Output", "\n$err", "stderr-0");
574 print $req->h2("No build log available");
576 print $req->h2("Build log:");
577 print_log_pretty($log);
580 print $req->p($req->small("Some of the above icons derived from the ".$req->a({-href=>"http://www.gnome.org"}, "Gnome Project")."'s stock icons."));
583 print $req->p("Switch to the ".$req->a({-href=>"$myself?function=View+Build;host=$host;tree=$tree;compiler=$compiler$rev_var", -title=>"Switch to colourful, javascript-enabled, styled view"}, "Enhanced View"));
585 print $req->h2("No error log available");
587 print $req->h2('Error log:');
588 print $req->div({-id=>"errorLog"}, $req->pre($err));
591 print $req->h2('No build log available');
594 print $req->h2('Build log:');
595 print $req->div({-id=>"buildLog"}, $req->pre($log));
602 ##################################################
603 # print the host's table of information
605 my (@requested_hosts) = @_;
607 my $output_type = "html";
609 if ($output_type eq 'text') {
610 print "Host summary:\n";
612 print $req->start_div({-class=>"build-section", -id=>"build-summary"});
613 print $req->h2('Host summary:');
616 foreach (@requested_hosts) {
617 util::InArray($_, [keys %hosts]) || fatal("unknown host");
620 for my $host (@requested_hosts) {
621 # make sure we have some data from it
622 unless($db->has_host($host)) {
623 if ($output_type ne 'text') {
624 print $req->comment("skipping $host");
631 for my $compiler (@compilers) {
632 for my $tree (sort keys %trees) {
633 my $revision = $db->build_revision($host, $tree, $compiler, "");
634 my $age_mtime = $db->build_age_mtime($host, $tree, $compiler, "");
635 my $age_ctime = $db->build_age_ctime($host, $tree, $compiler, "");
636 my $warnings = $db->err_count($host, $tree, $compiler, "");
637 if ($age_ctime != -1 && $age_ctime < $DEADAGE) {
638 my $status = build_status($host, $tree, $compiler, "");
640 if ($output_type eq 'text') {
641 printf "%-12s %-10s %-10s %-10s %-10s\n",
642 "Tree", "Compiler", "Build Age", "Status", "Warnings";
645 print $req->start_div({-class=>"host summary"}),
646 $req->a({-id=>$host, -name=>$host}),
647 $req->h3("$host - $hosts{$host}"),
648 $req->start_table({-class=>"real"}),
649 $req->thead($req->Tr(
650 $req->th(["Target", "Build<br/>Revision", "Build<br />Age", "Status<br />config/build<br />install/test", "Warnings"]))),
655 if ($output_type eq 'text') {
656 printf "%-12s %-10s %-10s %-10s %-10s\n",
657 $tree, $compiler, util::dhm_time($age_mtime),
658 util::strip_html($status), $warnings;
660 print $req->Tr($req->td([$req->span({-class=>"tree"}, tree_link($tree))."/$compiler", revision_link($revision, $tree), $req->div({-class=>"age"}, red_age($age_mtime)), $req->div({-class=>"status"}, $status), $warnings]));
667 if ($output_type eq 'text') {
670 print $req->end_tbody, $req->end_table;
674 push(@deadhosts, $host);
678 if ($output_type ne 'text') {
682 draw_dead_hosts($output_type, @deadhosts);
685 ##############################################
686 # prints the log in a visually appealing manner
687 sub print_log_pretty() {
690 # do some pretty printing for the actions
693 Running\ action\s+([\w\-]+)
695 ACTION\ (PASSED|FAILED):\ ([\w\-]+)
701 # handle pretty-printing of static-analysis tools
702 if ($actionName eq 'cc_checker') {
703 $output = print_log_cc_checker($output);
706 make_collapsible_html('action', $actionName, $output, $id++,
710 # $log is already CGI-escaped, so handle '>' in test name by handling >
712 --==--==--==--==--==--==--==--==--==--==--.*?
713 Running\ test\ ([\w\-=,_:\ /.&;]+).*?
714 --==--==--==--==--==--==--==--==--==--==--
716 ==========================================.*?
717 TEST\ (FAILED|PASSED|SKIPPED):.*?
718 ==========================================\s+
719 }{make_collapsible_html('test', $1, $2, $id++, $3)}exgs;
721 print $req->p($req->tt($req->pre($log)))."\n";
724 ##############################################
725 # generate pretty-printed html for static analysis tools
726 sub print_log_cc_checker($) {
730 # for now, we only handle the IBM Checker's output style
731 if ($input !~ m/^BEAM_VERSION/ms) {
739 my ($entry, $title, $status, $id);
741 foreach (split /\n/, $input) {
743 # for each line, check if the line is a new entry,
744 # otherwise, store the line under the current entry.
749 $output .= make_collapsible_html('cc_checker', $title, $content,
755 # clear maintenance vars
756 ($inEntry, $content) = (1, "");
759 m/^-- ((ERROR|WARNING|MISTAKE).*?)\s+>>>([a-zA-Z0-9]+_(\w+)_[a-zA-Z0-9]+)/;
761 # then store the result
762 ($title, $status, $id) = ("$1 $4", $2, $3);
763 } elsif (m/^CC_CHECKER STATUS/) {
765 $output .= make_collapsible_html('cc_checker', $title, $content,
773 # not a new entry, so part of the current entry's output
778 # This function does approximately the same as the following, following
779 # commented-out regular expression except that the regex doesn't quite
780 # handle IBM Checker's newlines quite right.
782 # --\ ((ERROR|WARNING|MISTAKE).*?)\s+
786 # }{make_collapsible_html('cc_checker', "$1 $4", $5, $3, $2)}exgs;
790 ##############################################
791 # generate html for a collapsible section
792 sub make_collapsible_html
794 my $type = shift; # the logical type of it. e.g. "test" or "action"
795 my $title = shift; # the title to be displayed
798 my $status = (shift or "");
800 my $icon = (defined $status && ($status =~ /failed/i)) ? 'icon_hide_16.png' : 'icon_unhide_16.png';
802 # trim leading and trailing whitespace
803 $output =~ s/^\s+//s;
804 $output =~ s/\s+$//s;
806 # note that we may be inside a <pre>, so we don't put any extra whitespace in this html
807 return $req->div({-class=>"$type unit \L$status\E",
809 $req->a({-href=>"javascript:handle('$id');"},
810 $req->img({-id=>"img-$id", -name=>"img-$id",
813 $req->div({-class => "$type title"}, $title),
815 $req->div({-class=> "$type status \L$status\E"}, $status) .
816 $req->div({-class => "$type output", -id=>"output-$id"}, $req->pre($output)));
819 ##############################################
822 return $req->startform("GET"),
823 $req->start_div({-id=>"build-menu"}),
824 $req->popup_menu(-name=>'host',
827 $req->popup_menu("tree", [sort (keys %trees, @pseudo_trees)]),
828 $req->popup_menu("compiler", \@compilers),
830 $req->submit('function', 'View Build'),
831 $req->submit('function', 'View Host'),
832 $req->submit('function', 'Recent Checkins'),
833 $req->submit('function', 'Summary'),
834 $req->submit('function', 'Recent Builds'),
836 $req->endform() . "\n";
839 ###############################################
840 # display top of page
845 ###############################################
848 my $fn_name = get_param('function') || '';
850 if ($fn_name eq 'text_diff') {
851 print header('application/x-diff');
852 history::diff(get_param('author'),
855 get_param('revision'),
857 } elsif ($fn_name eq 'Text_Summary') {
858 print header('text/plain');
859 view_summary('text');
863 if ($fn_name eq "View_Build") {
864 view_build(get_param("tree"), get_param("host"), get_param("compiler"),
865 get_param('revision'));
866 } elsif ($fn_name eq "View_Host") {
867 view_host(get_param('host'));
868 } elsif ($fn_name eq "Recent_Builds") {
869 view_recent_builds(get_param("tree"), get_param("sortby") || "revision");
870 } elsif ($fn_name eq "Recent_Checkins") {
871 history::history(get_param('tree'));
872 } elsif ($fn_name eq "diff") {
873 history::diff(get_param('author'),
876 get_param('revision'),
878 } elsif (path_info() ne "" and path_info() ne "/") {
879 my @paths = split('/', path_info());
880 if ($paths[1] eq "recent") {
881 view_recent_builds($paths[2], get_param('sortby') || 'revision');
882 } elsif ($paths[1] eq "host") {
883 view_host($paths[2]);
886 view_summary('html');