X-Git-Url: http://git.samba.org/?a=blobdiff_plain;f=buildfarm%2Fweb%2F__init__.py;h=ead25730a7599cbe5d5be9a1dc19db37707999d5;hb=9dee960b26fa090b2d0fb8449430e95246f8998c;hp=e0af29130e5fcfb62c5c71a137412ec3b1df667a;hpb=1f985416a874ef995e451af972947ff0cb24b989;p=build-farm.git diff --git a/buildfarm/web/__init__.py b/buildfarm/web/__init__.py index e0af2913..ead25730 100755 --- a/buildfarm/web/__init__.py +++ b/buildfarm/web/__init__.py @@ -2,7 +2,7 @@ # This CGI script presents the results of the build_farm build # Copyright (C) Jelmer Vernooij 2010 -# Copyright (C) Matthieu Patou 2010 +# Copyright (C) Matthieu Patou 2010-2012 # # Based on the original web/build.pl: # @@ -26,34 +26,51 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +"""Buildfarm web frontend.""" + # TODO: Allow filtering of the "Recent builds" list to show # e.g. only broken builds or only builds that you care about. +from collections import defaultdict import os from buildfarm import ( - data, hostdb, util, ) -from buildfarm.filecache import ( - CachingBuildFarm, +from buildfarm.build import ( + LogFileMissing, + NoSuchBuildError, + NoTestOutput, + BuildStatus, ) import cgi +from pygments import highlight +from pygments.lexers.text import DiffLexer +from pygments.formatters import HtmlFormatter import re import time import wsgiref.util webdir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "web")) - -GITWEB_BASE = "http://gitweb.samba.org" +GITWEB_BASE = "https://gitweb.samba.org" HISTORY_HORIZON = 1000 # this is automatically filled in deadhosts = [] +def select(name, values, default=None): + yield "" + + def get_param(form, param): """get a param from the request, after sanitizing it""" if param not in form: @@ -68,15 +85,6 @@ def get_param(form, param): return result[0] -def build_link(myself, tree, host, compiler, rev, status): - if rev: - opt_rev = ';revision=%s' % rev - else: - opt_rev = '' - return "%s" % ( - myself, host, tree, compiler, opt_rev, status) - - def html_build_status(status): def span(classname, contents): return "%s" % (classname, contents) @@ -95,50 +103,58 @@ def html_build_status(status): else: return span("status failed", stage.result) - ostatus = "" + ostatus = [] if "panic" in status.other_failures: - ostatus += "/"+span("status panic", "PANIC") + ostatus.append(span("status panic", "PANIC")) if "disk full" in status.other_failures: - ostatus += "/"+span("status failed", "disk full") + ostatus.append(span("status failed", "disk full")) if "timeout" in status.other_failures: - ostatus += "/"+span("status failed", "timeout") + ostatus.append(span("status failed", "timeout")) if "inconsistent test result" in status.other_failures: - ostatus += "/"+span("status failed", "unexpected return code") + ostatus.append(span("status failed", "unexpected return code")) bstatus = "/".join([span_status(s) for s in status.stages]) - if bstatus == "": - bstatus = "?" - return bstatus + ostatus + ret = bstatus + if ostatus: + ret += "(%s)" % ",".join(ostatus) + if ret == "": + ret = "?" + return ret + + +def build_uri(myself, build): + return "%s/build/%s" % (myself, build.log_checksum()) + +def build_link(myself, build): + return "%s" % (build_uri(myself, build), html_build_status(build.status())) -def build_status_html(myself, build): - status = html_build_status(build.status()) - return build_link(myself, build.tree, build.host, build.compiler, build.revision, status) +def tree_uri(myself, tree): + return "%s/tree/%s" % (myself, tree.name) -def build_status_vals(status): - """translate a status into a set of int representing status""" - status = util.strip_html(status) - status = status.replace("ok", "0") - status = status.replace("-", "0") - status = status.replace("?", "0.1") - status = status.replace("PANIC", "1") +def tree_link(myself, tree): + """return a link to a particular tree""" + return "%s:%s" % (tree_uri(myself, tree), tree.name, tree.name, tree.branch) - return status.split("/") + +def host_uri(myself, host): + return "%s/host/%s" % (myself, host) + +def host_link(myself, host): + return "%s" % (host_uri(myself, host), host) + + +def revision_uri(myself, revision, tree): + return "%s?function=diff;tree=%s;revision=%s" % (myself, tree, revision) def revision_link(myself, revision, tree): """return a link to a particular revision""" - if revision is None: return "unknown" - - revision = revision.lstrip() - rev_short = revision - if len(revision) == 40: - rev_short = re.sub("(^.{7}).*", "\\1(git)", rev_short) - - return "%s" % (myself, tree, revision, revision, rev_short) + return "%s" % ( + revision_uri(myself, revision, tree), revision, revision[:7]) def subunit_to_buildfarm_result(subunit_result): @@ -152,9 +168,12 @@ def subunit_to_buildfarm_result(subunit_result): return "failed" elif subunit_result == "xfail": return "xfailed" + elif subunit_result == "uxsuccess": + return "uxpassed" else: return "unknown" + def format_subunit_reason(reason): reason = re.sub("^\[\n+(.*?)\n+\]$", "\\1", reason) return "
%s
" % reason @@ -174,16 +193,20 @@ class LogPrettyPrinter(object): output = print_log_cc_checker(output) self.indice += 1 - return make_collapsible_html('action', actionName, output, self.indice, status) + return "".join(make_collapsible_html('action', actionName, output, self.indice, status)) # log is already CGI-escaped, so handle '>' in test name by handling > def _format_stage(self, m): self.indice += 1 - return make_collapsible_html('test', m.group(1), m.group(2), self.indice, m.group(3)) + return "".join(make_collapsible_html('test', m.group(1), m.group(2), self.indice, m.group(3))) def _format_skip_testsuite(self, m): self.indice += 1 - return make_collapsible_html('test', m.group(1), '', self.indice, 'skipped') + return "".join(make_collapsible_html('test', m.group(1), '', self.indice, 'skipped')) + + def _format_pretestsuite(self, m): + self.indice += 1 + return m.group(1)+"".join(make_collapsible_html('pretest', 'Pretest infos', m.group(2), self.indice, 'ok'))+"\n"+m.group(3) def _format_testsuite(self, m): testName = m.group(1) @@ -194,16 +217,21 @@ class LogPrettyPrinter(object): else: errorReason = "" self.indice += 1 - return make_collapsible_html('test', testName, content+errorReason, self.indice, status) + backlink = "" + if m.group(3) in ("error", "failure"): + self.test_links.append([testName, 'lnk-test-%d' %self.indice]) + backlink = "

