Mercurial > bitten > bitten-test
changeset 250:0271a2b1fc23
Improvements to the web interface:
* The build configuration page will now display the result of the individual steps for each build.
* Different icon for failed builds in the timeline. Closes #26.
* The timeline events will now show which steps failed for failed builds. In the RSS feed, it'll also include the actual error messages.
* The build view now displays more information about the build slave, and in a more structured way.
author | cmlenz |
---|---|
date | Tue, 04 Oct 2005 20:44:56 +0000 |
parents | dcba83c01266 |
children | d66359b298d4 |
files | bitten/model.py bitten/trac_ext/htdocs/bitten.css bitten/trac_ext/htdocs/bitten_buildf.png bitten/trac_ext/htdocs/failure.png bitten/trac_ext/templates/bitten_build.cs bitten/trac_ext/templates/bitten_config.cs bitten/trac_ext/web_ui.py |
diffstat | 7 files changed, 238 insertions(+), 43 deletions(-) [+] |
line wrap: on
line diff
--- a/bitten/model.py +++ b/bitten/model.py @@ -609,18 +609,22 @@ fetch = classmethod(fetch) - def select(cls, env, build=None, name=None, db=None): + def select(cls, env, build=None, name=None, status=None, db=None): """Retrieve existing build steps from the database that match the specified criteria. """ if not db: db = env.get_db_cnx() + assert status in (None, BuildStep.SUCCESS, BuildStep.FAILURE) + where_clauses = [] if build is not None: where_clauses.append(("build=%s", build)) if name is not None: where_clauses.append(("name=%s", name)) + if status is not None: + where_clauses.append(("status=%s", status)) if where_clauses: where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) else:
--- a/bitten/trac_ext/htdocs/bitten.css +++ b/bitten/trac_ext/htdocs/bitten.css @@ -1,15 +1,27 @@ /* Timeline styles */ -#content.timeline dt.successbuild, #content.timeline dt.successbuild a, +#content.timeline dt.successbuild, #content.timeline dt.successbuild a { + background-image: url(bitten_build.png) !important; +} #content.timeline dt.failedbuild, #content.timeline dt.failedbuild a { - background-image: url(bitten_build.png) !important + background-image: url(bitten_buildf.png) !important; } +#content.build h2.config, #content.build h2.step { background: #f7f7f7; + border-bottom: 1px solid #d7d7d7; margin: 2em 0 0; +} +#content.build h2.config :link, #content.build h2.config :visited { + color: #b00; + display: block; + border-bottom: none; +} #content.build h2.deactivated { text-decoration: line-through; } #content.build #prefs { line-height: 1.4em; } +#content.build form.config { margin-top: 1em; } #content.build form.config th { text-align: left; } #content.build form.config fieldset { margin-bottom: 1em; } -#content.build form.platforms ul { list-style: none; padding-left: 1em; } +#content.build div.platforms { margin-top: 2em; } +#content.build form.platforms ul { list-style-type: none; padding-left: 1em; } #content.build #charts { clear: right; float: right; width: 44%; } @@ -23,9 +35,61 @@ #content.build #builds td :link, #content.build #builds td :visited { font-weight: bold; } -#content.build #builds td.completed { background: #9d9; } -#content.build #builds td.failed { background: #d99; } -#content.build #builds td.in-progress { background: #ff9; } +#content.build #builds tbody td { background-position: 2px .5em; + background-repeat: no-repeat; +} +#content.build #builds td.completed { + background-color: #e8f6e8; background-image: url(bitten_build.png); +} +#content.build #builds td.failed { + background-color: #fbe8e7; background-image: url(bitten_buildf.png); +} +#content.build #builds td.in-progress { + background-color: #f6fae0; background-image: url(bitten_build.png); +} +#content.build #builds .info { margin-left: 16px; } +#content.build #builds :link, #content.build #builds :visited { + text-decoration: none; +} +#content.build #builds .info .status { color: #000; } +#content.build #builds .info .system { color: #999; font-size: smaller; + line-height: 1.2em; margin-top: .5em; +} +#content.build #builds ul.steps { + list-style-type: none; margin: .5em 0 0; padding: 0; +} +#content.build #builds ul.steps li.success, +#content.build #builds ul.steps li.failed { + border: 1px solid; margin: 1px 0; padding: 0 2px 0 12px; +} +#content.build #builds ul.steps li.success { + background: #9d9; border-color: #696; color: #393; +} +#content.build #builds ul.steps li.failed { + background: #d99 url(failure.png) 2px .3em no-repeat; border-color: #966; + color: #933; +} +#content.build #builds ul.steps li :link, +#content.build #builds ul.steps li :visited { border: none; color: inherit; + font-weight: bold; text-decoration: none; +} +#content.build #builds ul.steps li .duration { float: right; + font-size: smaller; +} +#content.build #builds ul.steps li.success .duration { color: #696; } +#content.build #builds ul.steps li.failed .duration { color: #966; } +#content.build #builds ul.steps li.failed ul { font-size: smaller; + line-height: 1.2em; list-style-type: square; margin: 0; + padding: 0 0 .5em 1.5em; +} + +#content.build #overview { line-height: 130%; margin-top: 1em; padding: .5em; } +#content.build #overview dt { font-weight: bold; padding-right: .25em; + position: absolute; left: 0; text-align: right; width: 11.5em; +} +#content.build #overview dd { margin-left: 12em; } +#content.build #overview .slave { margin-top: 1em; } +#content.build #overview .time { margin-top: 1em; } #content.build .tabs { list-style: none; float: left; width: 100%; margin: 0; padding: 0; }
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a5b902f0caf9aa53a97b38b3cc0436f483f24d92 GIT binary patch literal 289 zc%17D@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OSkfJR9T^y|-MHc(VFct$mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$J4)6(ajf#pYj?%Q$;+<XN^m*E} z^GhQ7rp>XkvO0SF^s{Hr?q_Cp^Ygc~wA^4|`2YWZT3Q;AJlGg74b(1J666>B9}O_5 zuAP|#lnnQDaSW-rWpXB3=zxHLOJK{4^e_LyXB!GGJ!LDumLWmuQAOaQw`RtEZ{{7@ zGI8&O<x?d09%JvmDl+jiqr%--{zdac4hcVYkJ;;%cV&0>YE#Cm5hi<>co>u_*nX^- SuDb?k1%s!npUXO@geCxb9(I)g
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..702258c3d72fb128148646894daed3c9facc0513 GIT binary patch literal 206 zc%17D@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxfSkfJR9T^y|-MHc(VFct$mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-#;3h)VW{XA{j{mjg6e*XXe|KDI> z0Fnm{m_SCdl?3?({|5nv&HI<^2Z|VYx;TbZ+;TZ($ala%fXPwe&Hw#xK5I_OEaN(2 t-}tumYMQ~{bx-Un99R8yFnY>iz@R9@@I-illP6FwgQu&X%Q~loCIE_oN?HH_
--- a/bitten/trac_ext/templates/bitten_build.cs +++ b/bitten/trac_ext/templates/bitten_build.cs @@ -4,16 +4,42 @@ <div id="ctxtnav" class="nav"></div> <div id="content" class="build"> <h1><?cs var:title ?></h1> - <p class="trigger">Triggered by: Changeset <a href="<?cs - var:build.chgset_href ?>">[<?cs var:build.rev ?>]</a> of <a href="<?cs - var:build.config.href ?>"><?cs var:build.config.name ?></a></p> - <p class="slave">Built by: <strong title="<?cs - var:build.slave.ip_address ?>"><?cs var:build.slave.name ?></strong> (<?cs - var:build.slave.os ?> <?cs var:build.slave.os.version ?><?cs - if:build.slave.machine ?> on <?cs var:build.slave.machine ?><?cs - /if ?>)</p> - <p class="time">Completed: <?cs var:build.started ?> (<?cs - var:build.started_delta ?> ago)<br />Took: <?cs var:build.duration ?></p><?cs + <dl id="overview"> + <dt class="config">Configuration:</dt> + <dd class="config"><a href="<?cs var:build.config.href ?>"><?cs + var:build.config.name ?></a> + </dd> + <dt class="trigger">Triggered by:</dt> + <dd class="trigger">Changeset <a href="<?cs + var:build.chgset_href ?>">[<?cs var:build.rev ?>]</a> by <?cs + var:build.chgset_author ?> + </dd> + <dt class="slave">Built by:</dt> + <dd class="slave"><code><?cs var:build.slave.name ?></code> (<?cs + var:build.slave.ipnr ?>) + </dd> + <dt class="os">Operating system:</dt> + <dd><?cs var:build.slave.os.name ?> <?cs var:build.slave.os.version ?> (<?cs + var:build.slave.os.family ?>) + </dd><?cs + if:build.slave.machine ?> + <dt class="machine">Hardware:</dt> + <dd class="machine"><?cs + var:build.slave.machine ?><?cs + if:build.slave.processor ?> (<?cs + var:build.slave.processor ?>)<?cs + /if ?> + </dd><?cs + /if ?> + <dt class="time">Completed:</dt> + <dd class="time"><?cs var:build.started ?> (<?cs + var:build.started_delta ?> ago) + </dd> + <dt class="duration">Took:</dt> + <dd class="duration"><?cs + var:build.duration ?> + </dd> + </dl><?cs if:build.can_delete ?> <div class="buttons"> <form method="post" action=""><div> @@ -23,7 +49,7 @@ </div><?cs /if ?><?cs each:step = build.steps ?> - <h2 id="<?cs var:step.name ?>"><?cs var:step.name ?> (<?cs + <h2 class="step" id="step_<?cs var:step.name ?>"><?cs var:step.name ?> (<?cs var:step.duration ?>)</h2> <p><?cs var:step.description ?></p><?cs if:len(step.errors) ?>
--- a/bitten/trac_ext/templates/bitten_config.cs +++ b/bitten/trac_ext/templates/bitten_config.cs @@ -33,8 +33,10 @@ </div> </form><?cs each:config = configs ?> - <h2<?cs if:!config.active ?> class="deactivated"<?cs /if ?>><a href="<?cs - var:config.href ?>"><?cs var:config.label ?></a></h2><?cs + <h2 class="config <?cs + if:!config.active ?>deactivated<?cs + /if ?>"><a href="<?cs var:config.href ?>"><?cs + var:config.label ?></a></h2><?cs if:config.description ?><div class="description"><?cs var:config.description ?></div><?cs /if ?><?cs @@ -139,9 +141,20 @@ /if ?> </div></form><?cs /if ?> - <ul><li>Path: <?cs if:config.path ?><a href="<?cs + <p class="path"> + Repository path: <?cs if:config.path ?><a href="<?cs var:config.browser_href ?>"><?cs - var:config.path ?></a></li><?cs /if ?></ul><?cs + var:config.path ?></a><?cs else ?>—<?cs /if ?><?cs + if:config.min_rev || config.max_rev ?> (<?cs + if:config.min_rev ?>starting at <a href="<?cs + var:config.min_rev_href ?>">[<?cs var:config.min_rev ?>]</a><?cs + /if ?><?cs + if:config.min_rev && config.max_rev ?>, <?cs /if ?><?cs + if:config.max_rev ?>up to <a href="<?cs + var:config.max_rev_href ?>">[<?cs var:config.max_rev ?>]</a><?cs + /if ?>)<?cs + /if ?> + </p><?cs if:config.description ?><div class="description"><?cs var:config.description ?></div><?cs /if ?> @@ -183,16 +196,38 @@ each:platform = config.platforms ?><?cs if:len(rev[platform.id]) ?><?cs with:build = rev[platform.id] ?><td class="<?cs - var:build.cls ?>"><a href="<?cs - var:build.href ?>" title="View build results"><?cs - var:build.slave.name ?></a> (<?cs - var:build.slave.os ?> <?cs - var:build.slave.os.version ?>)<br /><?cs - if:build.status == 'in progress' ?>started <?cs - var:build.started_delta ?> ago<?cs - else ?>took <?cs - var:build.duration ?></td><?cs - /if ?><?cs + var:build.cls ?>"><div class="info"><a href="<?cs + var:build.href ?>" title="View build results"><?cs var:build.id ?>: + <strong class="status"><?cs + if:build.status == 'completed' ?>Success<?cs + elif:build.status == 'failed' ?>Failed<?cs + else ?>In-progress<?cs + /if ?></strong></a> + <div class="system"> + <strong class="ipnr"><?cs var:build.slave.name ?></strong> (<?cs + var:build.slave.ipnr ?>)<br /> + <?cs var:build.slave.os.name ?> <?cs var:build.slave.os.version ?><?cs + if:build.slave.machine || build.slave.processor ?> / <?cs + alt:build.slave.processor ?><?cs + var:build.slave.machine ?><?cs + /alt ?><?cs + /if ?></div></div><?cs + if:len(build.steps) ?><ul class="steps"><?cs + each:step = build.steps ?><li class="<?cs + if:step.failed ?>failed<?cs else ?>success<?cs /if ?>"> + <span class="duration"><?cs var:step.duration ?></span> <a href="<?cs + var:step.href ?>"<?cs + if:step.description ?> title="<?cs + var:step.description ?>"<?cs + /if ?>><?cs + var:name(step) ?></a><?cs + if:step.failed && len(step.errors) ?><ul><?cs + each:error = step.errors ?><li><?cs + var:error ?></li><?cs + /each ?></ul><?cs + /if ?></li><?cs + /each ?></ul><?cs + /if ?></td><?cs /with ?><?cs else ?><td>—</td><?cs /if ?><?cs
--- a/bitten/trac_ext/web_ui.py +++ b/bitten/trac_ext/web_ui.py @@ -7,13 +7,18 @@ # you should have received as part of this distribution. The terms # are also available at http://bitten.cmlenz.net/wiki/License. +from datetime import datetime, timedelta import re -from time import localtime, strftime +try: + set +except NameError: + from sets import Set as set +from StringIO import StringIO import pkg_resources from trac.core import * from trac.Timeline import ITimelineEventProvider -from trac.util import escape, pretty_timedelta +from trac.util import escape, pretty_timedelta, format_date, format_datetime from trac.web import IRequestHandler from trac.web.chrome import INavigationContributor, ITemplateProvider, \ add_link, add_stylesheet @@ -36,16 +41,16 @@ 'href': env.href.build(build.config, build.id), 'chgset_href': env.href.changeset(build.rev)} if build.started: - hdf['started'] = strftime('%x %X', localtime(build.started)) + hdf['started'] = format_datetime(build.started) hdf['started_delta'] = pretty_timedelta(build.started) if build.stopped: - hdf['stopped'] = strftime('%x %X', localtime(build.stopped)) + hdf['stopped'] = format_datetime(build.stopped) hdf['stopped_delta'] = pretty_timedelta(build.stopped) hdf['duration'] = pretty_timedelta(build.stopped, build.started) hdf['slave'] = { 'name': build.slave, - 'ip_address': build.slave_info.get(Build.IP_ADDRESS), - 'os': build.slave_info.get(Build.OS_NAME), + 'ipnr': build.slave_info.get(Build.IP_ADDRESS), + 'os.name': build.slave_info.get(Build.OS_NAME), 'os.family': build.slave_info.get(Build.OS_FAMILY), 'os.version': build.slave_info.get(Build.OS_VERSION), 'machine': build.slave_info.get(Build.MACHINE), @@ -339,7 +344,9 @@ req.hdf['config.can_create'] = req.perm.has_permission('BUILD_CREATE') def _render_config(self, req, config_name): - config = BuildConfig.fetch(self.env, config_name) + db = self.env.get_db_cnx() + + config = BuildConfig.fetch(self.env, config_name, db=db) req.hdf['title'] = 'Build Configuration "%s"' \ % escape(config.label or config.name) add_link(req, 'up', self.env.href.build(), 'Build Status') @@ -348,6 +355,10 @@ description = wiki_to_html(description, self.env, req) req.hdf['config'] = { 'name': config.name, 'label': config.label, 'path': config.path, + 'min_rev': config.min_rev, + 'min_rev_href': self.env.href.changeset(config.min_rev), + 'max_rev': config.max_rev, + 'max_rev_href': self.env.href.changeset(config.max_rev), 'active': config.active, 'description': description, 'browser_href': self.env.href.browser(config.path), 'can_modify': req.perm.has_permission('BUILD_MODIFY'), @@ -355,13 +366,14 @@ } req.hdf['page.mode'] = 'view_config' - platforms = list(TargetPlatform.select(self.env, config=config_name)) + platforms = list(TargetPlatform.select(self.env, config=config_name, + db=db)) req.hdf['config.platforms'] = [ {'name': platform.name, 'id': platform.id} for platform in platforms ] has_reports = False - for report in Report.select(self.env, config=config.name): + for report in Report.select(self.env, config=config.name, db=db): has_reports = True break @@ -392,6 +404,17 @@ if build and build.status != Build.PENDING: build_hdf = _build_to_hdf(self.env, req, build) req.hdf['%s.%s' % (prefix, platform.id)] = build_hdf + for step in BuildStep.select(self.env, build=build.id, + db=db): + req.hdf['%s.%s.steps.%s' % (prefix, platform.id, + step.name)] = { + 'description': escape(step.description), + 'duration': datetime.fromtimestamp(step.stopped) - \ + datetime.fromtimestamp(step.started), + 'failed': not step.successful, + 'errors': step.errors, + 'href': build_hdf['href'] + '#step_' + step.name, + } idx += 1 if page > 1: @@ -519,6 +542,10 @@ req.hdf['build.steps'] = steps req.hdf['build.can_delete'] = req.perm.has_permission('BUILD_DELETE') + repos = self.env.get_repository(req.authname) + chgset = repos.get_changeset(build.rev) + req.hdf['build.chgset_author'] = chgset.author + add_stylesheet(req, 'bitten/bitten.css') return 'bitten_build.cs', None @@ -531,26 +558,65 @@ def get_timeline_events(self, req, start, stop, filters): if 'build' in filters: add_stylesheet(req, 'bitten/bitten.css') + db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT b.id,b.config,c.label,b.rev,p.name," "b.stopped,b.status FROM bitten_build AS b" - " INNER JOIN bitten_config AS c ON (c.name=b.config)" + " INNER JOIN bitten_config AS c ON (c.name=b.config) " " INNER JOIN bitten_platform AS p ON (p.id=b.platform) " "WHERE b.stopped>=%s AND b.stopped<=%s " "AND b.status IN (%s, %s) ORDER BY b.stopped", (start, stop, Build.SUCCESS, Build.FAILURE)) + event_kinds = {Build.SUCCESS: 'successbuild', Build.FAILURE: 'failedbuild'} for id, config, label, rev, platform, stopped, status in cursor: + + errors = [] + if status == Build.FAILURE: + for step in BuildStep.select(self.env, build=id, + status=BuildStep.FAILURE, + db=db): + errors += [(escape(step.name), escape(error)) for error + in step.errors] + title = 'Build of <em>%s [%s]</em> on %s %s' \ % (escape(label), escape(rev), escape(platform), _status_label[status]) + message = '' if req.args.get('format') == 'rss': href = self.env.abs_href.build(config, id) + if errors: + buf = StringIO() + prev_step = None + for step, error in errors: + if step != prev_step: + if prev_step is not None: + buf.write('</ul>') + buf.write('<p>Step %s failed:</p><ul>' % step) + prev_step = step + buf.write('<li>%s</li>' % escape(error)) + buf.write('</ul>') + message = buf.getvalue() else: href = self.env.href.build(config, id) - yield event_kinds[status], href, title, stopped, None, '' + if errors: + steps = [] + for step, error in errors: + if step not in steps: + steps.append(step) + steps = ['<em>%s</em>' % step for step in steps] + if len(steps) < 2: + message = steps[0] + elif len(steps) == 2: + message = ' and '.join(steps) + elif len(steps) > 2: + message = ', '.join(steps[:-1]) + ', and ' + \ + steps[-1] + message = 'Step%s ' % (len(steps) != 1 and 's' or '') \ + + message + ' failed' + yield event_kinds[status], href, title, stopped, None, message # Internal methods