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
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program; if not, write to the Free Software
21 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23 # TODO: Allow filtering of the "Recent builds" list to show
24 # e.g. only broken builds or only builds that you care about.
28 use FindBin qw($RealBin);
38 my $WEBDIR = "$RealBin";
39 my $BASEDIR = "$WEBDIR/..";
40 my $CACHEDIR = "$WEBDIR/../cache";
44 my $HEADCOLOR = "#a0a0e0";
46 my $DEADAGE = 60*60*24*4;
48 ##############################################
49 # this defines what it is possible to build
50 # and what boxes. Should be in a config file
51 my @compilers = util::load_list("$WEBDIR/compilers.list");
52 my (%hosts) = util::load_hash("$WEBDIR/hosts.list");
53 my @hosts = sort { $hosts{$a} cmp $hosts{$b} } keys %hosts;
54 my (%trees) = util::load_hash("$WEBDIR/trees.list");
55 # these aren't really trees... they're just things we want in the menu.
56 # (for recent checkins)
57 my @pseudo_trees = util::load_list("$WEBDIR/pseudo.list");
59 # this is automatically filled in
62 ###############################################
63 # work out a URL so I can refer to myself in links
64 my $myself = $req->self_url;
65 if ($myself =~ /(.*)[?].*/) {
68 if ($myself =~ /http:\/\/.*\/(.*)/) {
72 #$myself = "http://build.samba.org/";
74 ################################################
77 print "Content-type: text/html\r\n";
78 #print "Content-type: application/xhtml+xml\r\n";
82 print util::FileLoad("$BASEDIR/web/header.html");
83 print '<title>samba.org build farm</title>';
84 print util::FileLoad("$BASEDIR/web/header2.html");
86 print util::FileLoad("$BASEDIR/web/header3.html");
89 ################################################
90 # start CGI headers for diffs
91 sub cgi_headers_diff() {
92 print "Content-type: application/x-diff\r\n";
96 ################################################
97 # start CGI headers for text output
98 sub cgi_headers_text() {
99 print "Content-type: text/plain\r\n";
103 ################################################
106 print util::FileLoad("$BASEDIR/web/footer.html");
109 ################################################
110 # print an error on fatal errors
113 print "<h1>ERROR: $msg</h1>\n";
118 ################################
119 # get the name of the build file
120 sub build_fname($$$$)
127 return "oldrevs/build.$tree.$host.$compiler-$rev";
129 return "build.$tree.$host.$compiler";
132 ###########################################
133 # get a list of old builds and their status
134 sub get_old_revs($$$)
139 my @list = split('\n', `ls oldrevs/build.$tree.$host.$compiler-*.log`);
142 if ($l =~ /-(\d+).log$/) {
144 $ret{$rev} = build_status($host, $tree, $compiler, $rev);
151 ##############################################
152 # get the age of build from mtime
159 my $file=build_fname($tree, $host, $compiler, $rev);
163 $st = stat("$file.log");
165 $age = time() - $st->mtime;
171 ##############################################
172 # get the svn revision of build
173 sub build_revision($$$$)
179 my $file=build_fname($tree, $host, $compiler, $rev);
187 my $st1 = stat("$file.log");
188 my $st2 = stat("$CACHEDIR/$file.revision");
190 if ($st1 && $st2 && $st1->mtime <= $st2->mtime) {
191 return util::FileLoad("$CACHEDIR/$file.revision");
194 $log = util::FileLoad("$file.log");
196 if ($log =~ /BUILD REVISION:(.*)/) {
200 util::FileSave("$CACHEDIR/$file.revision", "$ret");
205 #############################################
206 # get the overall age of a host
211 for my $compiler (@compilers) {
212 for my $tree (keys %trees) {
213 my $age = build_age($host, $tree, $compiler, "");
214 if ($age != -1 && ($age < $ret || $ret == -1)) {
222 #############################################
223 # show an age as a string
228 if ($age > $OLDAGE) {
229 return sprintf("<span class=\"old\">%s</span>", util::dhm_time($age));
231 return util::dhm_time($age);
235 ##############################################
236 # get status of build
237 sub build_status($$$$)
243 my $file=build_fname($tree, $host, $compiler, $rev);
244 my $cachefile="$CACHEDIR/" . $file . ".status";
245 my ($cstatus, $bstatus, $istatus, $tstatus, $sstatus);
246 $cstatus = $bstatus = $istatus = $tstatus = $sstatus =
247 "<span class=\"status unknown\">?</span>";
252 my $st1 = stat("$file.log");
253 my $st2 = stat("$cachefile");
255 if ($st1 && $st2 && $st1->mtime <= $st2->mtime) {
256 return util::FileLoad($cachefile);
259 $log = util::FileLoad("$file.log");
261 if ($log =~ /TEST STATUS:(.*)/) {
263 $tstatus = "<span class=\"status passed\">ok</span>";
265 $tstatus = "<span class=\"status failed\">$1</span>";
269 if ($log =~ /INSTALL STATUS:(.*)/) {
271 $istatus = "<span class=\"status passed\">ok</span>";
273 $istatus = "<span class=\"status failed\">$1</span>";
277 if ($log =~ /BUILD STATUS:(.*)/) {
279 $bstatus = "<span class=\"status passed\">ok</span>";
281 $bstatus = "<span class=\"status failed\">$1</span>";
285 if ($log =~ /CONFIGURE STATUS:(.*)/) {
287 $cstatus = "<span class=\"status passed\">ok</span>";
289 $cstatus = "<span class=\"status failed\">$1</span>";
293 if ($log =~ /INTERNAL ERROR:(.*)/ || $log =~ /PANIC:(.*)/) {
294 $sstatus = "/<span class=\"status panic\">PANIC</span>";
299 $ret = "<a href=\"$myself?function=View+Build;host=$host;tree=$tree;compiler=$compiler";
301 $ret .= ";revision=$rev";
303 $ret .= "\">$cstatus/$bstatus/$istatus/$tstatus$sstatus</a>";
305 util::FileSave("$CACHEDIR/$file.status", $ret);
311 ##############################################
312 # get status of build
319 my $file=build_fname($tree, $host, $compiler, $rev);
322 my $st1 = stat("$file.err");
323 my $st2 = stat("$CACHEDIR/$file.errcount");
325 if ($st1 && $st2 && $st1->mtime <= $st2->mtime) {
326 return util::FileLoad("$CACHEDIR/$file.errcount");
329 $err = util::FileLoad("$file.err");
331 if (! $err) { return 0; }
333 my $ret = util::count_lines($err);
335 util::FileSave("$CACHEDIR/$file.errcount", "$ret");
342 ##############################################
344 sub view_summary($) {
346 my $list = `ls *.log`;
350 # either "text" or anything else.
351 my $output_type = shift;
358 # zero broken and panic counters
359 for my $tree (keys %trees) {
360 $broken_count{$tree} = 0;
361 $panic_count{$tree} = 0;
362 $host_count{$tree} = 0;
365 #set up a variable to store the broken builds table's code, so we can output when we want
366 my $broken_table = "";
370 # for the text report, include the current time
371 if ($output_type eq 'text') {
373 print "Build status as of $time\n\n";
376 for my $host (@hosts) {
377 for my $compiler (@compilers) {
378 for my $tree (keys %trees) {
379 my $status = build_status($host, $tree, $compiler, "");
380 my $age = build_age($host, $tree, $compiler, "");
382 if ($age != -1 && $age < $DEADAGE) {
383 $host_count{$tree}++;
386 if ($age < $DEADAGE && $status =~ /status failed/) {
387 $broken_count{$tree}++;
388 if ($status =~ /PANIC/) {
389 $panic_count{$tree}++;
396 if ($output_type eq 'text') {
397 print "Build counts:\n";
398 printf "%-12s %-6s %-6s %-6s\n", "Tree", "Total", "Broken", "Panic";
402 <div id="build-counts" class="build-section">
403 <h2>Build counts:</h2>
407 <th>Tree</th><th>Total</th><th>Broken</th><th>Panic</th>
415 for my $tree (sort keys %trees) {
416 if ($output_type eq 'text') {
417 printf "%-12s %-6s %-6s %-6s\n", $tree, $host_count{$tree},
418 $broken_count{$tree}, $panic_count{$tree};
421 print " <tr><td>$tree</td><td>$host_count{$tree}</td><td>$broken_count{$tree}</td>";
423 if ($panic_count{$tree}) {
424 $panic = " class=\"panic\"";
426 print "<td$panic>$panic_count{$tree}</td></tr>\n";
430 if ($output_type eq 'text') {
434 print " </tbody>\n</table></div>\n";
438 ##############################################
439 # Draw the "recent builds" view
441 sub view_recent_builds() {
451 my $tree=$req->param("tree");
453 # Convert from the DataDumper tree form to an array that
454 # can be sorted by time.
456 util::InArray($tree, [keys %trees]) || fatal("not a build tree");
458 for my $host (@hosts) {
459 for my $compiler (@compilers) {
460 my $status = build_status($host, $tree, $compiler, "");
461 my $age = build_age($host, $tree, $compiler, "");
462 my $revision = build_revision($host, $tree, $compiler, "");
463 push @all_builds, [$age, $hosts{$host}, "<a href=\"$myself?function=Summary;host=$host;tree=$tree;compiler=$compiler#$host\">$host</a>", $compiler, $tree, $status, $revision]
464 unless $age == -1 or $age >= $DEADAGE;
468 # sort by revision then age
469 @all_builds = sort {$$b[6] <=> $$a[6] || $$a[0] <=> $$b[0]} @all_builds;
474 <div id="recent-builds" class="build-section">
475 <h2>Recent builds of $tree</h2>
479 <th>Age</th><th>Revision</th><th colspan="4">Target</th><th>Status</th>
486 for my $build (@all_builds) {
487 my $age = $$build[0];
488 my $rev = $$build[6];
490 join("</td><td>" , util::dhm_time($age),
491 $rev, @$build[4, 1, 2, 3, 5]),
494 print " </tbody>\n</table>\n</div>\n";
498 ##############################################
499 # Draw the "dead hosts" table
500 sub draw_dead_hosts() {
501 my $output_type = shift;
504 # don't output anything if there are no dead hosts
505 if ($#deadhosts < 1) {
509 # don't include in text report
510 if ($output_type eq 'text') {
514 <div class="build-section" id="dead-hosts">
518 <tr><th>Host</th><th>OS</th><th>Min Age</th></tr>
523 for my $host (@deadhosts) {
524 my $age = host_age($host);
525 print " <tr><td>$host</td><td>$hosts{$host}</td><td>", util::dhm_time($age), "</td></tr>";
529 print " </tbody>\n</table>\n</div>\n";
532 ##############################################
533 # show the available old revisions, if any
534 sub show_oldrevs($$$)
539 my %revs = get_old_revs($tree, $host, $compiler);
540 my @revs = sort { $revs{$b} cmp $revs{$a} } keys %revs;
542 return if ($#revs < 1);
544 print "<h2>Older builds:</h2>\n";
547 <table class=\"real\">
548 <tr><th>Revision</th><th>Status</th></tr>
553 for my $rev (@revs) {
556 next if ($s eq $lastrev);
558 print "<tr><td>$rev</td><td>$revs{$rev}</td></tr>\n";
563 ##############################################
564 # view one build in detail
566 my $tree=$req->param("tree");
567 my $host=$req->param("host");
568 my $compiler=$req->param("compiler");
569 my $rev=$req->param('revision');
570 my $file=build_fname($tree, $host, $compiler, $rev);
576 my $age = build_age($host, $tree, $compiler, $rev);
577 my $revision = build_revision($host, $tree, $compiler, $rev);
578 my $status = build_status($host, $tree, $compiler, $rev);
580 util::InArray($host, [keys %hosts]) || fatal("unknown host");
581 util::InArray($compiler, \@compilers) || fatal("unknown compiler");
582 util::InArray($tree, [keys %trees]) || fatal("not a build tree");
585 $log = util::FileLoad("$file.log");
586 $err = util::FileLoad("$file.err");
589 $log = util::cgi_escape($log);
591 if ($log =~ /(.*)/) { $uname=$1; }
592 if ($log =~ /CFLAGS=(.*)/) { $cflags=$1; }
593 if ($log =~ /configure options: (.*)/) { $config=$1; }
597 $err = util::cgi_escape($err);
600 print "<h2>Host information:</h2>\n";
602 print util::FileLoad("../web/$host.html");
605 <table class=\"real\">
606 <tr><td>Host:</td><td><a href=\"$myself?function=Summary;host=$host;tree=$tree;compiler=$compiler#$host\">$host</a> - $hosts{$host}</td></tr>
607 <tr><td>Uname:</td><td>$uname</td></tr>
608 <tr><td>Tree:</td><td>$tree</td></tr>
609 <tr><td>Build Revision:</td><td>" . $revision . "</td></tr>
610 <tr><td>Build age:</td><td class=\"age\">" . red_age($age) . "</td></tr>
611 <tr><td>Status:</td><td>$status</td></tr>
612 <tr><td>Compiler:</td><td>$compiler</td></tr>
613 <tr><td>CFLAGS:</td><td>$cflags</td></tr>
614 <tr><td>configure options: </td><td>$config</td></tr>
618 show_oldrevs($tree, $host, $compiler);
620 # check the head of the output for our magic string
621 my $plain_logs = (defined $req->param("plain") &&
622 $req->param("plain") =~ /^(yes|1|on|true|y)$/i);
624 print "<div id=\"log\">\n";
628 print "<p>Switch to the <a href=\"$myself?function=View+Build;host=$host;tree=$tree;compiler=$compiler;plain=true\" title=\"Switch to bland, non-javascript, unstyled view\">Plain View</a></p>";
630 print "<div id=\"actionList\">\n";
631 # These can be pretty wide -- perhaps we need to
632 # allow them to wrap in some way?
634 print "<h2>No error log available</h2>\n";
636 print "<h2>Error log:</h2>\n";
637 print make_action_html("Error Output", "\n$err", "stderr-0", 0);
641 print "<h2>No build log available</h2>\n";
643 print "<h2>Build log:</h2>\n";
644 print_log_pretty($log);
647 print "<p><small>Some of the above icons derived from the <a href=\"http://www.gnome.org\">Gnome Project</a>'s stock icons.</p>";
651 print "<p>Switch to the <a href=\"$myself?function=View+Build;host=$host;tree=$tree;compiler=$compiler\" title=\"Switch to colourful, javascript-enabled, styled view \">Enhanced View</a></p>";
653 print "<h2>No error log available</h2>\n";
655 print "<h2>Error log:</h2>\n";
656 print "<div id=\"errorLog\"><pre>" . join('', $err) . "</pre></div>\n";
659 print "<h2>No build log available</h2>n";
662 print "<h2>Build log:</h2>\n";
663 print "<div id=\"buildLog\"><pre>" . join('', $log) . "</pre></div>\n";
670 ##############################################
671 # prints the log in a visually appealing manner
672 sub print_log_pretty() {
676 # do some pretty printing for the actions
678 $log =~ s{ Running\ action\s+([\w\-]+)
680 ACTION\ (PASSED|FAILED):\ ([\w\-]+)
681 }{make_action_html($1, $2, $id++, $3)}exgs;
684 --==--==--==--==--==--==--==--==--==--==--.*?
685 Running\ test\ ([\w\-=,_:\ ]+)\ \(level\ (\d+)\ (\w+)\).*?
686 --==--==--==--==--==--==--==--==--==--==--
688 ==========================================.*?
689 TEST\ (FAILED|PASSED|SKIPPED):(\ \(status\ (\d+)\))?.*?
690 ==========================================\s+
691 }{make_test_html($1, $4, $id++, $5)}exgs;
694 print "<tt><pre>" .join('', $log) . "</pre></tt><p>\n";
697 ##############################################
698 # generate html for a test section
705 my $return = "</pre>" . # don't want the pre openned by action affecting us
706 "<div class=\"test unit \L$status\E\" id=\"test-$id\">" .
707 "<a href=\"javascript:handle('$id');\">" .
708 "<img id=\"img-$id\" name=\"img-$id\" src=\"";
709 if (defined $status && $status eq "PASSED") {
710 $return .= "icon_unhide_16.png";
713 $return .= "icon_hide_16.png";
715 $return .= "\" /> " .
716 "<div class=\"test name\">$name</div> " .
718 "<div class=\"test status \L$status\E\">$status</div>" .
719 "<div class=\"test output\" id=\"output-$id\">" .
720 "<pre>$output</pre>" .
723 "<pre>"; # open the pre back up
728 ##############################################
729 # generate html for an action section
730 sub make_action_html($$$$)
737 my $return = "<div class=\"action unit \L$status\E\" id=\"action-$id\">" .
738 "<a href=\"javascript:handle('$id');\">" .
739 "<img id=\"img-$id\" name=\"img-$id\" src=\"";
741 if (defined $status && ($status =~ /failed/i)) {
742 $return .= 'icon_hide_24.png';
745 $return .= 'icon_unhide_24.png';
748 $return .= "\" /> " .
749 "<div class=\"action name\">$name</div>" .
752 if (defined $status) {
753 $return .= "<div class=\"action status \L$status\E\">$status</div>";
762 $return .= "<div class=\"action output\" id=\"output-$id\">" .
763 "<pre>Running action $name$output ACTION $status: $name</pre>" .
770 ##############################################
771 # simple html markup stripper
775 # get rid of comments
776 $string =~ s/<!\-\-(.*?)\-\->/$2/g;
779 while ($string =~ s&<(\w+).*?>(.*?)</\1>&$2&) {
787 ##############################################
790 print $req->startform("GET");
791 print "<div id=\"build-menu\">\n";
792 print $req->popup_menu(-name=>'host',
794 -labels=>\%hosts) . "\n";
795 print $req->popup_menu("tree", [sort (keys %trees, @pseudo_trees)]) . "\n";
796 print $req->popup_menu("compiler", \@compilers) . "\n";
798 print $req->submit('function', 'View Build') . "\n";
799 print $req->submit('function', 'Recent Checkins') . "\n";
800 print $req->submit('function', 'Summary') . "\n";
801 print $req->submit('function', 'Recent Builds') . "\n";
803 print $req->endform() . "\n";
807 ###############################################
808 # display top of page
811 chdir("$BASEDIR/data") || fatal("can't change to data directory");
815 ###############################################
817 my $fn_name = (defined $req->param('function')) ? $req->param('function') : '';
819 if ($fn_name eq 'text_diff') {
821 chdir("$BASEDIR/data") || fatal("can't change to data directory");
822 history::diff($req->param('author'),
825 $req->param('revision'),
828 elsif ($fn_name eq 'Text_Summary') {
830 chdir("$BASEDIR/data") || fatal("can't change to data directory");
831 view_summary('text');
836 if ($fn_name eq "View Build") {
839 elsif ($fn_name eq "Recent Builds") {
840 view_recent_builds();
842 elsif ($fn_name eq "Recent Checkins") {
843 history::history($req->param('tree'));
845 elsif ($fn_name eq "diff") {
846 history::diff($req->param('author'),
849 $req->param('revision'),
853 view_summary('html');