back to error list" + return "".join(make_collapsible_html('test', testName, content+errorReason+backlink, self.indice, status)) def _format_test(self, m): self.indice += 1 - return make_collapsible_html('test', m.group(1), m.group(2)+format_subunit_reason(m.group(4)), self.indice, subunit_to_buildfarm_result(m.group(3))) + return "".join(make_collapsible_html('test', m.group(1), m.group(2)+format_subunit_reason(m.group(4)), self.indice, subunit_to_buildfarm_result(m.group(3)))) def pretty_print(self, log): # do some pretty printing for the actions pattern = re.compile("(Running action\s+([\w\-]+)$(?:\s^.*$)*?\sACTION\ (PASSED|FAILED):\ ([\w\-]+)$)", re.M) log = pattern.sub(self._pretty_print, log) + buf = "" log = re.sub(""" --==--==--==--==--==--==--==--==--==--==--.*? @@ -215,17 +243,27 @@ class LogPrettyPrinter(object): ==========================================\s+ """, self._format_stage, log) + pattern = re.compile("(Running action test).*$\s((?:^.*$\s)*?)^((?:skip-)?testsuite: )", re.M) + log = pattern.sub(self._format_pretestsuite, log) + log = re.sub("skip-testsuite: ([\w\-=,_:\ /.&; \(\)]+).*?", self._format_skip_testsuite, log) + self.test_links = [] pattern = re.compile("^testsuite: (.+)$\s((?:^.*$\s)*?)testsuite-(\w+): .*?(?:(\[$\s(?:^.*$\s)*?^\]$)|$)", re.M) log = pattern.sub(self._format_testsuite, log) log = re.sub(""" ^test: ([\w\-=,_:\ /.&; \(\)]+).*? (.*?) - (success|xfail|failure|skip): [\w\-=,_:\ /.&; \(\)]+( \[.*?\])?.*? + (success|xfail|failure|skip|uxsuccess): [\w\-=,_:\ /.&; \(\)]+( \[.*?\])?.*? """, self._format_test, log) + for tst in self.test_links: + buf = "%s\n%s" % (buf, tst[1], tst[0]) + + if not buf == "": + divhtml = "".join(make_collapsible_html('testlinks', 'Shortcut to failed tests', "%s" % buf, self.indice, ""))+"\n" + log = re.sub("Running action\s+test", divhtml, log) return "

%s
" % log @@ -254,7 +292,7 @@ def print_log_cc_checker(input): if line.startswith("-- "): # got a new entry if inEntry: - output += make_collapsible_html('cc_checker', title, content, id, status) + output += "".join(make_collapsible_html('cc_checker', title, content, id, status)) else: output += content @@ -268,7 +306,7 @@ def print_log_cc_checker(input): (title, status, id) = ("%s %s" % (m.group(1), m.group(4)), m.group(2), m.group(3)) elif line.startswith("CC_CHECKER STATUS"): if inEntry: - output += make_collapsible_html('cc_checker', title, content, id, status) + output += "".join(make_collapsible_html('cc_checker', title, content, id, status)) inEntry = False content = "" @@ -296,53 +334,25 @@ def make_collapsible_html(type, title, output, id, status=""): :param type: the logical type of it. e.g. "test" or "action" :param title: the title to be displayed """ - if ((status == "" or "failed" == status.lower())): - icon = 'icon_hide_16.png' + if status.lower() in ("", "failed"): + icon = '/icon_hide_16.png' else: - icon = 'icon_unhide_16.png' + icon = '/icon_unhide_16.png' # trim leading and trailing whitespace output = output.strip() # note that we may be inside a
, so we don't put any extra whitespace
     # in this html
