launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #20364
[Merge] lp:~maxiberta/launchpad/snap-update-build-status-ui into lp:launchpad
Maximiliano Bertacchini has proposed merging lp:~maxiberta/launchpad/snap-update-build-status-ui into lp:launchpad.
Commit message:
Update latest snap builds table via AJAX.
Requested reviews:
Colin Watson (cjwatson)
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~maxiberta/launchpad/snap-update-build-status-ui/+merge/294403
Update latest snap builds table via AJAX.
Note: this is a WIP. Missing tests. See https://pastebin.canonical.com/156265/ for example of API usage.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~maxiberta/launchpad/snap-update-build-status-ui into lp:launchpad.
=== modified file 'lib/lp/buildmaster/model/buildqueue.py'
--- lib/lp/buildmaster/model/buildqueue.py 2015-07-08 16:05:11 +0000
+++ lib/lp/buildmaster/model/buildqueue.py 2016-05-14 00:27:07 +0000
@@ -260,7 +260,6 @@
def preloadForBuildFarmJobs(self, builds):
"""See `IBuildQueueSet`."""
from lp.buildmaster.model.builder import Builder
- prefetched_data = dict()
bqs = list(IStore(BuildQueue).find(
BuildQueue,
BuildQueue._build_farm_job_id.is_in(
=== modified file 'lib/lp/snappy/browser/snapbuild.py'
--- lib/lp/snappy/browser/snapbuild.py 2015-08-07 10:12:38 +0000
+++ lib/lp/snappy/browser/snapbuild.py 2016-05-14 00:27:07 +0000
@@ -16,7 +16,6 @@
action,
LaunchpadFormView,
)
-from lp.buildmaster.enums import BuildQueueStatus
from lp.services.librarian.browser import (
FileNavigationMixin,
ProxiedLibraryFileAlias,
@@ -70,39 +69,6 @@
page_title = label
@cachedproperty
- def eta(self):
- """The datetime when the build job is estimated to complete.
-
- This is the BuildQueue.estimated_duration plus the
- Job.date_started or BuildQueue.getEstimatedJobStartTime.
- """
- if self.context.buildqueue_record is None:
- return None
- queue_record = self.context.buildqueue_record
- if queue_record.status == BuildQueueStatus.WAITING:
- start_time = queue_record.getEstimatedJobStartTime()
- else:
- start_time = queue_record.date_started
- if start_time is None:
- return None
- duration = queue_record.estimated_duration
- return start_time + duration
-
- @cachedproperty
- def estimate(self):
- """If true, the date value is an estimate."""
- if self.context.date_finished is not None:
- return False
- return self.eta is not None
-
- @cachedproperty
- def date(self):
- """The date when the build completed or is estimated to complete."""
- if self.estimate:
- return self.eta
- return self.context.date_finished
-
- @cachedproperty
def files(self):
"""Return `LibraryFileAlias`es for files produced by this build."""
if not self.context.was_built:
=== modified file 'lib/lp/snappy/browser/tests/test_snapbuild.py'
--- lib/lp/snappy/browser/tests/test_snapbuild.py 2015-08-07 10:12:38 +0000
+++ lib/lp/snappy/browser/tests/test_snapbuild.py 2016-05-14 00:27:07 +0000
@@ -81,25 +81,6 @@
build_view = create_initialized_view(build, "+index")
self.assertEqual([], build_view.files)
- def test_eta(self):
- # SnapBuildView.eta returns a non-None value when it should, or None
- # when there's no start time.
- build = self.factory.makeSnapBuild()
- build.queueBuild()
- self.assertIsNone(create_initialized_view(build, "+index").eta)
- self.factory.makeBuilder(processors=[build.processor])
- self.assertIsNotNone(create_initialized_view(build, "+index").eta)
-
- def test_estimate(self):
- # SnapBuildView.estimate returns True until the job is completed.
- build = self.factory.makeSnapBuild()
- build.queueBuild()
- self.factory.makeBuilder(processors=[build.processor])
- build.updateStatus(BuildStatus.BUILDING)
- self.assertTrue(create_initialized_view(build, "+index").estimate)
- build.updateStatus(BuildStatus.FULLYBUILT)
- self.assertFalse(create_initialized_view(build, "+index").estimate)
-
class TestSnapBuildOperations(BrowserTestCase):
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2016-05-11 00:00:47 +0000
+++ lib/lp/snappy/interfaces/snap.py 2016-05-14 00:27:07 +0000
@@ -271,6 +271,21 @@
:return: `ISnapBuild`.
"""
+ @operation_parameters(
+ snap_build_ids=List(
+ title=_("A list of snap build ids."),
+ value_type=Int()))
+ @export_read_operation()
+ @operation_for_version("devel")
+ def getBuildSummariesForSnapBuildIds(snap_build_ids):
+ """Return a dictionary containing a summary of the build statuses.
+
+ :param snap_build_ids: A list of snap build ids.
+ :type source_ids: ``list``
+ :return: A dict consisting of the overall status summaries for the
+ given snap builds.
+ """
+
builds = exported(doNotSnapshot(CollectionField(
title=_("All builds of this snap package."),
description=_(
=== modified file 'lib/lp/snappy/interfaces/snapbuild.py'
--- lib/lp/snappy/interfaces/snapbuild.py 2016-05-06 16:34:21 +0000
+++ lib/lp/snappy/interfaces/snapbuild.py 2016-05-14 00:27:07 +0000
@@ -29,6 +29,7 @@
from zope.schema import (
Bool,
Choice,
+ Datetime,
Int,
)
@@ -106,6 +107,17 @@
required=True, readonly=True,
description=_("Whether this build record can be cancelled.")))
+ eta = Datetime(
+ title=_("The datetime when the build job is estimated to complete."),
+ readonly=True)
+
+ estimate = Bool(
+ title=_("If true, the date value is an estimate."), readonly=True)
+
+ date = Datetime(
+ title=_("The date when the build completed or is estimated to "
+ "complete."), readonly=True)
+
store_upload_jobs = CollectionField(
title=_("Store upload jobs for this build."),
# Really ISnapStoreUploadJob.
=== added file 'lib/lp/snappy/javascript/snap.update_build_statuses.js'
--- lib/lp/snappy/javascript/snap.update_build_statuses.js 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/javascript/snap.update_build_statuses.js 2016-05-14 00:27:07 +0000
@@ -0,0 +1,140 @@
+/* Copyright 2016 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * The lp.snappy.snap.update_build_statuses module uses the
+ * LP DynamicDomUpdater plugin for updating the latest builds table of a snap.
+ *
+ * @module Y.lp.snappy.snap.update_build_statuses
+ * @requires anim, node, lp.anim, lp.soyuz.dynamic_dom_updater
+ */
+YUI.add('lp.snappy.snap.update_build_statuses', function(Y) {
+ Y.log('loading lp.snappy.snap.update_build_statuses');
+ var module = Y.namespace('lp.snappy.snap.update_build_statuses');
+
+ module.pending_states = [
+ "NEEDSBUILD", "BUILDING", "UPLOADING", "CANCELLING"];
+
+ module.domUpdate = function(table, data_object) {
+ Y.each(data_object, function(build_summary, build_id) {
+ var ui_changed = false;
+
+ var tr_elem = Y.one("tr#build-" + build_id);
+ if (tr_elem === null) {
+ return;
+ }
+
+ var td_build_status = tr_elem.one("td.build_status");
+ var td_datebuilt = tr_elem.one("td.datebuilt");
+
+ if (td_build_status === null || td_datebuilt === null) {
+ return;
+ }
+
+ var link_node = td_build_status.one("a");
+ var img_node = td_build_status.one("img");
+
+ if (link_node === null || img_node === null) {
+ return;
+ }
+
+ if (!td_build_status.hasClass(build_summary.status)) {
+ ui_changed = true;
+ var new_src = null;
+ switch(build_summary.status) {
+ case 'BUILDING':
+ case 'UPLOADING':
+ new_src = '/@@/processing';
+ break;
+ case 'NEEDSBUILD':
+ new_src = '/@@/build-needed';
+ break;
+ case 'FAILEDTOBUILD':
+ new_src = '/@@/build-failed';
+ break;
+ case 'FULLYBUILT_PENDING':
+ new_src = '/@@/build-success-publishing';
+ break;
+ default:
+ new_src = '/@@/build-success';
+ }
+
+ td_build_status.setAttribute("class", "build_status");
+ td_build_status.addClass(build_summary.status);
+ link_node.set("innerHTML", build_summary.buildstate);
+ img_node.setAttribute("src", new_src);
+ img_node.setAttribute("title", build_summary.buildstate);
+ img_node.setAttribute("alt", "[" + build_summary.status + "]");
+ }
+
+ if (build_summary.when_complete !== null) {
+ ui_changed = true;
+ td_datebuilt.set("innerHTML", build_summary.when_complete);
+ if (build_summary.when_complete_estimate) {
+ td_datebuilt.appendChild(
+ document.createTextNode(' (estimated)'));
+ }
+ if (build_summary.build_log_url !== null) {
+ var new_link = Y.Node.create(
+ '<a class="sprite download">buildlog</a>');
+ new_link.setAttribute(
+ 'href', build_summary.build_log_url);
+ td_datebuilt.appendChild(document.createTextNode(' '));
+ td_datebuilt.appendChild(new_link);
+ if (build_summary.build_log_size !== null) {
+ td_datebuilt.appendChild(
+ document.createTextNode(' '));
+ td_datebuilt.append(
+ "(" + build_summary.build_log_size + " bytes)");
+ }
+ }
+ }
+
+ if (ui_changed) {
+ var anim = Y.lp.anim.green_flash({node: tr_elem});
+ anim.run();
+ }
+ });
+ };
+
+ module.parameterEvaluator = function(table_node) {
+ var td_list = table_node.all('td.build_status');
+ var pending = td_list.filter("." + module.pending_states.join(",."));
+ if (pending.size() === 0) {
+ return null;
+ }
+
+ var snap_build_ids = [];
+ Y.each(pending, function(node) {
+ var elem_id = node.ancestor().get('id');
+ var snap_build_id = elem_id.replace('build-', '');
+ snap_build_ids.push(snap_build_id);
+ });
+
+ return {snap_build_ids: snap_build_ids};
+ };
+
+ module.stopUpdatesCheck = function(table_node) {
+ // Stop updating when there aren't any builds to update
+ var td_list = table_node.all('td.build_status');
+ var pending = td_list.filter("." + module.pending_states.join(",."));
+ return (pending.size() === 0);
+ };
+
+ module.config = {
+ uri: null,
+ api_method_name: 'getBuildSummariesForSnapBuildIds',
+ lp_client: null,
+ domUpdateFunction: module.domUpdate,
+ parameterEvaluatorFunction: module.parameterEvaluator,
+ stopUpdatesCheckFunction: module.stopUpdatesCheck
+ };
+
+ module.setup = function(node, uri) {
+ module.config.uri = uri;
+ node.plug(Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater,
+ module.config);
+ };
+}, "0.1", {"requires":["anim",
+ "node",
+ "lp.anim",
+ "lp.soyuz.dynamic_dom_updater"]});
=== added file 'lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.html'
--- lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.html 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.html 2016-05-14 00:27:07 +0000
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 Canonical Ltd. This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<html>
+ <head>
+ <title>lp.snappy.snap.update_build_statuses Tests</title>
+
+ <!-- YUI and test setup -->
+ <script type="text/javascript"
+ src="../../../../../build/js/yui/yui/yui.js">
+ </script>
+ <link rel="stylesheet"
+ href="../../../../../build/js/yui/console/assets/console-core.css" />
+ <link rel="stylesheet"
+ href="../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
+ <link rel="stylesheet"
+ href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
+
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/testing/helpers.js"></script>
+
+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
+
+ <!-- Dependencies -->
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/client.js"></script>
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/anim/anim.js"></script>
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/extras/extras.js"></script>
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/soyuz/lp_dynamic_dom_updater.js"></script>
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/testing/assert.js"></script>
+
+ <!-- The module under test. -->
+ <script type="text/javascript" src="../snap.update_build_statuses.js"></script>
+
+ <!-- The test suite -->
+ <script type="text/javascript" src="test_snap.update_build_statuses.js"></script>
+
+ </head>
+ <body class="yui3-skin-sam">
+ <ul id="suites">
+ <li>lp.snappy.snap.update_build_statuses.test</li>
+ </ul>
+
+ <table id="latest-builds-listing" class="listing">
+ <thead>
+ <tr>
+ <th>Status</th>
+ <th>When complete</th>
+ <th>Architecture</th>
+ <th>Archive</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="build-1">
+ <td class="build_status NEEDSBUILD">
+ <img width="14" height="14" alt="[NEEDSBUILD]" title="Needs building" src="/@@/build-needed" />
+ <a href="snap/+build/1">Needs building</a>
+ </td>
+ <td class="datebuilt">
+ in 1 minute (estimated)
+ </td>
+ <td>
+ <a class="sprite distribution" href="/ubuntu/hoary/i386">i386</a>
+ </td>
+ <td>
+ <a href="/ubuntu" class="sprite distribution">Primary Archive for Ubuntu Linux</a>
+ </td>
+ </tr>
+ <tr id="build-2">
+ <td class="build_status FAILEDTOBUILD">
+ <img width="16" height="14" alt="[FAILEDTOBUILD]" title="Failed to build" src="/@@/build-failed" />
+ <a href="/~max/+snap/project3-snapo/+build/2">Failed to build</a>
+ </td>
+ <td class="datebuilt">
+ on 2016-05-10
+ <a class="sprite download" href="snap/+build/2/+files/build2.txt.gz">buildlog</a>
+ (23456 bytes)
+ </td>
+ <td>
+ <a class="sprite distribution" href="/ubuntu/hoary/i386">i386</a>
+ </td>
+ <td>
+ <a href="/ubuntu" class="sprite distribution">Primary Archive for Ubuntu Linux</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ </body>
+</html>
=== added file 'lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.js'
--- lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.js 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.js 2016-05-14 00:27:07 +0000
@@ -0,0 +1,116 @@
+/* Copyright 2016 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE). */
+
+YUI.add('lp.snappy.snap.update_build_statuses.test', function (Y) {
+ var tests = Y.namespace('lp.snappy.snap.update_build_statuses.test');
+ var module = Y.lp.snappy.snap.update_build_statuses;
+ tests.suite = new Y.Test.Suite('lp.snappy.snap.update_build_status Tests');
+
+ tests.suite.add(new Y.Test.Case({
+ name: 'lp.snappy.snap.update_build_statuses_tests',
+
+ setUp: function () {
+ this.table = Y.one('table#latest-builds-listing');
+ this.tr_build_1 = Y.one('tr#build-1');
+ this.td_status = this.tr_build_1.one('td.build_status');
+ this.td_datebuilt = this.tr_build_1.one("td.datebuilt");
+ this.td_status_class = this.td_status.getAttribute("class");
+ this.td_status_img = this.td_status.one("img");
+ this.td_status_a = this.td_status.one("a");
+ },
+
+ test_dom_updater_plugin_attached: function() {
+ Y.Assert.isUndefined(this.table._plugins.updater);
+ module.setup(this.table);
+ updater = Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater;
+ Y.Assert.areEqual(updater, this.table._plugins.updater);
+ // Unplug plugin to prevent DOM autorefresh during testing.
+ // DOM autorefresh should be tested in DynamicDomUpdater testsuite.
+ this.table.unplug(updater);
+ Y.Assert.isUndefined(this.table._plugins.updater);
+ },
+
+ test_parameter_evaluator: function() {
+ // parameterEvaluator should return an object with the ids of
+ // builds in pending states.
+ params = module.parameterEvaluator(this.table);
+ Y.lp.testing.assert.assert_equal_structure(
+ {snap_build_ids: ["1"]}, params);
+ },
+
+ test_parameter_evaluator_empty: function() {
+ // parameterEvaluator should return empty if no builds remaining
+ // in pending states.
+ this.td_status.setAttribute("class", "build_status FULLYBUILT");
+ params = module.parameterEvaluator(this.table);
+ Y.Assert.isNull(params);
+ // reset td class to the original value
+ this.td_status.setAttribute("class", this.td_status_class);
+ },
+
+ test_stop_updates_check: function() {
+ // stopUpdatesCheck should return false if pending builds exist.
+ Y.Assert.isFalse(module.stopUpdatesCheck(this.table));
+ // stopUpdatesCheck should return true if no pending builds exist.
+ this.td_status.setAttribute("class", "build_status FULLYBUILT");
+ Y.Assert.isTrue(module.stopUpdatesCheck(this.table));
+ for (i = 0; i < module.pending_states.length; i++) {
+ this.td_status.setAttribute(
+ "class", "build_status " + module.pending_states[i]);
+ Y.Assert.isFalse(module.stopUpdatesCheck(this.table));
+ }
+ // reset td class to the original value
+ this.td_status.setAttribute("class", this.td_status_class);
+ },
+
+ test_update_build_status_dom: function() {
+ var original_a_href = this.td_status_a.get("href");
+ data = {"1": {
+ "status": "BUILDING",
+ "build_log_url": null,
+ "when_complete_estimate": true,
+ "buildstate": "Currently building",
+ "build_log_size": null,
+ "when_complete": "in 1 minute"
+ }};
+ module.domUpdate(this.table, data);
+ Y.Assert.areEqual(
+ "build_status BUILDING", this.td_status.getAttribute("class"));
+ Y.Assert.areEqual(
+ "Currently building", this.td_status.get("text").trim());
+ Y.Assert.areEqual("[BUILDING]", this.td_status_img.get("alt"));
+ Y.Assert.areEqual(
+ "Currently building", this.td_status_img.get("title"));
+ Y.Assert.areEqual(
+ "file:///@@/processing", this.td_status_img.get("src"));
+ Y.Assert.areEqual(original_a_href, this.td_status_a.get("href"));
+ },
+
+ test_update_build_date_dom: function() {
+ data = {"1": {
+ "status": "NEEDSBUILD",
+ "build_log_url": "/+build/1/+files/build1.txt.gz",
+ "when_complete_estimate": true,
+ "buildstate": "Needs building",
+ "build_log_size": 12345,
+ "when_complete": "in 30 seconds"
+ }};
+ module.domUpdate(this.table, data);
+ Y.Assert.areEqual(
+ "in 30 seconds (estimated) buildlog (12345 bytes)",
+ this.td_datebuilt.get("text").trim());
+ var td_datebuilt_a = this.td_datebuilt.one("a");
+ Y.Assert.isNotNull(td_datebuilt_a);
+ Y.Assert.areEqual("buildlog", td_datebuilt_a.get("text").trim());
+ Y.Assert.areEqual(
+ "sprite download", td_datebuilt_a.getAttribute("class"));
+ Y.Assert.areEqual(
+ "file://" + data["1"].build_log_url,
+ td_datebuilt_a.get("href"));
+ }
+ }));
+
+}, '0.1', {
+ requires: ['test', 'console', 'lp.testing.assert',
+ 'lp.snappy.snap.update_build_statuses']
+});
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2016-05-11 00:00:47 +0000
+++ lib/lp/snappy/model/snap.py 2016-05-14 00:27:07 +0000
@@ -27,10 +27,13 @@
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
+
+from lp.app.browser.tales import DateTimeFormatterAPI
from lp.app.enums import PRIVATE_INFORMATION_TYPES
from lp.app.interfaces.security import IAuthorization
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.interfaces.processor import IProcessorSet
+from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
from lp.buildmaster.model.processor import Processor
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.branchcollection import (
@@ -338,6 +341,39 @@
result.order_by(order_by)
return result
+ def getBuildSummariesForSnapBuildIds(self, snap_build_ids):
+ """See `ISnap`."""
+ result = {}
+ if snap_build_ids is None:
+ return result
+ filter_term = SnapBuild.id.is_in(snap_build_ids)
+ order_by = Desc(SnapBuild.id)
+ builds = self._getBuilds(filter_term, order_by)
+
+ # Prefetch data to keep DB query count constant
+ getUtility(IBuildQueueSet).preloadForBuildFarmJobs(builds)
+
+ for build in builds:
+ if build.date is not None:
+ when_complete = DateTimeFormatterAPI(build.date).displaydate()
+ else:
+ when_complete = None
+
+ if build.log:
+ build_log_size = build.log.content.filesize
+ else:
+ build_log_size = None
+
+ result[build.id] = {}
+ result[build.id]["status"] = build.status.name
+ result[build.id]["buildstate"] = build.status
+ result[build.id]["when_complete"] = when_complete
+ result[build.id]["when_complete_estimate"] = build.estimate
+ result[build.id]["build_log_url"] = build.log_url
+ result[build.id]["build_log_size"] = build_log_size
+
+ return result
+
@property
def builds(self):
"""See `ISnap`."""
@@ -569,14 +605,8 @@
"""See `ISnapSet`."""
snaps = [removeSecurityProxy(snap) for snap in snaps]
- branch_ids = set()
- git_repository_ids = set()
person_ids = set()
for snap in snaps:
- if snap.branch_id is not None:
- branch_ids.add(snap.branch_id)
- if snap.git_repository_id is not None:
- git_repository_ids.add(snap.git_repository_id)
person_ids.add(snap.registrant_id)
person_ids.add(snap.owner_id)
=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 2016-05-06 16:34:21 +0000
+++ lib/lp/snappy/model/snapbuild.py 2016-05-14 00:27:07 +0000
@@ -29,6 +29,7 @@
from lp.app.errors import NotFoundError
from lp.buildmaster.enums import (
BuildFarmJobType,
+ BuildQueueStatus,
BuildStatus,
)
from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
@@ -352,6 +353,39 @@
return [self.lfaUrl(lfa) for _, lfa, _ in self.getFiles()]
@property
+ def eta(self):
+ """The datetime when the build job is estimated to complete.
+
+ This is the BuildQueue.estimated_duration plus the
+ Job.date_started or BuildQueue.getEstimatedJobStartTime.
+ """
+ if self.buildqueue_record is None:
+ return None
+ queue_record = self.buildqueue_record
+ if queue_record.status == BuildQueueStatus.WAITING:
+ start_time = queue_record.getEstimatedJobStartTime()
+ else:
+ start_time = queue_record.date_started
+ if start_time is None:
+ return None
+ duration = queue_record.estimated_duration
+ return start_time + duration
+
+ @property
+ def estimate(self):
+ """If true, the date value is an estimate."""
+ if self.date_finished is not None:
+ return False
+ return self.eta is not None
+
+ @property
+ def date(self):
+ """The date when the build completed or is estimated to complete."""
+ if self.estimate:
+ return self.eta
+ return self.date_finished
+
+ @property
def store_upload_jobs(self):
jobs = Store.of(self).find(
SnapBuildJob,
=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt 2016-01-27 12:43:00 +0000
+++ lib/lp/snappy/templates/snap-index.pt 2016-05-14 00:27:07 +0000
@@ -8,6 +8,18 @@
>
<body>
+ <metal:block fill-slot="head_epilogue">
+ <script type="text/javascript" id="snap-update-build-statuses">
+ LPJS.use('lp.snappy.snap.update_build_statuses', function(Y) {
+ Y.on('domready', function(e) {
+ var table = Y.one('table#latest-builds-listing');
+ var uri = LP.cache.context.self_link;
+ Y.lp.snappy.snap.update_build_statuses.setup(table, uri);
+ });
+ });
+ </script>
+ </metal:block>
+
<metal:registering fill-slot="registering">
Created by
<tal:registrant replace="structure context/registrant/fmt:link"/>
@@ -64,36 +76,34 @@
</thead>
<tbody>
<tal:snap-builds repeat="build view/builds">
- <tal:build-view define="buildview nocall:build/@@+index">
- <tr tal:attributes="id string:build-${build/id}">
- <td>
- <span tal:replace="structure build/image:icon"/>
- <a tal:content="build/status/title"
- tal:attributes="href build/fmt:url"/>
- </td>
- <td>
- <tal:date replace="buildview/date/fmt:displaydate"/>
- <tal:estimate condition="buildview/estimate">
- (estimated)
- </tal:estimate>
+ <tr tal:attributes="id string:build-${build/id}">
+ <td tal:attributes="class string:build_status ${build/status/name}">
+ <span tal:replace="structure build/image:icon"/>
+ <a tal:content="build/status/title"
+ tal:attributes="href build/fmt:url"/>
+ </td>
+ <td class="datebuilt">
+ <tal:date replace="build/date/fmt:displaydate"/>
+ <tal:estimate condition="build/estimate">
+ (estimated)
+ </tal:estimate>
- <tal:build-log define="file build/log" tal:condition="file">
- <a class="sprite download"
- tal:attributes="href build/log_url">buildlog</a>
- (<span tal:replace="file/content/filesize/fmt:bytes"/>)
- </tal:build-log>
- </td>
- <td>
- <a class="sprite distribution"
- tal:define="archseries build/distro_arch_series"
- tal:attributes="href archseries/fmt:url"
- tal:content="archseries/architecturetag"/>
- </td>
- <td>
- <tal:archive replace="structure build/archive/fmt:link"/>
- </td>
- </tr>
- </tal:build-view>
+ <tal:build-log define="file build/log" tal:condition="file">
+ <a class="sprite download"
+ tal:attributes="href build/log_url">buildlog</a>
+ (<span tal:replace="file/content/filesize/fmt:bytes"/>)
+ </tal:build-log>
+ </td>
+ <td>
+ <a class="sprite distribution"
+ tal:define="archseries build/distro_arch_series"
+ tal:attributes="href archseries/fmt:url"
+ tal:content="archseries/architecturetag"/>
+ </td>
+ <td>
+ <tal:archive replace="structure build/archive/fmt:link"/>
+ </td>
+ </tr>
</tal:snap-builds>
</tbody>
</table>
=== modified file 'lib/lp/snappy/templates/snapbuild-index.pt'
--- lib/lp/snappy/templates/snapbuild-index.pt 2015-08-07 10:12:38 +0000
+++ lib/lp/snappy/templates/snapbuild-index.pt 2016-05-14 00:27:07 +0000
@@ -134,7 +134,7 @@
</li>
</tal:started>
<tal:finish condition="not: context/date_finished">
- <li tal:define="eta view/eta" tal:condition="view/eta">
+ <li tal:define="eta context/eta" tal:condition="context/eta">
Estimated finish <tal:eta replace="eta/fmt:approximatedate"/>
</li>
</tal:finish>
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2016-05-06 09:45:45 +0000
+++ lib/lp/snappy/tests/test_snap.py 2016-05-14 00:27:07 +0000
@@ -64,6 +64,7 @@
login,
logout,
person_logged_in,
+ record_two_runs,
set_feature_flag,
StormStatementRecorder,
TestCaseWithFactory,
@@ -364,6 +365,70 @@
snap.destroySelf()
self.assertFalse(getUtility(ISnapSet).exists(owner, u"condemned"))
+ def test_getBuildSummariesForSnapBuildIds(self):
+ snap1 = self.factory.makeSnap()
+ snap2 = self.factory.makeSnap()
+ build11 = self.factory.makeSnapBuild(snap=snap1)
+ build12 = self.factory.makeSnapBuild(snap=snap1)
+ build2 = self.factory.makeSnapBuild(snap=snap2)
+ build3 = self.factory.makeSnapBuild()
+ summary1 = snap1.getBuildSummariesForSnapBuildIds(
+ [build11.id, build12.id])
+ summary2 = snap2.getBuildSummariesForSnapBuildIds([build2.id])
+ self.assertEqual([build11.id, build12.id], summary1.keys())
+ self.assertEqual([build2.id], summary2.keys())
+
+ def test_getBuildSummariesForSnapBuildIds_empty_input(self):
+ snap = self.factory.makeSnap()
+ self.factory.makeSnapBuild(snap=snap)
+ self.assertEqual({}, snap.getBuildSummariesForSnapBuildIds(None))
+ self.assertEqual({}, snap.getBuildSummariesForSnapBuildIds([]))
+ self.assertEqual({}, snap.getBuildSummariesForSnapBuildIds(()))
+ self.assertEqual({}, snap.getBuildSummariesForSnapBuildIds([None]))
+
+ def test_getBuildSummariesForSnapBuildIds_not_matching_snap(self):
+ # Should not return build summaries of other snaps.
+ snap1 = self.factory.makeSnap()
+ snap2 = self.factory.makeSnap()
+ build1 = self.factory.makeSnapBuild(snap=snap1)
+ build2 = self.factory.makeSnapBuild(snap=snap2)
+ summary1 = snap1.getBuildSummariesForSnapBuildIds([build2.id])
+ self.assertEqual({}, summary1)
+
+ def test_getBuildSummariesForSnapBuildIds_when_complete_field(self):
+ # Summary "when_complete" should be None unless estimate date or
+ # finish date is available.
+ snap = self.factory.makeSnap()
+ build = self.factory.makeSnapBuild(snap=snap)
+ self.assertIsNone(build.date)
+ summary = snap.getBuildSummariesForSnapBuildIds([build.id])
+ self.assertIsNone(summary[build.id]["when_complete"])
+ removeSecurityProxy(build).date_finished = UTC_NOW
+ summary = snap.getBuildSummariesForSnapBuildIds([build.id])
+ self.assertEqual("a moment ago", summary[build.id]["when_complete"])
+
+ def test_getBuildSummariesForSnapBuildIds_log_size_field(self):
+ # Summary "build_log_size" should be None unless the build has a log.
+ snap = self.factory.makeSnap()
+ build = self.factory.makeSnapBuild(snap=snap)
+ self.assertIsNone(build.log)
+ summary = snap.getBuildSummariesForSnapBuildIds([build.id])
+ self.assertIsNone(summary[build.id]["build_log_size"])
+ removeSecurityProxy(build).log = self.factory.makeLibraryFileAlias(
+ content='x' * 12345, db_only=True)
+ summary = snap.getBuildSummariesForSnapBuildIds([build.id])
+ self.assertEqual(12345, summary[build.id]["build_log_size"])
+
+ def test_getBuildSummariesForSnapBuildIds_query_count(self):
+ # DB query count should remain constant regardless of number of builds.
+ snap = self.factory.makeSnap()
+ recorder1, recorder2 = record_two_runs(
+ lambda: snap.getBuildSummariesForSnapBuildIds(
+ build.id for build in snap.builds),
+ lambda: self.factory.makeSnapBuild(snap=snap),
+ 1, 5)
+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
+
class TestSnapDeleteWithBuilds(TestCaseWithFactory):
=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py 2016-05-13 17:51:34 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py 2016-05-14 00:27:07 +0000
@@ -336,6 +336,23 @@
self.build.id),
self.build.log_url)
+ def test_eta(self):
+ # SnapBuild.eta returns a non-None value when it should, or None
+ # when there's no start time.
+ self.build.queueBuild()
+ self.assertIsNone(self.build.eta)
+ self.factory.makeBuilder(processors=[self.build.processor])
+ self.assertIsNotNone(self.build.eta)
+
+ def test_estimate(self):
+ # SnapBuild.estimate returns True until the job is completed.
+ self.build.queueBuild()
+ self.factory.makeBuilder(processors=[self.build.processor])
+ self.build.updateStatus(BuildStatus.BUILDING)
+ self.assertTrue(self.build.estimate)
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.assertFalse(self.build.estimate)
+
class TestSnapBuildSet(TestCaseWithFactory):
Follow ups