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 ################################################
59 print start_html(-title => 'samba.org build farm',
60 -script => {-language=> 'JAVASCRIPT', -src=>"/build_farm.js" },
62 -keywords => "Samba SMB CIFS Build Farm",
63 -description => "Home of the Samba Build Farm, the automated testing facility.",
68 Link({-rel => "stylesheet",
69 -href => "/build_farm.css",
72 Link({-rel => "stylesheet",
73 -href => "http://master.samba.org/samba/style/common.css",
76 Link({-rel=>"shortcut icon",
77 -href=>"http://www.samba.org/samba/images/favicon.ico"})
81 print util::FileLoad("$WEBDIR/header2.html");
83 print util::FileLoad("$WEBDIR/header3.html");
86 ################################################
89 print util::FileLoad("$WEBDIR/footer.html");
93 ################################################
94 # print an error on fatal errors
98 print $req->h1("ERROR: $msg");
103 ################################################
104 # get a param from the request, after sanitizing it
108 if (!defined $req->param($param)) {
109 return wantarray ? () : undef;
114 @result = $req->param($param);
116 $result[0] = $req->param($param);
119 for (my $i = 0; $i <= $#result; $i++) {
120 $result[$i] =~ s/ /_/g;
124 if ($_ =~ m/[^a-zA-Z0-9\-\_\.]/) {
125 fatal("Parameter $param is invalid");
126 return wantarray ? () : undef;
130 return wantarray ? @result : $result[0];
133 sub build_status($$$$)
135 my ($host, $tree, $compiler, $rev) = @_;
137 return a({-href=>"$myself?function=View+Build;host=$host;tree=$tree;compiler=$compiler" . ($rev?";revision=$rev":"")}, $db->build_status($host, $tree, $compiler, $rev));
141 #############################################
142 # get the overall age of a host
147 for my $compiler (@compilers) {
148 for my $tree (keys %trees) {
149 my $age = $db->build_age_mtime($host, $tree, $compiler, "");
150 if ($age != -1 && ($age < $ret || $ret == -1)) {
158 #############################################
159 # show an age as a string
164 if ($age > $OLDAGE) {
165 return $req->span({-class=>"old"}, util::dhm_time($age));
167 return util::dhm_time($age);
170 ##############################################
171 # translate a status into a set of int representing status
172 sub build_status_vals($) {
173 my $status = util::strip_html(shift);
177 $status =~ s/PANIC/1/g;
179 return split m%/%, $status;
182 ##############################################
190 # either "text" or anything else.
191 my $output_type = shift;
198 # zero broken and panic counters
199 for my $tree (keys %trees) {
200 $broken_count{$tree} = 0;
201 $panic_count{$tree} = 0;
202 $host_count{$tree} = 0;
205 # set up a variable to store the broken builds table's code, so we can output when we want
206 my $broken_table = "";
210 # for the text report, include the current time
211 if ($output_type eq 'text') {
213 print "Build status as of $time\n\n";
216 for my $host (@hosts) {
217 for my $compiler (@compilers) {
218 for my $tree (keys %trees) {
219 my $status = build_status($host, $tree, $compiler, "");
220 next if $status =~ /^Unknown Build/;
221 my $age_mtime = $db->build_age_mtime($host, $tree, $compiler, "");
223 if ($age_mtime != -1 && $age_mtime < $DEADAGE) {
224 $host_count{$tree}++;
227 if ($age_mtime < $DEADAGE && $status =~ /status failed/) {
228 $broken_count{$tree}++;
229 if ($status =~ /PANIC/) {
230 $panic_count{$tree}++;
237 if ($output_type eq 'text') {
238 print "Build counts:\n";
239 printf "%-12s %-6s %-6s %-6s\n", "Tree", "Total", "Broken", "Panic";
241 print $req->start_div({-id=>"build-counts", -class=>"build-section"});
242 print $req->h2('Build counts:');
243 print $req->start_table({-class => "real"}),
245 $req->Tr($req->th("Tree"), $req->th("Total"),
246 $req->th("Broken"), $req->th("Panic"),
247 $req->th("Test Coverage"))),
251 for my $tree (sort keys %trees) {
252 if ($output_type eq 'text') {
253 printf "%-12s %-6s %-6s %-6s\n", $tree, $host_count{$tree},
254 $broken_count{$tree}, $panic_count{$tree};
256 print $req->start_Tr;
257 print $req->td(tree_link($tree));
258 print $req->td($host_count{$tree});
259 print $req->td($broken_count{$tree});
260 if ($panic_count{$tree}) {
261 print $req->start_td({-class => "panic"});
263 print $req->start_td;
265 print $panic_count{$tree} . $req->end_td;
266 print $req->td($db->lcov_status($tree));
267 print $req->end_Tr . "\n";
271 if ($output_type eq 'text') {
274 print $req->end_tbody, $req->end_table;
279 ##############################################
280 # return a link to a particular revision
281 sub revision_link($$)
283 my ($revision, $tree) = @_;
285 $revision =~ s/^\s+//g;
286 return "0" if ($revision eq "0");
289 -href=>"$myself?function=diff;tree=$tree;revision=$revision",
290 -title=>"View Diff for $revision"
294 ###############################################
295 # return a link to a particular tree
300 return $req->a({-href=>"$myself?function=Recent+Builds;tree=$tree",
301 -title=>"View recent builds for $tree"}, $tree);
304 ##############################################
305 # Draw the "recent builds" view
306 sub view_recent_builds($$) {
307 my ($tree, $sort_by) = @_;
315 my $sort = { revision => sub {
316 util::strip_html($$b[6]) <=> util::strip_html($$a[6])
318 age => sub { $$a[0] <=> $$b[0] },
319 host => sub { $$a[2] cmp $$b[2] },
320 platform => sub { $$a[1] cmp $$b[1] },
321 compiler => sub { $$a[3] cmp $$b[3] },
323 my (@bstat) = build_status_vals($$b[5]);
324 my (@astat) = build_status_vals($$a[5]);
327 if (defined $bstat[4] && !defined $astat[4]) {
329 } elsif (!defined $bstat[4] && defined $astat[4]) {
332 return ($bstat[0] <=> $astat[0] || # configure
333 $bstat[1] <=> $astat[1] || # compile
334 $bstat[2] <=> $astat[2] || # install
335 $bstat[3] <=> $astat[3] # test
340 util::InArray($tree, [keys %trees]) || fatal("not a build tree");
341 util::InArray($sort_by, [keys %$sort]) || fatal("not a valid sort");
343 for my $host (@hosts) {
344 for my $compiler (@compilers) {
345 my $status = build_status($host, $tree, $compiler, "");
346 my $age_mtime = $db->build_age_mtime($host, $tree, $compiler, "");
347 my $age_ctime = $db->build_age_ctime($host, $tree, $compiler, "");
348 my $revision = $db->build_revision($host, $tree, $compiler, "");
349 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)]
350 unless $age_mtime == -1 or $age_mtime >= $DEADAGE;
354 @all_builds = sort { $sort->{$sort_by}() || $sort->{age}() } @all_builds;
356 my $sorturl = "$myself?tree=$tree;function=Recent+Builds";
358 print $req->start_div({-id=>"recent-builds", -class=>"build-section"}),
359 $req->h2("Recent builds of $tree"),
360 $req->start_table({-class => "real"}),
364 $req->a({-href => "$sorturl;sortby=age",
365 -title => "Sort by build age"}, "Age"),
366 $req->a({-href => "$sorturl;sortby=revision",
367 -title => "Sort by build revision"},
370 $req->a({-href => "$sorturl;sortby=platform",
371 -title => "Sort by platform"}, "Platform"),
372 $req->a({-href => "$sorturl;sortby=host",
373 -title => "Sort by host"}, "Host"),
374 $req->a({-href=>"$sorturl;sortby=compiler",
375 -title=>"Sort by compiler"}, "Compiler"),
376 $req->a({-href=>"$sorturl;sortby=status",
377 -title=>"Sort by build status"}, "Status")]
382 for my $build (@all_builds) {
384 $req->td([util::dhm_time($$build[0]), $$build[6], $$build[4],
385 $$build[1], $$build[2], $$build[3], $$build[5]]));
387 print $req->end_tbody, $req->end_table;
391 ##############################################
392 # Draw the "dead hosts" table
393 sub draw_dead_hosts {
394 my $output_type = shift;
397 # don't output anything if there are no dead hosts
398 return if ($#deadhosts < 0);
400 # don't include in text report
401 return if ($output_type eq 'text');
403 print $req->start_div({-class => "build-section", -id=>"dead-hosts"}),
404 $req->h2('Dead Hosts:'),
405 $req->start_table({-class => "real"}),
406 $req->thead($req->Tr($req->th(["Host", "OS", "Min Age"]))),
409 for my $host (@deadhosts) {
410 my $age_ctime = host_age($host);
411 print $req->tr($req->td([$host, $hosts{$host}, util::dhm_time($age_ctime)]));
414 print $req->end_tbody, $req->end_table;
418 ##############################################
419 # show the available old revisions, if any
420 sub show_oldrevs($$$)
422 my ($tree, $host, $compiler) = @_;
423 my %revs = $db->get_old_revs($tree, $host, $compiler);
424 my @revs = sort { $revs{$b} cmp $revs{$a} } keys %revs;
426 return if ($#revs < 1);
428 my $ret = $req->h2("Older builds:");
430 $ret .= $req->start_table({-class => "real"}).
431 $req->thead($req->Tr($req->th(["Revision", "Status"]))).
436 for my $rev (@revs) {
439 next if ($s eq $lastrev);
441 $ret.=$req->Tr($req->td([revision_link($rev, $tree), $revs{$rev}]));
443 if ($lastrev ne "") {
444 # Only print table if there was any actual data
445 print $ret . $req->end_tbody, $req->end_table;
449 ##############################################
450 # view one build in detail
451 sub view_build($$$$) {
452 my ($tree, $host, $compiler, $rev) = @_;
453 # ensure the params are valid before using them
454 util::InArray($host, [keys %hosts]) || fatal("unknown host");
455 util::InArray($compiler, \@compilers) || fatal("unknown compiler");
456 util::InArray($tree, [keys %trees]) || fatal("not a build tree");
461 my $age_mtime = $db->build_age_mtime($host, $tree, $compiler, $rev);
462 my $revision = $db->build_revision($host, $tree, $compiler, $rev);
463 my $status = build_status($host, $tree, $compiler, $rev);
465 $rev = int($rev) if $rev;
467 my $log = $db->read_log($tree, $host, $compiler, $rev);
468 my $err = $db->read_err($tree, $host, $compiler, $rev);
471 $log = escapeHTML($log);
473 if ($log =~ /(.*)/) { $uname=$1; }
474 if ($log =~ /CFLAGS=(.*)/) { $cflags=$1; }
475 if ($log =~ /configure options: (.*)/) { $config=$1; }
479 $err = escapeHTML($err);
482 print $req->h2('Host information:');
484 print util::FileLoad("../web/$host.html");
486 print $req->table({-class=>"real"},
488 $req->td(["Host:", $req->a({-href=>"$myself?function=View+Host;host=$host;tree=$tree;compiler=$compiler#$host"}, $host)." - $hosts{$host}"]),
489 $req->td(["Uname:", $uname]),
490 $req->td(["Tree:", tree_link($tree)]),
491 $req->td(["Build Revision:", revision_link($revision, $tree)]),
492 $req->td(["Build age:", $req->div({-class=>"age"}, red_age($age_mtime))]),
493 $req->td(["Status:", $status]),
494 $req->td(["Compiler:", $compiler]),
495 $req->td(["CFLAGS:", $cflags]),
496 $req->td(["configure options:", $config])]));
498 show_oldrevs($tree, $host, $compiler);
500 # check the head of the output for our magic string
501 my $plain_logs = (defined get_param("plain") &&
502 get_param("plain") =~ /^(yes|1|on|true|y)$/i);
505 $rev_var = ";revision=$rev";
508 print $req->start_div({-id=>"log"});
511 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"));
513 print $req->start_div({-id=>"actionList"});
514 # These can be pretty wide -- perhaps we need to
515 # allow them to wrap in some way?
517 print $req->h2("No error log available");
519 print $req->h2("Error log:");
520 print make_collapsible_html('action', "Error Output", "\n$err", "stderr-0");
524 print $req->h2("No build log available");
526 print $req->h2("Build log:");
527 print_log_pretty($log);
530 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."));
533 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"));
535 print $req->h2("No error log available");
537 print $req->h2('Error log:');
538 print $req->div({-id=>"errorLog"}, $req->pre($err));
541 print $req->h2('No build log available');
544 print $req->h2('Build log:');
545 print $req->div({-id=>"buildLog"}, $req->pre($log));
552 ##################################################
553 # print the host's table of information
555 my (@requested_hosts) = @_;
557 my $output_type = "html";
559 if ($output_type eq 'text') {
560 print "Host summary:\n";
562 print $req->start_div({-class=>"build-section", -id=>"build-summary"});
563 print $req->h2('Host summary:');
566 foreach (@requested_hosts) {
567 util::InArray($_, [keys %hosts]) || fatal("unknown host");
570 for my $host (@requested_hosts) {
571 # make sure we have some data from it
572 unless($db->has_host($host)) {
573 if ($output_type ne 'text') {
574 print $req->comment("skipping $host");
581 for my $compiler (@compilers) {
582 for my $tree (sort keys %trees) {
583 my $revision = $db->build_revision($host, $tree, $compiler, "");
584 my $age_mtime = $db->build_age_mtime($host, $tree, $compiler, "");
585 my $age_ctime = $db->build_age_ctime($host, $tree, $compiler, "");
586 my $warnings = $db->err_count($host, $tree, $compiler, "");
587 if ($age_ctime != -1 && $age_ctime < $DEADAGE) {
588 my $status = build_status($host, $tree, $compiler, "");
590 if ($output_type eq 'text') {
591 printf "%-12s %-10s %-10s %-10s %-10s\n",
592 "Tree", "Compiler", "Build Age", "Status", "Warnings";
595 print $req->start_div({-class=>"host summary"}),
596 $req->a({-id=>$host, -name=>$host}),
597 $req->h3("$host - $hosts{$host}"),
598 $req->start_table({-class=>"real"}),
599 $req->thead($req->Tr(
600 $req->th(["Target", "Build<br/>Revision", "Build<br />Age", "Status<br />config/build<br />install/test", "Warnings"]))),
605 if ($output_type eq 'text') {
606 printf "%-12s %-10s %-10s %-10s %-10s\n",
607 $tree, $compiler, util::dhm_time($age_mtime),
608 util::strip_html($status), $warnings;
610 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]));
617 if ($output_type eq 'text') {
620 print $req->end_tbody, $req->end_table;
624 push(@deadhosts, $host);
628 if ($output_type ne 'text') {
632 draw_dead_hosts($output_type, @deadhosts);
635 ##############################################
636 # prints the log in a visually appealing manner
637 sub print_log_pretty() {
640 # do some pretty printing for the actions
643 Running\ action\s+([\w\-]+)
645 ACTION\ (PASSED|FAILED):\ ([\w\-]+)
651 # handle pretty-printing of static-analysis tools
652 if ($actionName eq 'cc_checker') {
653 $output = print_log_cc_checker($output);
656 make_collapsible_html('action', $actionName, $output, $id++,
660 # $log is already CGI-escaped, so handle '>' in test name by handling >
662 --==--==--==--==--==--==--==--==--==--==--.*?
663 Running\ test\ ([\w\-=,_:\ /.&;]+).*?
664 --==--==--==--==--==--==--==--==--==--==--
666 ==========================================.*?
667 TEST\ (FAILED|PASSED|SKIPPED):.*?
668 ==========================================\s+
669 }{make_collapsible_html('test', $1, $2, $id++, $3)}exgs;
671 print $req->p($req->tt($req->pre($log)))."\n";
674 ##############################################
675 # generate pretty-printed html for static analysis tools
676 sub print_log_cc_checker($) {
680 # for now, we only handle the IBM Checker's output style
681 if ($input !~ m/^BEAM_VERSION/ms) {
689 my ($entry, $title, $status, $id);
691 foreach (split /\n/, $input) {
693 # for each line, check if the line is a new entry,
694 # otherwise, store the line under the current entry.
699 $output .= make_collapsible_html('cc_checker', $title, $content,
705 # clear maintenance vars
706 ($inEntry, $content) = (1, "");
709 m/^-- ((ERROR|WARNING|MISTAKE).*?)\s+>>>([a-zA-Z0-9]+_(\w+)_[a-zA-Z0-9]+)/;
711 # then store the result
712 ($title, $status, $id) = ("$1 $4", $2, $3);
713 } elsif (m/^CC_CHECKER STATUS/) {
715 $output .= make_collapsible_html('cc_checker', $title, $content,
723 # not a new entry, so part of the current entry's output
728 # This function does approximately the same as the following, following
729 # commented-out regular expression except that the regex doesn't quite
730 # handle IBM Checker's newlines quite right.
732 # --\ ((ERROR|WARNING|MISTAKE).*?)\s+
736 # }{make_collapsible_html('cc_checker', "$1 $4", $5, $3, $2)}exgs;
740 ##############################################
741 # generate html for a collapsible section
742 sub make_collapsible_html
744 my $type = shift; # the logical type of it. e.g. "test" or "action"
745 my $title = shift; # the title to be displayed
748 my $status = (shift or "");
750 my $icon = (defined $status && ($status =~ /failed/i)) ? 'icon_hide_16.png' : 'icon_unhide_16.png';
752 # trim leading and trailing whitespace
753 $output =~ s/^\s+//s;
754 $output =~ s/\s+$//s;
756 # note that we may be inside a <pre>, so we don't put any extra whitespace in this html
757 return $req->div({-class=>"$type unit \L$status\E",
759 $req->a({-href=>"javascript:handle('$id');"},
760 $req->img({-id=>"img-$id", -name=>"img-$id",
763 $req->div({-class => "$type title"}, $title),
765 $req->div({-class=> "$type status \L$status\E"}, $status) .
766 $req->div({-class => "$type output", -id=>"output-$id"}, $req->pre($output)));
769 ##############################################
772 return $req->startform("GET"),
773 $req->start_div({-id=>"build-menu"}),
774 $req->popup_menu(-name=>'host',
777 $req->popup_menu("tree", [sort (keys %trees, @pseudo_trees)]),
778 $req->popup_menu("compiler", \@compilers),
780 $req->submit('function', 'View Build'),
781 $req->submit('function', 'View Host'),
782 $req->submit('function', 'Recent Checkins'),
783 $req->submit('function', 'Summary'),
784 $req->submit('function', 'Recent Builds'),
786 $req->endform() . "\n";
789 ###############################################
790 # display top of page
795 ###############################################
798 my $fn_name = get_param('function') || '';
800 if ($fn_name eq 'text_diff') {
801 print header('application/x-diff');
802 history::diff(get_param('author'),
805 get_param('revision'),
807 } elsif ($fn_name eq 'Text_Summary') {
808 print header('text/plain');
809 view_summary('text');
813 if ($fn_name eq "View_Build") {
814 view_build(get_param("tree"), get_param("host"), get_param("compiler"),
815 get_param('revision'));
816 } elsif ($fn_name eq "View_Host") {
817 view_host(get_param('host'));
818 } elsif ($fn_name eq "Recent_Builds") {
819 view_recent_builds(get_param("tree"), get_param("sortby") || "revision");
820 } elsif ($fn_name eq "Recent_Checkins") {
821 history::history(get_param('tree'));
822 } elsif ($fn_name eq "diff") {
823 history::diff(get_param('author'),
826 get_param('revision'),
828 } elsif (path_info() ne "" and path_info() ne "/") {
829 my @paths = split('/', path_info());
830 if ($paths[1] eq "recent") {
831 view_recent_builds($paths[2], get_param('sortby') || 'revision');
832 } elsif ($paths[1] eq "host") {
833 view_host($paths[2]);
836 view_summary('html');