-    ret = "
" % (type, status, type, id) - ret += "" % id - ret += "%s" %(id, id, status, icon) - ret += "
%s
" % (type, title) - #ret += " " - ret += "
%s
" % (type, status, status) - ret += "
" % (type, id) - if output and len(output): - ret += "
%s
>" % (output) - ret += "
" - return ret - - -def diff_pretty(diff): - """pretty up a diff -u""" - # FIXME: JRV 20101109 Use pygments for this - ret = "" - lines = diff.splitlines() - - line_types = { - 'diff': 'diff_diff', - '=': 'diff_separator', - 'Index:': 'diff_index', - 'index': 'diff_index', - '-': 'diff_removed', - '+': 'diff_added', - '@@': 'diff_fragment_header' - } - - for line in lines: - for r, cls in line_types.iteritems(): - if line.startswith(r): - line = "%s" % (cls, line) - continue - ret += line + "\n" - - return ret + yield "
" % (type, status, type, id) + yield "" % (type, id, id) + yield "%s" % (id, id, status, icon) + yield "
%s
" % (type, title) + yield "
%s
" % (type, status, status) + yield "
" % (type, id) + if output: + yield "
%s
" % (output,) + yield "
" def web_paths(t, paths): @@ -356,60 +366,6 @@ def web_paths(t, paths): raise Exception("Unknown scm %s" % t.scm) -def history_row_html(myself, entry, tree, changes): - """show one row of history table""" - msg = cgi.escape(entry.message) - t = time.asctime(time.gmtime(entry.date)) - age = util.dhm_time(time.time()-entry.date) - - t = t.replace(" ", " ") - - yield """ -
-
- %s
- %s ago""" % (t, age) - if entry.revision: - yield " - %s
" % entry.revision - revision_url = "revision=%s" % entry.revision - else: - revision_url = "author=%s" % entry.author - yield """
-
- show diffs -
- download diffs -
-
-
%s
-
-
-
- Author: %s -
""" % (myself, tree.name, entry.date, revision_url, - myself, tree.name, entry.date, revision_url, - msg, entry.author) - - (added, modified, removed) = changes - - if modified: - yield "
Modified: " - yield web_paths(tree, modified) - yield "
\n" - - if added: - yield "
Added: " - yield web_paths(tree, added) - yield "
\n" - - if removed: - yield "
Removed: " - yield web_paths(tree, removed) - yield "
\n" - - yield "
\n" - - def history_row_text(entry, tree, changes): """show one row of history table""" msg = cgi.escape(entry.message) @@ -426,15 +382,6 @@ def history_row_text(entry, tree, changes): yield "\n\n%s\n\n\n" % msg -def show_diff(diff, text_html): - if text_html == "html": - diff = cgi.escape(diff) - diff = diff_pretty(diff) - return "
%s
\n" % diff - else: - return "%s\n" % diff - - class BuildFarmPage(object): def __init__(self, buildfarm): @@ -443,16 +390,14 @@ class BuildFarmPage(object): def red_age(self, age): """show an age as a string""" if age > self.buildfarm.OLDAGE: - return "%s" % util.dhm_time(age) + return "%s" % util.dhm_time(age) return util.dhm_time(age) - def tree_link(self, myself, tree): - # return a link to a particular tree - branch = "" - if tree in self.buildfarm.trees: - branch = ":%s" % self.buildfarm.trees[tree].branch - - return "%s%s" % (myself, tree, tree, tree, branch) + def tree_link(self, myself, treename): + try: + return tree_link(myself, self.buildfarm.trees[treename]) + except KeyError: + return treename def render(self, output_type): raise NotImplementedError(self.render) @@ -460,53 +405,50 @@ class BuildFarmPage(object): class ViewBuildPage(BuildFarmPage): - def show_oldrevs(self, myself, tree, host, compiler): + def show_oldrevs(self, myself, build, host, compiler, limit): """show the available old revisions, if any""" - old_rev_builds = self.buildfarm.builds.get_old_revs(tree, host, compiler) - if len(old_rev_builds) == 0: + tree = build.tree + old_builds = self.buildfarm.builds.get_old_builds(tree, host, compiler) + + if not old_builds: return yield "

Older builds:

\n" yield "\n" - yield "\n" + yield "\n" yield "\n" - for build in old_rev_builds: - yield "\n" % ( - revision_link(myself, build.revision, tree), - build_status_html(myself, build)) + nb = 0 + for old_build in old_builds: + if limit >= 0 and nb >= limit: + break + nb = nb + 1 + yield "\n" % ( + revision_link(myself, old_build.revision, tree), + build_link(myself, old_build), + util.dhm_time(old_build.age)) yield "
RevisionStatus
RevisionStatusAge
%s%s
%s%s%s
\n" - def render(self, myself, tree, host, compiler, rev, plain_logs=False): - """view one build in detail""" - # ensure the params are valid before using them - self.buildfarm.hostdb.host(host) - assert compiler in self.buildfarm.compilers, "unknown compiler %s" % compiler - assert tree in self.buildfarm.trees, "not a build tree %s" % tree - - uname = "" - cflags = "" - config = "" - build = buildfarm.get_build(tree, host, compiler, rev) - age_mtime = build.age_mtime() - try: - (revision, revision_time) = build.revision_details() - except data.MissingRevisionInfo: - revision = None + yield "

Show all previous build list\n" % (build_uri(myself, build)) - status = build_status_html(myself, build) + def render(self, myself, build, plain_logs=False, limit=10): + """view one build in detail""" - if rev: - assert re.match("^[0-9a-fA-F]*$", rev) + uname = None + cflags = None + config = None - f = build.read_log() try: - log = f.read() - finally: - f.close() + f = build.read_log() + try: + log = f.read() + finally: + f.close() + except LogFileMissing: + log = None f = build.read_err() try: err = f.read() @@ -526,41 +468,56 @@ class ViewBuildPage(BuildFarmPage): if m: config = m.group(1) - if err: - err = cgi.escape(err) + err = cgi.escape(err) yield '

Host information:

' - host_web_file = "../web/%s.html" % host + host_web_file = "../web/%s.html" % build.host if os.path.exists(host_web_file): yield util.FileLoad(host_web_file) yield "\n" yield "\n" %\ - (myself, host, tree, compiler, host, self.buildfarm.hostdb.host(host).platform.encode("utf-8")) - yield "\n" % uname - yield "\n" % self.tree_link(myself, tree) - yield "\n" % revision_link(myself, revision, tree) - yield "\n" % self.red_age(age_mtime) - yield "\n" % status - yield "\n" % compiler - yield "\n" % cflags - yield "\n" % config + (myself, build.host, build.tree, build.compiler, build.host, self.buildfarm.hostdb[build.host].platform.encode("utf-8")) + if uname is not None: + yield "\n" % uname + yield "\n" % self.tree_link(myself, build.tree) + yield "\n" % revision_link(myself, build.revision, build.tree) + yield "\n" % self.red_age(build.age) + yield "\n" % build_link(myself, build) + yield "\n" % build.compiler + if cflags is not None: + yield "\n" % cflags + if config is not None: + yield "\n" % config yield "
Host:%s - %s
Uname:%s
Tree:%s
Build Revision:%s
Build age:
%s
Status:%s
Compiler:%s
CFLAGS:%s
configure options:%s
Uname:%s
Tree:%s
Build Revision:%s
Build age:
%s
Status:%s
Compiler:%s
CFLAGS:%s
configure options:%s
\n" - yield "".join(self.show_oldrevs(myself, tree, host, compiler)) + yield "".join(self.show_oldrevs(myself, build, build.host, build.compiler, limit)) # check the head of the output for our magic string rev_var = "" - if rev: - rev_var = ";revision=%s" % rev + if build.revision: + rev_var = ";revision=%s" % build.revision yield "
" + yield "

Subunit output" % build_uri(myself, build) + try: + previous_build = self.buildfarm.builds.get_previous_build(build.tree, build.host, build.compiler, build.revision) + except NoSuchBuildError: + pass + else: + yield ", diff against previous" % ( + build_uri(myself, build), previous_build.log_checksum()) + yield "

" + yield "

Standard output (as plain text), " % build_uri(myself, build) + yield "Standard error (as plain text)" % build_uri(myself, build) + yield "

" + if not plain_logs: yield "

Switch to the Plain View

" % (myself, host, tree, compiler, rev_var) + " unstyled view'>Plain View

" % (myself, build.host, build.tree, build.compiler, rev_var) yield "
" # These can be pretty wide -- perhaps we need to @@ -569,20 +526,20 @@ class ViewBuildPage(BuildFarmPage): yield "

No error log available

\n" else: yield "

Error log:

" - yield make_collapsible_html('action', "Error Output", "\n%s" % err, "stderr-0", "errorlog") + yield "".join(make_collapsible_html('action', "Error Output", "\n%s" % err, "stderr-0", "errorlog")) - if log == "": + if log is None: yield "

No build log available

" else: yield "

Build log:

\n" yield print_log_pretty(log) - yield "

Some of the above icons derived from the Gnome Project's stock icons.

" + yield "

Some of the above icons derived from the Gnome Project's stock icons.

" yield "
" else: yield "

Switch to the Enhanced View

" % (myself, host, tree, compiler, rev_var) + " view'>Enhanced View

" % (myself, build.host, build.tree, build.compiler, rev_var) if err == "": yield "

No error log available

" else: @@ -599,56 +556,44 @@ class ViewBuildPage(BuildFarmPage): class ViewRecentBuildsPage(BuildFarmPage): - def render(self, myself, tree, sort_by): + def render(self, myself, tree, sort_by=None): """Draw the "recent builds" view""" - i = 0 - cols = 2 - broken = 0 - last_host = "" all_builds = [] + def build_platform(build): + host = self.buildfarm.hostdb[build.host] + return host.platform.encode("utf-8") + + def build_platform_safe(build): + try: + host = self.buildfarm.hostdb[build.host] + except hostdb.NoSuchHost: + return "UNKNOWN" + else: + return host.platform.encode("utf-8") + cmp_funcs = { - "revision": lambda a, b: cmp(a[7], b[7]), - "age": lambda a, b: cmp(a[0], b[0]), - "host": lambda a, b: cmp(a[2], b[2]), - "platform": lambda a, b: cmp(a[1], b[1]), - "compiler": lambda a, b: cmp(a[3], b[3]), - "status": lambda a, b: cmp(a[6], b[6]), + "revision": lambda a, b: cmp(a.revision, b.revision), + "age": lambda a, b: cmp(a.age, b.age), + "host": lambda a, b: cmp(a.host, b.host), + "platform": lambda a, b: cmp(build_platform_safe(a), build_platform_safe(b)), + "compiler": lambda a, b: cmp(a.compiler, b.compiler), + "status": lambda a, b: cmp(a.status(), b.status()), } - assert tree in self.buildfarm.trees, "not a build tree" - assert sort_by in cmp_funcs, "not a valid sort" + if sort_by is None: + sort_by = "age" - t = self.buildfarm.trees[tree] + if sort_by not in cmp_funcs: + yield "not a valid sort mechanism: %r" % sort_by + return - for host in self.buildfarm.hostdb.hosts(): - for compiler in self.buildfarm.compilers: - try: - build = buildfarm.get_build(tree, host.name.encode("utf-8"), compiler) - status = build_status_html(myself, build) - except data.NoSuchBuildError: - pass - else: - age_mtime = build.age_mtime() - age_ctime = build.age_ctime() - try: - (revision, revision_time) = build.revision_details() - except data.MissingRevisionInfo: - pass - else: - all_builds.append([ - age_ctime, - host.platform.encode("utf-8"), - "%s" - % (myself, host.name.encode("utf-8"), - tree, compiler, host.name.encode("utf-8"), - host.name.encode("utf-8")), - compiler, tree, status, build.status(), - revision_link(myself, revision, tree), - revision_time]) + all_builds = list(self.buildfarm.get_tree_builds(tree)) all_builds.sort(cmp_funcs[sort_by]) + t = self.buildfarm.trees[tree] + sorturl = "%s?tree=%s;function=Recent+Builds" % (myself, tree) yield "
" @@ -666,94 +611,85 @@ class ViewRecentBuildsPage(BuildFarmPage): yield "" for build in all_builds: - yield "" - yield "%s" % util.dhm_time(build[0]) - yield "%s" % build[7] - yield "%s" % build[4] - yield "%s" % build[1] - yield "%s" % build[2] - yield "%s" % build[3] - yield "%s" % build[5] - yield "" + try: + build_platform_name = build_platform(build) + yield "" + yield "%s" % util.dhm_time(build.age) + yield "%s" % revision_link(myself, build.revision, build.tree) + yield "%s" % build.tree + yield "%s" % build_platform_name + yield "%s" % host_link(myself, build.host) + yield "%s" % build.compiler + yield "%s" % build_link(myself, build) + yield "" + except hostdb.NoSuchHost: + pass yield "" yield "
" class ViewHostPage(BuildFarmPage): - def _render_build(self, myself, build): - try: - (revision, revision_time) = build.revision_details() - except data.MissingRevisionInfo: - revision = None - age_mtime = build.age_mtime() - age_ctime = build.age_ctime() + def _render_build_list_header(self, host): + yield "" else: - yield "
" - yield "" - yield "

%s - %s

" % (host, self.buildfarm.hostdb.host(host).platform.encode("utf-8")) - yield "" - yield "" - yield "" - - if output_type == 'text': - yield "%-12s %-10s %-10s %-10s %-10s\n" % ( - tree, compiler, util.dhm_time(age_mtime), - util.strip_html(status), warnings) - else: - yield "" - yield "" - yield "" - yield "" - yield "" % status - yield "" % warnings - yield "" - row+=1 + deadhosts.append(hostname) - def render(self, myself, output_type, *requested_hosts): - """print the host's table of information""" + yield "" + yield "".join(self.draw_dead_hosts(*deadhosts)) - if output_type == 'text': - yield "Host summary:\n" - else: - yield "
" - yield '

Host summary:

' + def render_text(self, myself, *requested_hosts): + """print the host's table of information""" + yield "Host summary:\n" for host in requested_hosts: # make sure we have some data from it try: - self.buildfarm.hostdb.host(host) + self.buildfarm.hostdb[host] except hostdb.NoSuchHost: continue - row = 0 - for compiler in self.buildfarm.compilers: - for tree in sorted(self.buildfarm.trees.keys()): - try: - build = buildfarm.get_build(tree, host, compiler) - except data.NoSuchBuildError: - pass - else: - self._render_build(myself, build) - - if row != 0: - if output_type == 'text': - yield "\n" - else: - yield "
TargetBuild
Revision
Build
Age
Status
config/build
install/test
Warnings
" + self.tree_link(myself, tree) +"/" + compiler + "" + revision_link(myself, revision, tree) + "
" + self.red_age(age_mtime) + "
%s
%s
" - yield "
" - else: - deadhosts.append(host) - - if output_type != 'text': - yield "
" - yield "".join(self.draw_dead_hosts(*deadhosts)) + builds = list(self.buildfarm.get_host_builds(host)) + if len(builds) > 0: + yield "%-12s %-10s %-10s %-10s %-10s\n" % ( + "Tree", "Compiler", "Build Age", "Status", "Warnings") + for build in builds: + yield "%-12s %-10s %-10s %-10s %-10s\n" % ( + build.tree, build.compiler, + util.dhm_time(build.age), + str(build.status()), build.err_count()) + yield "\n" def draw_dead_hosts(self, *deadhosts): """Draw the "dead hosts" table""" @@ -769,10 +705,14 @@ class ViewHostPage(BuildFarmPage): yield "" for host in deadhosts: - age_ctime = self.buildfarm.hostdb.host_age(host) + last_build = self.buildfarm.host_last_build(host) + age = time.time() - last_build + try: + platform = self.buildfarm.hostdb[host].platform.encode("utf-8") + except hostdb.NoSuchHost: + continue yield "%s%s%s" %\ - (host, self.buildfarm.hostdb.host(host).platform.encode("utf-8"), - util.dhm_time(age_ctime)) + (host, platform, util.dhm_time(age)) yield "" yield "" @@ -780,93 +720,160 @@ class ViewHostPage(BuildFarmPage): class ViewSummaryPage(BuildFarmPage): - def render(self, myself, output_type): - """view build summary""" - i = 0 - cols = 2 - broken = 0 - broken_count = {} - panic_count = {} - host_count = {} - - # zero broken and panic counters - for tree in self.buildfarm.trees: - broken_count[tree] = 0 - panic_count[tree] = 0 - host_count[tree] = 0 + def _get_counts(self): + broken_count = defaultdict(lambda: 0) + panic_count = defaultdict(lambda: 0) + host_count = defaultdict(lambda: 0) # set up a variable to store the broken builds table's code, so we can # output when we want broken_table = "" - last_host = "" + builds = self.buildfarm.get_summary_builds() + + for tree, status_str in builds: + host_count[tree]+=1 + status = BuildStatus.__deserialize__(status_str) + + if status.failed: + broken_count[tree]+=1 + if "panic" in status.other_failures: + panic_count[tree]+=1 + return (host_count, broken_count, panic_count) + + def render_text(self, myself): + (host_count, broken_count, panic_count) = self._get_counts() # for the text report, include the current time - if output_type == 'text': - t = time.gmtime() - yield "Build status as of %s\n\n" % t + yield "Build status as of %s\n\n" % time.asctime() - for host in self.buildfarm.hostdb.hosts(): - for compiler in self.buildfarm.compilers: - for tree in self.buildfarm.trees: - try: - build = buildfarm.get_build(tree, host.name.encode("utf-8"), compiler) - status = build_status_html(myself, build) - except data.NoSuchBuildError: - continue - age_mtime = build.age_mtime() - host_count[tree]+=1 - - if "status failed" in status: - broken_count[tree]+=1 - if "PANIC" in status: - panic_count[tree]+=1 - - if output_type == 'text': - yield "Build counts:\n" - yield "%-12s %-6s %-6s %-6s\n" % ("Tree", "Total", "Broken", "Panic") - else: - yield "
" - yield "

Build counts:

" - yield "" - yield "" - yield "" + yield "Build counts:\n" + yield "%-12s %-6s %-6s %-6s\n" % ("Tree", "Total", "Broken", "Panic") + + for tree in sorted(self.buildfarm.trees.keys()): + yield "%-12s %-6s %-6s %-6s\n" % (tree, host_count[tree], + broken_count[tree], panic_count[tree]) + yield "\n" + + def render_html(self, myself): + """view build summary""" + + (host_count, broken_count, panic_count) = self._get_counts() + + yield "
" + yield "

Build counts:

" + yield "
TreeTotalBrokenPanicTest coverage
" + yield "" + yield "" for tree in sorted(self.buildfarm.trees.keys()): - if output_type == 'text': - yield "%-12s %-6s %-6s %-6s\n" % (tree, host_count[tree], - broken_count[tree], panic_count[tree]) + yield "" + yield "" % self.tree_link(myself, tree) + yield "" % host_count[tree] + yield "" % broken_count[tree] + if panic_count[tree]: + yield "" - yield "" % self.tree_link(myself, tree) - yield "" % host_count[tree] - yield "" % broken_count[tree] - if panic_count[tree]: - yield "" % panic_count[tree] + + try: + lcov_status = self.buildfarm.lcov_status(tree) + except NoSuchBuildError: + yield "" + else: + if lcov_status is not None: + yield "" % ( + self.buildfarm.LCOVHOST, tree, lcov_status) else: - yield "" % panic_count[tree] - try: - lcov_status = buildfarm.lcov_status(tree) - except data.NoSuchBuildError: yield "" + + try: + unused_fns = self.buildfarm.unused_fns(tree) + except NoSuchBuildError: + yield "" + else: + if unused_fns is not None: + yield "" % ( + self.buildfarm.LCOVHOST, tree, unused_fns) else: - if lcov_status is not None: - yield "" % (buildfarm.LCOVHOST, tree, lcov_status) - else: - yield "" - yield "" + yield "" + yield "" - if output_type == 'text': - yield "\n" + yield "
TreeTotalBrokenPanicTest coverage
%s%s%s" else: - yield "
%s%s%s" + yield "" + yield "%d%s %%" - yield "%dUnused Functions%s %%
" + yield "
" + + +class HistoryPage(BuildFarmPage): + + def history_row_html(self, myself, entry, tree, changes): + """show one row of history table""" + msg = cgi.escape(entry.message) + t = time.asctime(time.gmtime(entry.date)) + age = util.dhm_time(time.time()-entry.date) + + t = t.replace(" ", " ") + + yield """ +
+
+ %s
+ %s ago""" % (t, age) + if entry.revision: + yield " - %s
" % entry.revision + revision_url = "revision=%s" % entry.revision else: - yield "" - yield "
" + revision_url = "author=%s" % entry.author + yield """
+
+ show diffs +
+ download diffs +
+
+
%s
+
+
+
+ Author: %s +
""" % (myself, tree.name, entry.date, revision_url, + myself, tree.name, entry.date, revision_url, + msg, entry.author) + + (added, modified, removed) = changes + + if modified: + yield "
Modified: " + yield web_paths(tree, modified) + yield "
\n" + + if added: + yield "
Added: " + yield web_paths(tree, added) + yield "
\n" + + if removed: + yield "
Removed: " + yield web_paths(tree, removed) + yield "
\n" + + builds = list(self.buildfarm.get_revision_builds(tree.name, entry.revision)) + if builds: + yield "
\n" + yield "Builds: \n" + for build in builds: + yield "%s(%s) " % (build_link(myself, build), host_link(myself, build.host)) + yield "
\n" + yield "\n" -class DiffPage(BuildFarmPage): +class DiffPage(HistoryPage): def render(self, myself, tree, revision): - t = self.buildfarm.trees[tree] + try: + t = self.buildfarm.trees[tree] + except KeyError: + yield "Unknown tree %s" % tree + return branch = t.get_branch() (entry, diff) = branch.diff(revision) # get information about the current diff @@ -874,11 +881,12 @@ class DiffPage(BuildFarmPage): tree, t.branch, revision) yield "

%s

" % title changes = branch.changes_summary(revision) - yield "".join(history_row_html(myself, entry, t, changes)) - yield show_diff(diff, "html") + yield "".join(self.history_row_html(myself, entry, t, changes)) + diff = highlight(diff, DiffLexer(), HtmlFormatter()) + yield "
%s
\n" % diff.encode("utf-8") -class RecentCheckinsPage(BuildFarmPage): +class RecentCheckinsPage(HistoryPage): limit = 40 @@ -891,17 +899,14 @@ class RecentCheckinsPage(BuildFarmPage): for entry in branch.log(limit=HISTORY_HORIZON): m = re_author.match(entry.author) authors[m.group(2)] = m.group(1) - if author in ("ALL", "", m.group(2)): + if author in (None, "ALL", m.group(2)): interesting.append(entry) yield "

Recent checkins for %s (%s branch %s)

\n" % ( tree, t.scm, t.branch) yield "
" yield "Select Author: " - yield "" + yield "".join(select(name="author", values=authors, default=author)) yield "" yield "" % tree yield "" @@ -909,7 +914,7 @@ class RecentCheckinsPage(BuildFarmPage): for entry in interesting[:self.limit]: changes = branch.changes_summary(entry.revision) - yield "".join(history_row_html(myself, entry, t, changes)) + yield "".join(self.history_row_html(myself, entry, t, changes)) yield "\n" @@ -918,26 +923,20 @@ class BuildFarmApp(object): def __init__(self, buildfarm): self.buildfarm = buildfarm - # host.properties are unicode object and the framework expect string object - self.hosts = dict([(host.name.encode("utf-8"), host) for host in self.buildfarm.hostdb.hosts()]) - - def main_menu(self): + def main_menu(self, tree, host, compiler): """main page""" yield "\n" yield "
\n" - yield "\n" - yield "\n" - yield "\n" + host_dict = {} + for h in self.buildfarm.hostdb.hosts(): + host_dict[h.name] = "%s -- %s" % (h.platform.encode("utf-8"), h.name) + yield "".join(select("host", host_dict, default=host)) + tree_dict = {} + for t in self.buildfarm.trees.values(): + tree_dict[t.name] = "%s:%s" % (t.name, t.branch) + yield "".join(select("tree", tree_dict, default=tree)) + yield "".join(select("compiler", dict(zip(self.buildfarm.compilers, self.buildfarm.compilers)), default=compiler)) yield "
\n" yield "\n" yield "\n" @@ -947,6 +946,32 @@ class BuildFarmApp(object): yield "
\n" yield "
\n" + def html_page(self, form, lines): + yield "\n" + yield " \n" + yield " samba.org build farm\n" + yield " \n" + yield " \n" + yield " \n" + yield " " + yield " " + yield " " + yield " " + yield " " + yield "" + + yield util.FileLoad(os.path.join(webdir, "header2.html")) + + tree = get_param(form, "tree") + host = get_param(form, "host") + compiler = get_param(form, "compiler") + yield "".join(self.main_menu(tree, host, compiler)) + yield util.FileLoad(os.path.join(webdir, "header3.html")) + yield "".join(lines) + yield util.FileLoad(os.path.join(webdir, "footer.html")) + yield "" + yield "" + def __call__(self, environ, start_response): form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ) fn_name = get_param(form, 'function') or '' @@ -961,77 +986,149 @@ class BuildFarmApp(object): (entry, diff) = branch.diff(revision) changes = branch.changes_summary(revision) yield "".join(history_row_text(entry, tree, changes)) - yield show_diff(diff, "text") + yield "%s\n" % diff elif fn_name == 'Text_Summary': start_response('200 OK', [('Content-type', 'text/plain')]) page = ViewSummaryPage(self.buildfarm) - yield "".join(page.render(myself, 'text')) - else: - start_response('200 OK', [('Content-type', 'text/html')]) - - yield "\n" - yield " \n" - yield " samba.org build farm\n" - yield " \n" - yield " \n" - yield " \n" - yield " " - yield " " - yield " " - yield " " - yield " " - yield "" - - yield util.FileLoad(os.path.join(webdir, "header2.html")) - yield "".join(self.main_menu()) - yield util.FileLoad(os.path.join(webdir, "header3.html")) + yield "".join(page.render_text(myself)) + elif fn_name: + start_response('200 OK', [ + ('Content-type', 'text/html; charset=utf-8')]) + + tree = get_param(form, "tree") + host = get_param(form, "host") + compiler = get_param(form, "compiler") + if fn_name == "View_Build": plain_logs = (get_param(form, "plain") is not None and get_param(form, "plain").lower() in ("yes", "1", "on", "true", "y")) - tree = get_param(form, "tree") - host = get_param(form, "host") - compiler = get_param(form, "compiler") - page = ViewBuildPage(self.buildfarm) - yield "".join(page.render(myself, tree, host, compiler, get_param(form, "revision"), plain_logs)) + revision = get_param(form, "revision") + checksum = get_param(form, "checksum") + try: + build = self.buildfarm.get_build(tree, host, + compiler, revision, checksum=checksum) + except NoSuchBuildError: + yield "No such build: %s on %s with %s, rev %r, checksum %r" % ( + tree, host, compiler, revision, checksum) + else: + page = ViewBuildPage(self.buildfarm) + plain_logs = (get_param(form, "plain") is not None and get_param(form, "plain").lower() in ("yes", "1", "on", "true", "y")) + yield "".join(self.html_page(form, page.render(myself, build, plain_logs))) elif fn_name == "View_Host": page = ViewHostPage(self.buildfarm) - yield "".join(page.render(myself, "html", get_param(form, 'host'))) + yield "".join(self.html_page(form, page.render_html(myself, get_param(form, 'host')))) elif fn_name == "Recent_Builds": page = ViewRecentBuildsPage(self.buildfarm) - yield "".join(page.render(myself, get_param(form, "tree"), get_param(form, "sortby") or "revision")) + yield "".join(self.html_page(form, page.render(myself, get_param(form, "tree"), get_param(form, "sortby") or "age"))) elif fn_name == "Recent_Checkins": # validate the tree - tree = get_param(form, "tree") author = get_param(form, 'author') page = RecentCheckinsPage(self.buildfarm) - yield "".join(page.render(myself, tree, author)) + yield "".join(self.html_page(form, page.render(myself, tree, author))) elif fn_name == "diff": - tree = get_param(form, "tree") revision = get_param(form, 'revision') page = DiffPage(self.buildfarm) - yield "".join(page.render(myself, tree, revision)) - elif os.getenv("PATH_INFO") not in (None, "", "/"): - paths = os.getenv("PATH_INFO").split('/') - if paths[1] == "recent": - page = ViewRecentBuildsPage(self.buildfarm) - yield "".join(page.render(myself, paths[2], get_param(form, 'sortby') or 'revision')) - elif paths[1] == "host": - page = ViewHostPage(self.buildfarm) - yield "".join(page.render(myself, "html", paths[2])) + yield "".join(self.html_page(form, page.render(myself, tree, revision))) + elif fn_name == "Summary": + page = ViewSummaryPage(self.buildfarm) + yield "".join(self.html_page(form, page.render_html(myself))) else: + yield "Unknown function %s" % fn_name + else: + fn = wsgiref.util.shift_path_info(environ) + if fn == "tree": + tree = wsgiref.util.shift_path_info(environ) + subfn = wsgiref.util.shift_path_info(environ) + if subfn in ("", None, "+recent"): + start_response('200 OK', [ + ('Content-type', 'text/html; charset=utf-8')]) + page = ViewRecentBuildsPage(self.buildfarm) + yield "".join(self.html_page(form, page.render(myself, tree, get_param(form, 'sortby') or 'age'))) + elif subfn == "+recent-ids": + start_response('200 OK', [ + ('Content-type', 'text/plain; charset=utf-8')]) + yield "".join([x.log_checksum()+"\n" for x in self.buildfarm.get_tree_builds(tree) if x.has_log()]) + else: + start_response('200 OK', [ + ('Content-type', 'text/html; charset=utf-8')]) + yield "Unknown subfn %s" % subfn + elif fn == "host": + start_response('200 OK', [ + ('Content-type', 'text/html; charset=utf-8')]) + page = ViewHostPage(self.buildfarm) + yield "".join(self.html_page(form, page.render_html(myself, wsgiref.util.shift_path_info(environ)))) + elif fn == "build": + build_checksum = wsgiref.util.shift_path_info(environ) + try: + build = self.buildfarm.builds.get_by_checksum(build_checksum) + except NoSuchBuildError: + start_response('404 Page Not Found', [ + ('Content-Type', 'text/html; charset=utf8')]) + yield "No build with checksum %s found" % build_checksum + return + page = ViewBuildPage(self.buildfarm) + subfn = wsgiref.util.shift_path_info(environ) + if subfn == "+plain": + start_response('200 OK', [ + ('Content-type', 'text/html; charset=utf-8')]) + yield "".join(page.render(myself, build, True)) + elif subfn == "+subunit": + start_response('200 OK', [ + ('Content-type', 'text/x-subunit; charset=utf-8'), + ('Content-Disposition', 'attachment; filename="%s.%s.%s-%s.subunit"' % (build.tree, build.host, build.compiler, build.revision))]) + try: + yield build.read_subunit().read() + except NoTestOutput: + yield "There was no test output" + elif subfn == "+stdout": + start_response('200 OK', [ + ('Content-type', 'text/plain; charset=utf-8'), + ('Content-Disposition', 'attachment; filename="%s.%s.%s-%s.log"' % (build.tree, build.host, build.compiler, build.revision))]) + yield build.read_log().read() + elif subfn == "+stderr": + start_response('200 OK', [ + ('Content-type', 'text/plain; charset=utf-8'), + ('Content-Disposition', 'attachment; filename="%s.%s.%s-%s.err"' % (build.tree, build.host, build.compiler, build.revision))]) + yield build.read_err().read() + elif subfn == "+subunit-diff": + start_response('200 OK', [ + ('Content-type', 'text/plain; charset=utf-8')]) + subunit_this = build.read_subunit().readlines() + other_build_checksum = wsgiref.util.shift_path_info(environ) + other_build = self.buildfarm.builds.get_by_checksum(other_build_checksum) + subunit_other = other_build.read_subunit().readlines() + import difflib + yield "".join(difflib.unified_diff(subunit_other, subunit_this)) + + elif subfn in ("", "limit", None): + if subfn == "limit": + try: + limit = int(wsgiref.util.shift_path_info(environ)) + except: + limit = 10 + else: + limit = 10 + start_response('200 OK', [ + ('Content-type', 'text/html; charset=utf-8')]) + yield "".join(self.html_page(form, page.render(myself, build, False, limit))) + elif fn in ("", None): + start_response('200 OK', [ + ('Content-type', 'text/html; charset=utf-8')]) page = ViewSummaryPage(self.buildfarm) - yield "".join(page.render(myself, 'html')) - yield util.FileLoad(os.path.join(webdir, "footer.html")) - yield "" - yield "" + yield "".join(self.html_page(form, page.render_html(myself))) + else: + start_response('404 Page Not Found', [ + ('Content-type', 'text/html; charset=utf-8')]) + yield "Unknown function %s" % fn if __name__ == '__main__': import optparse parser = optparse.OptionParser("[options]") - parser.add_option("--cachedirname", help="Cache directory name", type=str) - parser.add_option("--port", help="Port to listen on [localhost:8000]", default="localhost:8000", type=str) + parser.add_option("--port", help="Port to listen on [localhost:8000]", + default="localhost:8000", type=str) opts, args = parser.parse_args() - buildfarm = CachingBuildFarm(cachedirname=opts.cachedirname) + from buildfarm import BuildFarm + buildfarm = BuildFarm() buildApp = BuildFarmApp(buildfarm) from wsgiref.simple_server import make_server import mimetypes