Merge lp:~cjwatson/launchpad/snap-request-builds-ui into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18780
Proposed branch: lp:~cjwatson/launchpad/snap-request-builds-ui
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/snap-daily-builds-request-builds
Diff against target: 1746 lines (+1034/-232)
14 files modified
lib/lp/app/browser/configure.zcml (+8/-1)
lib/lp/app/browser/tales.py (+35/-1)
lib/lp/snappy/browser/snap.py (+20/-17)
lib/lp/snappy/browser/tests/test_snap.py (+62/-23)
lib/lp/snappy/interfaces/snap.py (+36/-3)
lib/lp/snappy/interfaces/snapjob.py (+21/-0)
lib/lp/snappy/javascript/snap.update_build_statuses.js (+131/-38)
lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.html (+20/-14)
lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.js (+342/-126)
lib/lp/snappy/model/snap.py (+100/-3)
lib/lp/snappy/model/snapjob.py (+62/-4)
lib/lp/snappy/templates/snap-index.pt (+12/-0)
lib/lp/snappy/tests/test_snap.py (+172/-2)
lib/lp/testing/factory.py (+13/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-request-builds-ui
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+348299@code.launchpad.net

Commit message

Convert the snap web UI to use Snap.requestBuilds.

Description of the change

The hard bit here was sorting out the JavaScript that auto-refreshes bits of Snap:+index. I went for converting Snap.getBuildSummariesForSnapBuildIds into a more general Snap.getBuildSummaries that handles both build requests and builds, making it possible to extend the existing updater.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml 2017-09-01 12:57:34 +0000
+++ lib/lp/app/browser/configure.zcml 2018-09-13 15:15:44 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009-2015 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -591,6 +591,13 @@
591 name="image"591 name="image"
592 />592 />
593593
594 <adapter
595 for="lp.snappy.interfaces.snap.ISnapBuildRequest"
596 provides="zope.traversing.interfaces.IPathAdapter"
597 factory="lp.app.browser.tales.SnapBuildRequestImageDisplayAPI"
598 name="image"
599 />
600
594 <!-- TALES badges: namespace -->601 <!-- TALES badges: namespace -->
595602
596 <adapter603 <adapter
597604
=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py 2017-11-10 11:23:27 +0000
+++ lib/lp/app/browser/tales.py 2018-09-13 15:15:44 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2017 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Implementation of the lp: htmlform: fmt: namespaces in TALES."""4"""Implementation of the lp: htmlform: fmt: namespaces in TALES."""
@@ -95,6 +95,7 @@
95 )95 )
96from lp.services.webapp.session import get_cookie_domain96from lp.services.webapp.session import get_cookie_domain
97from lp.services.webapp.url import urlappend97from lp.services.webapp.url import urlappend
98from lp.snappy.interfaces.snap import SnapBuildRequestStatus
98from lp.soyuz.enums import ArchivePurpose99from lp.soyuz.enums import ArchivePurpose
99from lp.soyuz.interfaces.archive import (100from lp.soyuz.interfaces.archive import (
100 IArchive,101 IArchive,
@@ -1164,6 +1165,39 @@
1164 return self.icon_template % (alt, title, source)1165 return self.icon_template % (alt, title, source)
11651166
11661167
1168class SnapBuildRequestImageDisplayAPI(ObjectImageDisplayAPI):
1169 """Adapter for ISnapBuildRequest objects to an image.
1170
1171 Used for image:icon.
1172 """
1173 icon_template = (
1174 '<img width="%(width)s" height="14" alt="%(alt)s" '
1175 'title="%(title)s" src="%(src)s" />')
1176
1177 def icon(self):
1178 """Return the appropriate <img> tag for the build request icon."""
1179 icon_map = {
1180 SnapBuildRequestStatus.PENDING: {'src': "/@@/processing"},
1181 SnapBuildRequestStatus.FAILED: {
1182 'src': "/@@/build-failed",
1183 'width': "16",
1184 },
1185 SnapBuildRequestStatus.COMPLETED: {'src': "/@@/build-success"},
1186 }
1187
1188 alt = '[%s]' % self._context.status.name
1189 title = self._context.status.title
1190 source = icon_map[self._context.status].get('src')
1191 width = icon_map[self._context.status].get('width', '14')
1192
1193 return self.icon_template % {
1194 'alt': alt,
1195 'title': title,
1196 'src': source,
1197 'width': width,
1198 }
1199
1200
1167class BadgeDisplayAPI:1201class BadgeDisplayAPI:
1168 """Adapter for IHasBadges to the images for the badges.1202 """Adapter for IHasBadges to the images for the badges.
11691203
11701204
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2018-09-13 09:36:38 +0000
+++ lib/lp/snappy/browser/snap.py 2018-09-13 15:15:44 +0000
@@ -257,11 +257,16 @@
257 archive = Reference(IArchive, title=u'Source archive', required=True)257 archive = Reference(IArchive, title=u'Source archive', required=True)
258 distro_arch_series = List(258 distro_arch_series = List(
259 Choice(vocabulary='SnapDistroArchSeries'),259 Choice(vocabulary='SnapDistroArchSeries'),
260 title=u'Architectures', required=True)260 title=u'Architectures', required=True,
261 description=(
262 u'If you do not explicitly select any architectures, then '
263 u'the snap package will be built for all architectures '
264 u'allowed by its configuration.'))
261 pocket = Choice(265 pocket = Choice(
262 title=u'Pocket', vocabulary=PackagePublishingPocket, required=True,266 title=u'Pocket', vocabulary=PackagePublishingPocket, required=True,
263 description=u'The package stream within the source distribution '267 description=(
264 'series to use when building the snap package.')268 u'The package stream within the source distribution series '
269 u'to use when building the snap package.'))
265270
266 custom_widget_archive = SnapArchiveWidget271 custom_widget_archive = SnapArchiveWidget
267 custom_widget_distro_arch_series = LabeledMultiCheckBoxWidget272 custom_widget_distro_arch_series = LabeledMultiCheckBoxWidget
@@ -280,18 +285,10 @@
280 """See `LaunchpadFormView`."""285 """See `LaunchpadFormView`."""
281 return {286 return {
282 'archive': self.context.distro_series.main_archive,287 'archive': self.context.distro_series.main_archive,
283 'distro_arch_series': self.context.getAllowedArchitectures(),288 'distro_arch_series': [],
284 'pocket': PackagePublishingPocket.UPDATES,289 'pocket': PackagePublishingPocket.UPDATES,
285 }290 }
286291
287 def validate(self, data):
288 """See `LaunchpadFormView`."""
289 arches = data.get('distro_arch_series', [])
290 if not arches:
291 self.setFieldError(
292 'distro_arch_series',
293 "You need to select at least one architecture.")
294
295 def requestBuild(self, data):292 def requestBuild(self, data):
296 """User action for requesting a number of builds.293 """User action for requesting a number of builds.
297294
@@ -318,12 +315,18 @@
318315
319 @action('Request builds', name='request')316 @action('Request builds', name='request')
320 def request_action(self, action, data):317 def request_action(self, action, data):
321 builds, informational = self.requestBuild(data)318 if data['distro_arch_series']:
319 builds, informational = self.requestBuild(data)
320 already_pending = informational.get('already_pending')
321 notification_text = new_builds_notification_text(
322 builds, already_pending)
323 self.request.response.addNotification(notification_text)
324 else:
325 self.context.requestBuilds(
326 self.user, data['archive'], data['pocket'])
327 self.request.response.addNotification(
328 _('Builds will be dispatched soon.'))
322 self.next_url = self.cancel_url329 self.next_url = self.cancel_url
323 already_pending = informational.get('already_pending')
324 notification_text = new_builds_notification_text(
325 builds, already_pending)
326 self.request.response.addNotification(notification_text)
327330
328331
329class ISnapEditSchema(Interface):332class ISnapEditSchema(Interface):
330333
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2018-07-13 16:42:08 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2018-09-13 15:15:44 +0000
@@ -26,6 +26,9 @@
26import responses26import responses
27import soupmatchers27import soupmatchers
28from testtools.matchers import (28from testtools.matchers import (
29 AfterPreprocessing,
30 Equals,
31 Is,
29 MatchesSetwise,32 MatchesSetwise,
30 MatchesStructure,33 MatchesStructure,
31 )34 )
@@ -33,6 +36,7 @@
33from zope.component import getUtility36from zope.component import getUtility
34from zope.publisher.interfaces import NotFound37from zope.publisher.interfaces import NotFound
35from zope.security.interfaces import Unauthorized38from zope.security.interfaces import Unauthorized
39from zope.security.proxy import removeSecurityProxy
3640
37from lp.app.enums import InformationType41from lp.app.enums import InformationType
38from lp.app.interfaces.launchpad import ILaunchpadCelebrities42from lp.app.interfaces.launchpad import ILaunchpadCelebrities
@@ -64,6 +68,7 @@
64 ISnapSet,68 ISnapSet,
65 SNAP_PRIVATE_FEATURE_FLAG,69 SNAP_PRIVATE_FEATURE_FLAG,
66 SNAP_TESTING_FLAGS,70 SNAP_TESTING_FLAGS,
71 SnapBuildRequestStatus,
67 SnapPrivateFeatureDisabled,72 SnapPrivateFeatureDisabled,
68 )73 )
69from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet74from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet
@@ -1443,6 +1448,9 @@
1443 Architectures:1448 Architectures:
1444 amd641449 amd64
1445 i3861450 i386
1451 If you do not explicitly select any architectures, then the snap
1452 package will be built for all architectures allowed by its
1453 configuration.
1446 Pocket:1454 Pocket:
1447 Release1455 Release
1448 Security1456 Security
@@ -1462,12 +1470,13 @@
1462 self.assertRaises(1470 self.assertRaises(
1463 Unauthorized, self.getViewBrowser, self.snap, "+request-builds")1471 Unauthorized, self.getViewBrowser, self.snap, "+request-builds")
14641472
1465 def test_request_builds_action(self):1473 def test_request_builds_with_architectures_action(self):
1466 # Requesting a build creates pending builds.1474 # Requesting a build with architectures selected creates pending
1475 # builds.
1467 browser = self.getViewBrowser(1476 browser = self.getViewBrowser(
1468 self.snap, "+request-builds", user=self.person)1477 self.snap, "+request-builds", user=self.person)
1469 self.assertTrue(browser.getControl("amd64").selected)1478 browser.getControl("amd64").selected = True
1470 self.assertTrue(browser.getControl("i386").selected)1479 browser.getControl("i386").selected = True
1471 browser.getControl("Request builds").click()1480 browser.getControl("Request builds").click()
14721481
1473 login_person(self.person)1482 login_person(self.person)
@@ -1483,44 +1492,74 @@
1483 self.assertContentEqual(1492 self.assertContentEqual(
1484 [2510], set(build.buildqueue_record.lastscore for build in builds))1493 [2510], set(build.buildqueue_record.lastscore for build in builds))
14851494
1486 def test_request_builds_ppa(self):1495 def test_request_builds_with_architectures_ppa(self):
1487 # Selecting a different archive creates builds in that archive.1496 # Selecting a different archive with architectures selected creates
1497 # builds in that archive.
1488 ppa = self.factory.makeArchive(1498 ppa = self.factory.makeArchive(
1489 distribution=self.ubuntu, owner=self.person, name="snap-ppa")1499 distribution=self.ubuntu, owner=self.person, name="snap-ppa")
1490 browser = self.getViewBrowser(1500 browser = self.getViewBrowser(
1491 self.snap, "+request-builds", user=self.person)1501 self.snap, "+request-builds", user=self.person)
1492 browser.getControl("PPA").click()1502 browser.getControl("PPA").click()
1493 browser.getControl(name="field.archive.ppa").value = ppa.reference1503 browser.getControl(name="field.archive.ppa").value = ppa.reference
1494 self.assertTrue(browser.getControl("amd64").selected)1504 browser.getControl("amd64").selected = True
1495 browser.getControl("i386").selected = False1505 self.assertFalse(browser.getControl("i386").selected)
1496 browser.getControl("Request builds").click()1506 browser.getControl("Request builds").click()
14971507
1498 login_person(self.person)1508 login_person(self.person)
1499 builds = self.snap.pending_builds1509 builds = self.snap.pending_builds
1500 self.assertEqual([ppa], [build.archive for build in builds])1510 self.assertEqual([ppa], [build.archive for build in builds])
15011511
1502 def test_request_builds_no_architectures(self):1512 def test_request_builds_with_architectures_rejects_duplicate(self):
1503 # Selecting no architectures causes a validation failure.1513 # A duplicate build request with architectures selected causes a
1504 browser = self.getViewBrowser(1514 # notification.
1505 self.snap, "+request-builds", user=self.person)
1506 browser.getControl("amd64").selected = False
1507 browser.getControl("i386").selected = False
1508 browser.getControl("Request builds").click()
1509 self.assertIn(
1510 "You need to select at least one architecture.",
1511 extract_text(find_main_content(browser.contents)))
1512
1513 def test_request_builds_rejects_duplicate(self):
1514 # A duplicate build request causes a notification.
1515 self.snap.requestBuild(1515 self.snap.requestBuild(
1516 self.person, self.ubuntu.main_archive, self.distroseries["amd64"],1516 self.person, self.ubuntu.main_archive, self.distroseries["amd64"],
1517 PackagePublishingPocket.UPDATES)1517 PackagePublishingPocket.UPDATES)
1518 browser = self.getViewBrowser(1518 browser = self.getViewBrowser(
1519 self.snap, "+request-builds", user=self.person)1519 self.snap, "+request-builds", user=self.person)
1520 self.assertTrue(browser.getControl("amd64").selected)1520 browser.getControl("amd64").selected = True
1521 self.assertTrue(browser.getControl("i386").selected)1521 browser.getControl("i386").selected = True
1522 browser.getControl("Request builds").click()1522 browser.getControl("Request builds").click()
1523 main_text = extract_text(find_main_content(browser.contents))1523 main_text = extract_text(find_main_content(browser.contents))
1524 self.assertIn("1 new build has been queued.", main_text)1524 self.assertIn("1 new build has been queued.", main_text)
1525 self.assertIn(1525 self.assertIn(
1526 "An identical build is already pending for amd64.", main_text)1526 "An identical build is already pending for amd64.", main_text)
1527
1528 def test_request_builds_no_architectures_action(self):
1529 # Requesting a build with no architectures selected creates a
1530 # pending build request.
1531 browser = self.getViewBrowser(
1532 self.snap, "+request-builds", user=self.person)
1533 self.assertFalse(browser.getControl("amd64").selected)
1534 self.assertFalse(browser.getControl("i386").selected)
1535 browser.getControl("Request builds").click()
1536
1537 login_person(self.person)
1538 [request] = self.snap.pending_build_requests
1539 self.assertThat(removeSecurityProxy(request), MatchesStructure(
1540 snap=Equals(self.snap),
1541 status=Equals(SnapBuildRequestStatus.PENDING),
1542 error_message=Is(None),
1543 builds=AfterPreprocessing(list, Equals([])),
1544 archive=Equals(self.ubuntu.main_archive),
1545 _job=MatchesStructure(
1546 requester=Equals(self.person),
1547 pocket=Equals(PackagePublishingPocket.UPDATES),
1548 channels=Is(None))))
1549
1550 def test_request_builds_no_architectures_ppa(self):
1551 # Selecting a different archive with no architectures selected
1552 # creates a build request targeting that archive.
1553 ppa = self.factory.makeArchive(
1554 distribution=self.ubuntu, owner=self.person, name="snap-ppa")
1555 browser = self.getViewBrowser(
1556 self.snap, "+request-builds", user=self.person)
1557 browser.getControl("PPA").click()
1558 browser.getControl(name="field.archive.ppa").value = ppa.reference
1559 self.assertFalse(browser.getControl("amd64").selected)
1560 self.assertFalse(browser.getControl("i386").selected)
1561 browser.getControl("Request builds").click()
1562
1563 login_person(self.person)
1564 [request] = self.snap.pending_build_requests
1565 self.assertEqual(ppa, request.archive)
15271566
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2018-09-10 11:18:42 +0000
+++ lib/lp/snappy/interfaces/snap.py 2018-09-13 15:15:44 +0000
@@ -317,6 +317,11 @@
317 value_type=Reference(schema=Interface),317 value_type=Reference(schema=Interface),
318 required=True, readonly=True))318 required=True, readonly=True))
319319
320 archive = Reference(
321 IArchive,
322 title=u"The source archive for builds produced by this request",
323 required=True, readonly=True)
324
320325
321class ISnapView(Interface):326class ISnapView(Interface):
322 """`ISnap` attributes that require launchpad.View permission."""327 """`ISnap` attributes that require launchpad.View permission."""
@@ -449,21 +454,49 @@
449 :return: `ISnapBuildRequest`.454 :return: `ISnapBuildRequest`.
450 """455 """
451456
457 pending_build_requests = exported(doNotSnapshot(CollectionField(
458 title=_("Pending build requests for this snap package."),
459 value_type=Reference(ISnapBuildRequest),
460 required=True, readonly=True)))
461
462 # XXX cjwatson 2018-06-20: Deprecated as an exported method; can become
463 # an internal helper method once production JavaScript no longer uses
464 # it.
452 @operation_parameters(465 @operation_parameters(
453 snap_build_ids=List(466 snap_build_ids=List(
454 title=_("A list of snap build ids."),467 title=_("A list of snap build IDs."), value_type=Int()))
455 value_type=Int()))
456 @export_read_operation()468 @export_read_operation()
457 @operation_for_version("devel")469 @operation_for_version("devel")
458 def getBuildSummariesForSnapBuildIds(snap_build_ids):470 def getBuildSummariesForSnapBuildIds(snap_build_ids):
459 """Return a dictionary containing a summary of the build statuses.471 """Return a dictionary containing a summary of the build statuses.
460472
461 :param snap_build_ids: A list of snap build ids.473 :param snap_build_ids: A list of snap build IDs.
462 :type source_ids: ``list``474 :type source_ids: ``list``
463 :return: A dict consisting of the overall status summaries for the475 :return: A dict consisting of the overall status summaries for the
464 given snap builds.476 given snap builds.
465 """477 """
466478
479 @call_with(user=REQUEST_USER)
480 @operation_parameters(
481 request_ids=List(
482 title=_("A list of snap build request IDs."), value_type=Int(),
483 required=False),
484 build_ids=List(
485 title=_("A list of snap build IDs."), value_type=Int(),
486 required=False))
487 @export_read_operation()
488 @operation_for_version("devel")
489 def getBuildSummaries(request_ids=None, build_ids=None, user=None):
490 """Return a dictionary containing a summary of build information.
491
492 :param request_ids: A list of snap build request IDs.
493 :param build_ids: A list of snap build IDs.
494 :param user: The `IPerson` requesting this information.
495 :return: A dict of {"requests", "builds"}, consisting of the overall
496 status summaries for the given snap build requests and snap
497 builds respectively.
498 """
499
467 builds = exported(doNotSnapshot(CollectionField(500 builds = exported(doNotSnapshot(CollectionField(
468 title=_("All builds of this snap package."),501 title=_("All builds of this snap package."),
469 description=_(502 description=_(
470503
=== modified file 'lib/lp/snappy/interfaces/snapjob.py'
--- lib/lp/snappy/interfaces/snapjob.py 2018-09-10 11:18:42 +0000
+++ lib/lp/snappy/interfaces/snapjob.py 2018-09-13 15:15:44 +0000
@@ -112,6 +112,16 @@
112 for these builds.112 for these builds.
113 """113 """
114114
115 def findBySnap(snap, statuses=None, job_ids=None):
116 """Find jobs for a snap.
117
118 :param snap: A snap package to search for.
119 :param statuses: An optional iterable of `JobStatus`es to search for.
120 :param job_ids: An optional iterable of job IDs to search for.
121 :return: A sequence of `SnapRequestBuildsJob`s with the specified
122 snap.
123 """
124
115 def getBySnapAndID(snap, job_id):125 def getBySnapAndID(snap, job_id):
116 """Get a job by snap and job ID.126 """Get a job by snap and job ID.
117127
@@ -119,3 +129,14 @@
119 :raises: `NotFoundError` if there is no job with the specified snap129 :raises: `NotFoundError` if there is no job with the specified snap
120 and ID, or its `job_type` is not `SnapJobType.REQUEST_BUILDS`.130 and ID, or its `job_type` is not `SnapJobType.REQUEST_BUILDS`.
121 """131 """
132
133 def findBuildsForJobs(jobs, user=None):
134 """Find builds resulting from an iterable of `SnapRequestBuildJob`s.
135
136 :param jobs: An iterable of `SnapRequestBuildJob`s to search for.
137 :param user: If passed, check that the builds are for archives
138 visible by this user. (No access checks are performed on the
139 snaps or on the builds.)
140 :return: A dictionary mapping `SnapRequestBuildJob` IDs to lists of
141 their resulting builds.
142 """
122143
=== modified file 'lib/lp/snappy/javascript/snap.update_build_statuses.js'
--- lib/lp/snappy/javascript/snap.update_build_statuses.js 2017-08-31 13:35:55 +0000
+++ lib/lp/snappy/javascript/snap.update_build_statuses.js 2018-09-13 15:15:44 +0000
@@ -1,4 +1,4 @@
1/* Copyright 2016 Canonical Ltd. This software is licensed under the1/* Copyright 2016-2018 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *3 *
4 * The lp.snappy.snap.update_build_statuses module uses the4 * The lp.snappy.snap.update_build_statuses module uses the
@@ -15,11 +15,108 @@
15 module.pending_states = [15 module.pending_states = [
16 "NEEDSBUILD", "BUILDING", "UPLOADING", "CANCELLING"];16 "NEEDSBUILD", "BUILDING", "UPLOADING", "CANCELLING"];
1717
18 module.update_date_built = function(node, build_summary) {
19 node.set("text", build_summary.when_complete);
20 if (build_summary.when_complete_estimate) {
21 node.appendChild(document.createTextNode(' (estimated)'));
22 }
23 if (build_summary.build_log_url !== null) {
24 var new_link = Y.Node.create(
25 '<a class="sprite download">buildlog</a>');
26 new_link.setAttribute('href', build_summary.build_log_url);
27 node.appendChild(document.createTextNode(' '));
28 node.appendChild(new_link);
29 if (build_summary.build_log_size !== null) {
30 node.appendChild(document.createTextNode(' '));
31 node.append("(" + build_summary.build_log_size + " bytes)");
32 }
33 }
34 };
35
18 module.domUpdate = function(table, data_object) {36 module.domUpdate = function(table, data_object) {
19 Y.each(data_object, function(build_summary, build_id) {37 var tbody = table.one('tbody');
38 if (tbody === null) {
39 return;
40 }
41 var tbody_changed = false;
42
43 Y.each(data_object['requests'], function(request_summary, request_id) {
44 var tr_elem = tbody.one('tr#request-' + request_id);
45 if (tr_elem === null) {
46 return;
47 }
48
49 if (request_summary['status'] === 'FAILED') {
50 // XXX cjwatson 2018-06-18: Maybe we should show the error
51 // message in this case, but we don't show non-pending
52 // requests in the non-JS case, so it's not clear where
53 // would be appropriate.
54 tr_elem.remove();
55 tbody_changed = true;
56 return;
57 } else if (request_summary['status'] === 'COMPLETED') {
58 // Insert rows for the new builds.
59 Y.Array.each(request_summary['builds'],
60 function(build_summary) {
61 // Construct the new row.
62 var new_row = Y.Node.create(
63 '<tr>' +
64 '<td class="build_status"><img/><a/></td>' +
65 '<td class="datebuilt"/>' +
66 '<td><a class="sprite distribution"/></td>' +
67 '<td><span class="archive-placeholder"/></td>' +
68 '</tr>');
69 new_row.set('id', 'build-' + build_summary.id);
70 new_row.one('td.build_status a')
71 .set('href', build_summary.self_link);
72 Y.lp.buildmaster.buildstatus.update_build_status(
73 new_row.one('td.build_status'), build_summary);
74 if (build_summary.when_complete !== null) {
75 module.update_date_built(
76 new_row.one('td.datebuilt'), build_summary);
77 }
78 new_row.one('td a.distribution')
79 .set('href', build_summary.distro_arch_series_link)
80 .set('text', build_summary.architecture_tag);
81 new_row.one('td .archive-placeholder')
82 .replace(build_summary.archive_link);
83
84 // Insert the new row, maintaining descending-ID sorted
85 // order.
86 var tr_next = null;
87 tbody.get('children').some(function(tr) {
88 var tr_id = tr.get('id');
89 if (tr_id !== null &&
90 tr_id.substr(0, 6) === 'build-') {
91 var build_id = parseInt(
92 tr_id.replace('build-', ''), 10);
93 if (!isNaN(build_id) &&
94 build_id < build_summary.id) {
95 tr_next = tr;
96 return true;
97 }
98 }
99 return false;
100 });
101 tbody.insert(new_row, tr_next);
102 });
103
104 // Remove the completed build request row.
105 tr_elem.remove();
106 tbody_changed = true;
107 return;
108 }
109 });
110
111 if (tbody_changed) {
112 var anim = Y.lp.anim.green_flash({node: tbody});
113 anim.run();
114 }
115
116 Y.each(data_object['builds'], function(build_summary, build_id) {
20 var ui_changed = false;117 var ui_changed = false;
21118
22 var tr_elem = Y.one("tr#build-" + build_id);119 var tr_elem = tbody.one("tr#build-" + build_id);
23 if (tr_elem === null) {120 if (tr_elem === null) {
24 return;121 return;
25 }122 }
@@ -38,25 +135,7 @@
38135
39 if (build_summary.when_complete !== null) {136 if (build_summary.when_complete !== null) {
40 ui_changed = true;137 ui_changed = true;
41 td_datebuilt.set("innerHTML", build_summary.when_complete);138 module.update_date_built(td_datebuilt, build_summary);
42 if (build_summary.when_complete_estimate) {
43 td_datebuilt.appendChild(
44 document.createTextNode(' (estimated)'));
45 }
46 if (build_summary.build_log_url !== null) {
47 var new_link = Y.Node.create(
48 '<a class="sprite download">buildlog</a>');
49 new_link.setAttribute(
50 'href', build_summary.build_log_url);
51 td_datebuilt.appendChild(document.createTextNode(' '));
52 td_datebuilt.appendChild(new_link);
53 if (build_summary.build_log_size !== null) {
54 td_datebuilt.appendChild(
55 document.createTextNode(' '));
56 td_datebuilt.append(
57 "(" + build_summary.build_log_size + " bytes)");
58 }
59 }
60 }139 }
61140
62 if (ui_changed) {141 if (ui_changed) {
@@ -67,32 +146,46 @@
67 };146 };
68147
69 module.parameterEvaluator = function(table_node) {148 module.parameterEvaluator = function(table_node) {
70 var td_list = table_node.all('td.build_status');149 var td_request_list = table_node.all('td.request_status');
71 var pending = td_list.filter("." + module.pending_states.join(",."));150 var pending_requests = td_request_list.filter('.PENDING');
72 if (pending.size() === 0) {151 var td_build_list = table_node.all('td.build_status');
152 var pending_builds = td_build_list.filter(
153 "." + module.pending_states.join(",."));
154 if (pending_requests.size() === 0 && pending_builds.size() === 0) {
73 return null;155 return null;
74 }156 }
75157
76 var snap_build_ids = [];158 var request_ids = [];
77 Y.each(pending, function(node) {159 Y.each(pending_requests, function(node) {
78 var elem_id = node.ancestor().get('id');160 var elem_id = node.ancestor().get('id');
79 var snap_build_id = elem_id.replace('build-', '');161 var request_id = elem_id.replace('request-', '');
80 snap_build_ids.push(snap_build_id);162 request_ids.push(request_id);
81 });163 });
82164
83 return {snap_build_ids: snap_build_ids};165 var build_ids = [];
166 Y.each(pending_builds, function(node) {
167 var elem_id = node.ancestor().get('id');
168 var build_id = elem_id.replace('build-', '');
169 build_ids.push(build_id);
170 });
171
172 return {request_ids: request_ids, build_ids: build_ids};
84 };173 };
85174
86 module.stopUpdatesCheck = function(table_node) {175 module.stopUpdatesCheck = function(table_node) {
87 // Stop updating when there aren't any builds to update176 // Stop updating when there aren't any build requests or builds to
88 var td_list = table_node.all('td.build_status');177 // update.
89 var pending = td_list.filter("." + module.pending_states.join(",."));178 var td_request_list = table_node.all('td.request_status');
90 return (pending.size() === 0);179 var pending_requests = td_request_list.filter('.PENDING');
180 var td_build_list = table_node.all('td.build_status');
181 var pending_builds = td_build_list.filter(
182 "." + module.pending_states.join(",."));
183 return pending_requests.size() === 0 && pending_builds.size() === 0;
91 };184 };
92185
93 module.config = {186 module.config = {
94 uri: null,187 uri: null,
95 api_method_name: 'getBuildSummariesForSnapBuildIds',188 api_method_name: 'getBuildSummaries',
96 lp_client: null,189 lp_client: null,
97 domUpdateFunction: module.domUpdate,190 domUpdateFunction: module.domUpdate,
98 parameterEvaluatorFunction: module.parameterEvaluator,191 parameterEvaluatorFunction: module.parameterEvaluator,
99192
=== modified file 'lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.html'
--- lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.html 2017-08-31 13:35:55 +0000
+++ lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.html 2018-09-13 15:15:44 +0000
@@ -1,6 +1,6 @@
1<!DOCTYPE html>1<!DOCTYPE html>
2<!--2<!--
3Copyright 2016 Canonical Ltd. This software is licensed under the3Copyright 2016-2018 Canonical Ltd. This software is licensed under the
4GNU Affero General Public License version 3 (see the file LICENSE).4GNU Affero General Public License version 3 (see the file LICENSE).
5-->5-->
66
@@ -62,19 +62,10 @@
62 </tr>62 </tr>
63 </thead>63 </thead>
64 <tbody>64 <tbody>
65 <tr id="build-1">65 <tr id="request-1">
66 <td class="build_status NEEDSBUILD">66 <td class="request_status PENDING">
67 <img width="14" height="14" alt="[NEEDSBUILD]" title="Needs building" src="/@@/build-needed" />67 <img width="14" height="14" alt="[PENDING]" title="Pending" src="/@@/build-needed" />
68 <a href="snap/+build/1">Needs building</a>68 Pending build request
69 </td>
70 <td class="datebuilt">
71 in 1 minute (estimated)
72 </td>
73 <td>
74 <a class="sprite distribution" href="/ubuntu/hoary/i386">i386</a>
75 </td>
76 <td>
77 <a href="/ubuntu" class="sprite distribution">Primary Archive for Ubuntu Linux</a>
78 </td>69 </td>
79 </tr>70 </tr>
80 <tr id="build-2">71 <tr id="build-2">
@@ -94,6 +85,21 @@
94 <a href="/ubuntu" class="sprite distribution">Primary Archive for Ubuntu Linux</a>85 <a href="/ubuntu" class="sprite distribution">Primary Archive for Ubuntu Linux</a>
95 </td>86 </td>
96 </tr>87 </tr>
88 <tr id="build-1">
89 <td class="build_status NEEDSBUILD">
90 <img width="14" height="14" alt="[NEEDSBUILD]" title="Needs building" src="/@@/build-needed" />
91 <a href="snap/+build/1">Needs building</a>
92 </td>
93 <td class="datebuilt">
94 in 1 minute (estimated)
95 </td>
96 <td>
97 <a class="sprite distribution" href="/ubuntu/hoary/i386">i386</a>
98 </td>
99 <td>
100 <a href="/ubuntu" class="sprite distribution">Primary Archive for Ubuntu Linux</a>
101 </td>
102 </tr>
97 </tbody>103 </tbody>
98 </table>104 </table>
99105
100106
=== modified file 'lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.js'
--- lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.js 2017-08-31 13:35:55 +0000
+++ lib/lp/snappy/javascript/tests/test_snap.update_build_statuses.js 2018-09-13 15:15:44 +0000
@@ -1,4 +1,4 @@
1/* Copyright 2016 Canonical Ltd. This software is licensed under the1/* Copyright 2016-2018 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE). */2 * GNU Affero General Public License version 3 (see the file LICENSE). */
33
4YUI.add('lp.snappy.snap.update_build_statuses.test', function (Y) {4YUI.add('lp.snappy.snap.update_build_statuses.test', function (Y) {
@@ -10,13 +10,42 @@
10 name: 'lp.snappy.snap.update_build_statuses_tests',10 name: 'lp.snappy.snap.update_build_statuses_tests',
1111
12 setUp: function () {12 setUp: function () {
13 this.table = Y.one('table#latest-builds-listing');13 // Clone the table from the test data so that we can reliably
14 this.tr_build_1 = Y.one('tr#build-1');14 // restore it.
15 this.table = Y.one('table#latest-builds-listing').cloneNode(true);
16 this.tbody = this.table.one('tbody');
17 this.tr_request_1 = this.tbody.one('tr#request-1');
18 this.tr_build_1 = this.tbody.one('tr#build-1');
15 this.td_status = this.tr_build_1.one('td.build_status');19 this.td_status = this.tr_build_1.one('td.build_status');
16 this.td_datebuilt = this.tr_build_1.one("td.datebuilt");20 this.td_datebuilt = this.tr_build_1.one("td.datebuilt");
17 this.td_status_class = this.td_status.getAttribute("class");21 },
18 this.td_status_img = this.td_status.one("img");22
19 this.td_status_a = this.td_status.one("a");23 assert_node_matches: function(expected, node) {
24 Y.each(expected, function(value, key) {
25 if (key === "tag") {
26 Y.Assert.areEqual(
27 value, node.get("tagName").toLowerCase());
28 } else if (key === "attrs") {
29 Y.each(value, function(attr_value, attr_key) {
30 Y.Assert.areEqual(
31 attr_value, node.getAttribute(attr_key));
32 });
33 } else if (key === "text") {
34 Y.Assert.areEqual(value, node.get("text").trim());
35 } else if (key === "children") {
36 var children = [];
37 node.get("children").each(function(child) {
38 children.push(child);
39 });
40 Y.Array.each(Y.Array.zip(value, children), function(item) {
41 Y.Assert.isObject(item[0]);
42 Y.Assert.isObject(item[1]);
43 this.assert_node_matches(item[0], item[1]);
44 }, this);
45 } else {
46 Y.Assert.fail("unhandled key " + key);
47 }
48 }, this);
20 },49 },
2150
22 test_dom_updater_plugin_attached: function() {51 test_dom_updater_plugin_attached: function() {
@@ -32,167 +61,354 @@
3261
33 test_parameter_evaluator: function() {62 test_parameter_evaluator: function() {
34 // parameterEvaluator should return an object with the ids of63 // parameterEvaluator should return an object with the ids of
35 // builds in pending states.64 // build requests and builds in pending states.
36 var params = module.parameterEvaluator(this.table);65 var params = module.parameterEvaluator(this.table);
37 Y.lp.testing.assert.assert_equal_structure(66 Y.lp.testing.assert.assert_equal_structure(
38 {snap_build_ids: ["1"]}, params);67 {request_ids: ["1"], build_ids: ["1"]}, params);
39 },68 },
4069
41 test_parameter_evaluator_empty: function() {70 test_parameter_evaluator_empty: function() {
42 // parameterEvaluator should return empty if no builds remaining71 // parameterEvaluator should return empty if no builds remaining
43 // in pending states.72 // in pending states.
73 this.tr_request_1.remove();
44 this.td_status.setAttribute("class", "build_status FULLYBUILT");74 this.td_status.setAttribute("class", "build_status FULLYBUILT");
45 var params = module.parameterEvaluator(this.table);75 var params = module.parameterEvaluator(this.table);
46 Y.Assert.isNull(params);76 Y.Assert.isNull(params);
47 // reset td class to the original value
48 this.td_status.setAttribute("class", this.td_status_class);
49 },77 },
5078
51 test_stop_updates_check: function() {79 test_stop_updates_check: function() {
52 // stopUpdatesCheck should return false if pending builds exist.80 // stopUpdatesCheck should return false if pending build
53 Y.Assert.isFalse(module.stopUpdatesCheck(this.table));81 // requests or pending builds exist.
54 // stopUpdatesCheck should return true if no pending builds exist.82 Y.Assert.isFalse(module.stopUpdatesCheck(this.table));
83 this.tr_request_1.one('td.request_status')
84 .setAttribute('class', 'request_status COMPLETED');
85 Y.Assert.isFalse(module.stopUpdatesCheck(this.table));
86 // stopUpdatesCheck should return true if no pending build
87 // requests or pending builds exist.
55 this.td_status.setAttribute("class", "build_status FULLYBUILT");88 this.td_status.setAttribute("class", "build_status FULLYBUILT");
56 Y.Assert.isTrue(module.stopUpdatesCheck(this.table));89 Y.Assert.isTrue(module.stopUpdatesCheck(this.table));
90 this.tr_request_1.remove();
91 Y.Assert.isTrue(module.stopUpdatesCheck(this.table));
57 for (var i = 0; i < module.pending_states.length; i++) {92 for (var i = 0; i < module.pending_states.length; i++) {
58 this.td_status.setAttribute(93 this.td_status.setAttribute(
59 "class", "build_status " + module.pending_states[i]);94 "class", "build_status " + module.pending_states[i]);
60 Y.Assert.isFalse(module.stopUpdatesCheck(this.table));95 Y.Assert.isFalse(module.stopUpdatesCheck(this.table));
61 }96 }
62 // reset td class to the original value97 },
63 this.td_status.setAttribute("class", this.td_status_class);98
64 },99 test_update_build_request_status_dom_completed: function() {
65100 var data = {
66 test_update_build_status_dom_building: function() {101 "requests": {
67 var original_a_href = this.td_status_a.get("href");102 "1": {
68 var data = {103 "status": "COMPLETED",
69 "1": {104 "error_message": null,
70 "status": "BUILDING",105 "builds": [
71 "build_log_url": null,106 {
72 "when_complete_estimate": true,107 "self_link": "/~max/+snap/snap/+build/3",
73 "buildstate": "Currently building",108 "id": 3,
74 "build_log_size": null,109 "distro_arch_series_link":
75 "when_complete": "in 1 minute"110 "/ubuntu/hoary/amd64",
76 }111 "architecture_tag": "amd64",
77 };112 "archive_link":
78 module.domUpdate(this.table, data);113 '<a href="/ubuntu" ' +
79 Y.Assert.areEqual(114 'class="sprite distribution">Primary ' +
80 "build_status BUILDING", this.td_status.getAttribute("class"));115 'Archive for Ubuntu Linux</a>',
81 Y.Assert.areEqual(116 "status": "NEEDSBUILD",
82 "Currently building", this.td_status.get("text").trim());117 "build_log_url": null,
83 Y.Assert.areEqual("[BUILDING]", this.td_status_img.get("alt"));118 "when_complete_estimate": false,
84 Y.Assert.areEqual(119 "buildstate": "Needs building",
85 "Currently building", this.td_status_img.get("title"));120 "build_log_size": null,
86 Y.Assert.areEqual(121 "when_complete": null
87 "file:///@@/processing", this.td_status_img.get("src"));122 },
88 Y.Assert.areEqual("14", this.td_status_img.get("width"));123 {
89 Y.Assert.areEqual(original_a_href, this.td_status_a.get("href"));124 "self_link": "/~max/+snap/snap/+build/4",
90 },125 "id": 4,
91126 "distro_arch_series_link":
92 test_update_build_status_dom_building: function() {127 "/ubuntu/hoary/i386",
93 var original_a_href = this.td_status_a.get("href");128 "architecture_tag": "i386",
94 var data = {129 "archive_link":
95 "1": {130 '<a href="/ubuntu" ' +
96 "status": "BUILDING",131 'class="sprite distribution">Primary ' +
97 "build_log_url": null,132 'Archive for Ubuntu Linux</a>',
98 "when_complete_estimate": true,133 "status": "BUILDING",
99 "buildstate": "Currently building",134 "build_log_url": null,
100 "build_log_size": null,135 "when_complete_estimate": true,
101 "when_complete": "in 1 minute"136 "buildstate": "Currently building",
102 }137 "build_log_size": null,
103 };138 "when_complete": "in 1 minute"
104 module.domUpdate(this.table, data);139 }
105 Y.Assert.areEqual(140 ]
106 "build_status BUILDING", this.td_status.getAttribute("class"));141 }
107 Y.Assert.areEqual(142 },
108 "Currently building", this.td_status.get("text").trim());143 "builds": {}
109 Y.Assert.areEqual("[BUILDING]", this.td_status_img.get("alt"));144 };
110 Y.Assert.areEqual(145 module.domUpdate(this.table, data);
111 "Currently building", this.td_status_img.get("title"));146 Y.ArrayAssert.itemsAreEqual(
112 Y.Assert.areEqual(147 ["build-4", "build-3", "build-2", "build-1"],
113 "file:///@@/processing", this.td_status_img.get("src"));148 this.tbody.get("children").get("id"));
114 Y.Assert.areEqual("14", this.td_status_img.get("width"));149 this.assert_node_matches({
115 Y.Assert.areEqual(original_a_href, this.td_status_a.get("href"));150 "tag": "tr",
151 "attrs": {"id": "build-3"},
152 "children": [
153 {
154 "tag": "td",
155 "attrs": {"class": "build_status NEEDSBUILD"},
156 "children": [
157 {
158 "tag": "img",
159 "attrs": {
160 "alt": "[NEEDSBUILD]",
161 "title": "Needs building",
162 "src": "/@@/build-needed",
163 "width": "14"
164 }
165 },
166 {
167 "tag": "a",
168 "attrs": {"href": "/~max/+snap/snap/+build/3"},
169 "text": "Needs building"
170 }
171 ]
172 },
173 {
174 "tag": "td",
175 "attrs": {"class": "datebuilt"},
176 "text": "",
177 "children": []
178 },
179 {
180 "tag": "td",
181 "children": [{
182 "tag": "a",
183 "attrs": {
184 "class": "sprite distribution",
185 "href": "/ubuntu/hoary/amd64"
186 },
187 "text": "amd64"
188 }]
189 },
190 {
191 "tag": "td",
192 "children": [{
193 "tag": "a",
194 "attrs": {
195 "class": "sprite distribution",
196 "href": "/ubuntu"
197 },
198 "text": "Primary Archive for Ubuntu Linux"
199 }]
200 }
201 ]
202 }, this.tbody.one("tr#build-3"));
203 this.assert_node_matches({
204 "tag": "tr",
205 "attrs": {"id": "build-4"},
206 "children": [
207 {
208 "tag": "td",
209 "attrs": {"class": "build_status BUILDING"},
210 "children": [
211 {
212 "tag": "img",
213 "attrs": {
214 "alt": "[BUILDING]",
215 "title": "Currently building",
216 "src": "/@@/processing",
217 "width": "14"
218 }
219 },
220 {
221 "tag": "a",
222 "attrs": {"href": "/~max/+snap/snap/+build/4"},
223 "text": "Currently building"
224 }
225 ]
226 },
227 {
228 "tag": "td",
229 "attrs": {"class": "datebuilt"},
230 "text": "in 1 minute (estimated)",
231 "children": []
232 },
233 {
234 "tag": "td",
235 "children": [{
236 "tag": "a",
237 "attrs": {
238 "class": "sprite distribution",
239 "href": "/ubuntu/hoary/i386"
240 },
241 "text": "i386"
242 }]
243 },
244 {
245 "tag": "td",
246 "children": [{
247 "tag": "a",
248 "attrs": {
249 "class": "sprite distribution",
250 "href": "/ubuntu"
251 },
252 "text": "Primary Archive for Ubuntu Linux"
253 }]
254 }
255 ]
256 }, this.tbody.one("tr#build-4"));
257 },
258
259 test_update_build_request_status_dom_failed: function() {
260 var data = {
261 "requests": {
262 "1": {
263 "status": "FAILED",
264 "error_message": "Something went wrong",
265 "builds": []
266 }
267 },
268 "builds": {}
269 };
270 module.domUpdate(this.table, data);
271 Y.ArrayAssert.itemsAreEqual(
272 ["build-2", "build-1"], this.tbody.get("children").get("id"));
273 },
274
275 test_update_build_status_dom_building: function() {
276 var original_a_href = this.td_status.one("a").getAttribute("href");
277 var data = {
278 "requests": {},
279 "builds": {
280 "1": {
281 "status": "BUILDING",
282 "build_log_url": null,
283 "when_complete_estimate": true,
284 "buildstate": "Currently building",
285 "build_log_size": null,
286 "when_complete": "in 1 minute"
287 }
288 }
289 };
290 module.domUpdate(this.table, data);
291 this.assert_node_matches({
292 "attrs": {"class": "build_status BUILDING"},
293 "text": "Currently building",
294 "children": [
295 {
296 "tag": "img",
297 "attrs": {
298 "alt": "[BUILDING]",
299 "title": "Currently building",
300 "src": "/@@/processing",
301 "width": "14"
302 }
303 },
304 {
305 "tag": "a",
306 "attrs": {"href": original_a_href}
307 }
308 ]
309 }, this.td_status);
116 },310 },
117311
118 test_update_build_status_dom_failedtobuild: function() {312 test_update_build_status_dom_failedtobuild: function() {
119 var original_a_href = this.td_status_a.get("href");313 var original_a_href = this.td_status.one("a").getAttribute("href");
120 var data = {314 var data = {
121 "1": {315 "requests": {},
122 "status": "FAILEDTOBUILD",316 "builds": {
123 "build_log_url": null,317 "1": {
124 "when_complete_estimate": false,318 "status": "FAILEDTOBUILD",
125 "buildstate": "Failed to build",319 "build_log_url": null,
126 "build_log_size": null,320 "when_complete_estimate": false,
127 "when_complete": "1 minute ago"321 "buildstate": "Failed to build",
322 "build_log_size": null,
323 "when_complete": "1 minute ago"
324 }
128 }325 }
129 };326 };
130 module.domUpdate(this.table, data);327 module.domUpdate(this.table, data);
131 Y.Assert.areEqual(328 this.assert_node_matches({
132 "build_status FAILEDTOBUILD",329 "attrs": {"class": "build_status FAILEDTOBUILD"},
133 this.td_status.getAttribute("class"));330 "text": "Failed to build",
134 Y.Assert.areEqual(331 "children": [
135 "Failed to build", this.td_status.get("text").trim());332 {
136 Y.Assert.areEqual(333 "tag": "img",
137 "[FAILEDTOBUILD]", this.td_status_img.get("alt"));334 "attrs": {
138 Y.Assert.areEqual(335 "alt": "[FAILEDTOBUILD]",
139 "Failed to build", this.td_status_img.get("title"));336 "title": "Failed to build",
140 Y.Assert.areEqual(337 "src": "/@@/build-failed",
141 "file:///@@/build-failed", this.td_status_img.get("src"));338 "width": "16"
142 Y.Assert.areEqual("16", this.td_status_img.get("width"));339 }
143 Y.Assert.areEqual(original_a_href, this.td_status_a.get("href"));340 },
341 {
342 "tag": "a",
343 "attrs": {"href": original_a_href}
344 }
345 ]
346 }, this.td_status);
144 },347 },
145348
146 test_update_build_status_dom_chrootwait: function() {349 test_update_build_status_dom_chrootwait: function() {
147 var original_a_href = this.td_status_a.get("href");350 var original_a_href = this.td_status.one("a").getAttribute("href");
148 var data = {351 var data = {
149 "1": {352 "requests": {},
150 "status": "CHROOTWAIT",353 "builds": {
151 "build_log_url": null,354 "1": {
152 "when_complete_estimate": false,355 "status": "CHROOTWAIT",
153 "buildstate": "Chroot problem",356 "build_log_url": null,
154 "build_log_size": null,357 "when_complete_estimate": false,
155 "when_complete": "1 minute ago"358 "buildstate": "Chroot problem",
359 "build_log_size": null,
360 "when_complete": "1 minute ago"
361 }
156 }362 }
157 };363 };
158 module.domUpdate(this.table, data);364 module.domUpdate(this.table, data);
159 Y.Assert.areEqual(365 this.assert_node_matches({
160 "build_status CHROOTWAIT",366 "attrs": {"class": "build_status CHROOTWAIT"},
161 this.td_status.getAttribute("class"));367 "text": "Chroot problem",
162 Y.Assert.areEqual(368 "children": [
163 "Chroot problem", this.td_status.get("text").trim());369 {
164 Y.Assert.areEqual("[CHROOTWAIT]", this.td_status_img.get("alt"));370 "tag": "img",
165 Y.Assert.areEqual(371 "attrs": {
166 "Chroot problem", this.td_status_img.get("title"));372 "alt": "[CHROOTWAIT]",
167 Y.Assert.areEqual(373 "title": "Chroot problem",
168 "file:///@@/build-chrootwait", this.td_status_img.get("src"));374 "src": "/@@/build-chrootwait",
169 Y.Assert.areEqual("14", this.td_status_img.get("width"));375 "width": "14"
170 Y.Assert.areEqual(original_a_href, this.td_status_a.get("href"));376 }
377 },
378 {
379 "tag": "a",
380 "attrs": {"href": original_a_href}
381 }
382 ]
383 }, this.td_status);
171 },384 },
172385
173 test_update_build_date_dom: function() {386 test_update_build_date_dom: function() {
174 var data = {387 var data = {
175 "1": {388 "requests": {},
176 "status": "NEEDSBUILD",389 "builds": {
177 "build_log_url": "/+build/1/+files/build1.txt.gz",390 "1": {
178 "when_complete_estimate": true,391 "status": "NEEDSBUILD",
179 "buildstate": "Needs building",392 "build_log_url": "/+build/1/+files/build1.txt.gz",
180 "build_log_size": 12345,393 "when_complete_estimate": true,
181 "when_complete": "in 30 seconds"394 "buildstate": "Needs building",
395 "build_log_size": 12345,
396 "when_complete": "in 30 seconds"
397 }
182 }398 }
183 };399 };
184 module.domUpdate(this.table, data);400 module.domUpdate(this.table, data);
185 Y.Assert.areEqual(401 this.assert_node_matches({
186 "in 30 seconds (estimated) buildlog (12345 bytes)",402 "text": "in 30 seconds (estimated) buildlog (12345 bytes)",
187 this.td_datebuilt.get("text").trim());403 "children": [{
188 var td_datebuilt_a = this.td_datebuilt.one("a");404 "tag": "a",
189 Y.Assert.isNotNull(td_datebuilt_a);405 "attrs": {
190 Y.Assert.areEqual("buildlog", td_datebuilt_a.get("text").trim());406 "class": "sprite download",
191 Y.Assert.areEqual(407 "href": data["builds"]["1"].build_log_url
192 "sprite download", td_datebuilt_a.getAttribute("class"));408 },
193 Y.Assert.areEqual(409 "text": "buildlog"
194 "file://" + data["1"].build_log_url,410 }]
195 td_datebuilt_a.get("href"));411 }, this.td_datebuilt);
196 }412 }
197 }));413 }));
198414
199415
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2018-09-10 11:18:42 +0000
+++ lib/lp/snappy/model/snap.py 2018-09-13 15:15:44 +0000
@@ -45,7 +45,10 @@
45from zope.security.interfaces import Unauthorized45from zope.security.interfaces import Unauthorized
46from zope.security.proxy import removeSecurityProxy46from zope.security.proxy import removeSecurityProxy
4747
48from lp.app.browser.tales import DateTimeFormatterAPI48from lp.app.browser.tales import (
49 ArchiveFormatterAPI,
50 DateTimeFormatterAPI,
51 )
49from lp.app.enums import PRIVATE_INFORMATION_TYPES52from lp.app.enums import PRIVATE_INFORMATION_TYPES
50from lp.app.errors import (53from lp.app.errors import (
51 IncompatibleArguments,54 IncompatibleArguments,
@@ -119,7 +122,13 @@
119 LibraryFileContent,122 LibraryFileContent,
120 )123 )
121from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint124from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint
125from lp.services.propertycache import (
126 cachedproperty,
127 get_property_cache,
128 )
129from lp.services.webapp.authorization import precache_permission_for_objects
122from lp.services.webapp.interfaces import ILaunchBag130from lp.services.webapp.interfaces import ILaunchBag
131from lp.services.webapp.publisher import canonical_url
123from lp.services.webhooks.interfaces import IWebhookSet132from lp.services.webhooks.interfaces import IWebhookSet
124from lp.services.webhooks.model import WebhookTargetMixin133from lp.services.webhooks.model import WebhookTargetMixin
125from lp.snappy.adapters.buildarch import determine_architectures_to_build134from lp.snappy.adapters.buildarch import determine_architectures_to_build
@@ -178,11 +187,21 @@
178 """187 """
179188
180 def __init__(self, snap, id):189 def __init__(self, snap, id):
181 self._job = getUtility(ISnapRequestBuildsJobSource).getBySnapAndID(
182 snap, id)
183 self.snap = snap190 self.snap = snap
184 self.id = id191 self.id = id
185192
193 @classmethod
194 def fromJob(cls, job):
195 """See `ISnapBuildRequest`."""
196 request = cls(job.snap, job.job_id)
197 get_property_cache(request)._job = job
198 return request
199
200 @cachedproperty
201 def _job(self):
202 job_source = getUtility(ISnapRequestBuildsJobSource)
203 return job_source.getBySnapAndID(self.snap, self.id)
204
186 @property205 @property
187 def date_requested(self):206 def date_requested(self):
188 """See `ISnapBuildRequest`."""207 """See `ISnapBuildRequest`."""
@@ -215,6 +234,11 @@
215 """See `ISnapBuildRequest`."""234 """See `ISnapBuildRequest`."""
216 return self._job.builds235 return self._job.builds
217236
237 @property
238 def archive(self):
239 """See `ISnapBuildRequest`."""
240 return self._job.archive
241
218242
219@implementer(ISnap, IHasOwner)243@implementer(ISnap, IHasOwner)
220class Snap(Storm, WebhookTargetMixin):244class Snap(Storm, WebhookTargetMixin):
@@ -643,6 +667,15 @@
643 """See `ISnap`."""667 """See `ISnap`."""
644 return SnapBuildRequest(self, job_id)668 return SnapBuildRequest(self, job_id)
645669
670 @property
671 def pending_build_requests(self):
672 """See `ISnap`."""
673 job_source = getUtility(ISnapRequestBuildsJobSource)
674 # The returned jobs are ordered by descending ID.
675 jobs = job_source.findBySnap(
676 self, statuses=(JobStatus.WAITING, JobStatus.RUNNING))
677 return [SnapBuildRequest.fromJob(job) for job in jobs]
678
646 def _getBuilds(self, filter_term, order_by):679 def _getBuilds(self, filter_term, order_by):
647 """The actual query to get the builds."""680 """The actual query to get the builds."""
648 query_args = [681 query_args = [
@@ -673,6 +706,10 @@
673 order_by = Desc(SnapBuild.id)706 order_by = Desc(SnapBuild.id)
674 builds = self._getBuilds(filter_term, order_by)707 builds = self._getBuilds(filter_term, order_by)
675708
709 # The user can obviously see this snap, and Snap._getBuilds ensures
710 # that they can see the relevant archive for each build as well.
711 precache_permission_for_objects(None, "launchpad.View", builds)
712
676 # Prefetch data to keep DB query count constant713 # Prefetch data to keep DB query count constant
677 lfas = load_related(LibraryFileAlias, builds, ["log_id"])714 lfas = load_related(LibraryFileAlias, builds, ["log_id"])
678 load_related(LibraryFileContent, lfas, ["contentID"])715 load_related(LibraryFileContent, lfas, ["contentID"])
@@ -698,6 +735,66 @@
698 }735 }
699 return result736 return result
700737
738 def getBuildSummaries(self, request_ids=None, build_ids=None, user=None):
739 """See `ISnap`."""
740 all_build_ids = []
741 result = {"requests": {}, "builds": {}}
742
743 if request_ids:
744 job_source = getUtility(ISnapRequestBuildsJobSource)
745 jobs = job_source.findBySnap(self, job_ids=request_ids)
746 requests = [SnapBuildRequest.fromJob(job) for job in jobs]
747 builds_by_request = job_source.findBuildsForJobs(jobs, user=user)
748 for builds in builds_by_request.values():
749 # It's safe to remove the proxy here, because the IDs will
750 # go through Snap._getBuilds which checks visibility. This
751 # saves an Archive query per build in the security adapter.
752 all_build_ids.extend(
753 [removeSecurityProxy(build).id for build in builds])
754 else:
755 requests = []
756
757 if build_ids:
758 all_build_ids.extend(build_ids)
759
760 all_build_summaries = self.getBuildSummariesForSnapBuildIds(
761 all_build_ids)
762
763 for request in requests:
764 build_summaries = []
765 for build in sorted(
766 builds_by_request[request.id], key=attrgetter("id"),
767 reverse=True):
768 if build.id in all_build_summaries:
769 # Include enough information for
770 # snap.update_build_statuses.js to populate new build
771 # rows.
772 build_summary = {
773 "self_link": canonical_url(
774 build, path_only_if_possible=True),
775 "id": build.id,
776 "distro_arch_series_link": canonical_url(
777 build.distro_arch_series,
778 path_only_if_possible=True),
779 "architecture_tag": (
780 build.distro_arch_series.architecturetag),
781 "archive_link": ArchiveFormatterAPI(
782 build.archive).link(None),
783 }
784 build_summary.update(all_build_summaries[build.id])
785 build_summaries.append(build_summary)
786 result["requests"][request.id] = {
787 "status": request.status.name,
788 "error_message": request.error_message,
789 "builds": build_summaries,
790 }
791
792 for build_id in (build_ids or []):
793 if build_id in all_build_summaries:
794 result["builds"][build_id] = all_build_summaries[build_id]
795
796 return result
797
701 @property798 @property
702 def builds(self):799 def builds(self):
703 """See `ISnap`."""800 """See `ISnap`."""
704801
=== modified file 'lib/lp/snappy/model/snapjob.py'
--- lib/lp/snappy/model/snapjob.py 2018-09-10 11:18:42 +0000
+++ lib/lp/snappy/model/snapjob.py 2018-09-13 15:15:44 +0000
@@ -12,12 +12,15 @@
12 'SnapRequestBuildsJob',12 'SnapRequestBuildsJob',
13 ]13 ]
1414
15from itertools import chain
16
15from lazr.delegates import delegate_to17from lazr.delegates import delegate_to
16from lazr.enum import (18from lazr.enum import (
17 DBEnumeratedType,19 DBEnumeratedType,
18 DBItem,20 DBItem,
19 )21 )
20from storm.locals import (22from storm.locals import (
23 Desc,
21 Int,24 Int,
22 JSON,25 JSON,
23 Reference,26 Reference,
@@ -29,11 +32,14 @@
29 implementer,32 implementer,
30 provider,33 provider,
31 )34 )
35from zope.security.proxy import removeSecurityProxy
3236
33from lp.app.errors import NotFoundError37from lp.app.errors import NotFoundError
34from lp.registry.interfaces.person import IPersonSet38from lp.registry.interfaces.person import IPersonSet
35from lp.registry.interfaces.pocket import PackagePublishingPocket39from lp.registry.interfaces.pocket import PackagePublishingPocket
36from lp.services.config import config40from lp.services.config import config
41from lp.services.database.bulk import load_related
42from lp.services.database.decoratedresultset import DecoratedResultSet
37from lp.services.database.enumcol import EnumCol43from lp.services.database.enumcol import EnumCol
38from lp.services.database.interfaces import (44from lp.services.database.interfaces import (
39 IMasterStore,45 IMasterStore,
@@ -58,7 +64,10 @@
58 ISnapRequestBuildsJobSource,64 ISnapRequestBuildsJobSource,
59 )65 )
60from lp.snappy.model.snapbuild import SnapBuild66from lp.snappy.model.snapbuild import SnapBuild
61from lp.soyuz.model.archive import Archive67from lp.soyuz.model.archive import (
68 Archive,
69 get_enabled_archive_filter,
70 )
6271
6372
64class SnapJobType(DBEnumeratedType):73class SnapJobType(DBEnumeratedType):
@@ -188,6 +197,30 @@
188 return job197 return job
189198
190 @classmethod199 @classmethod
200 def findBySnap(cls, snap, statuses=None, job_ids=None):
201 """See `ISnapRequestBuildsJobSource`."""
202 clauses = [
203 SnapJob.snap == snap,
204 SnapJob.job_type == cls.class_job_type,
205 ]
206 if statuses is not None:
207 clauses.extend([
208 SnapJob.job == Job.id,
209 Job._status.is_in(statuses),
210 ])
211 if job_ids is not None:
212 clauses.append(SnapJob.job_id.is_in(job_ids))
213 snap_jobs = IStore(SnapJob).find(SnapJob, *clauses).order_by(
214 Desc(SnapJob.job_id))
215
216 def preload_jobs(rows):
217 load_related(Job, rows, ["job_id"])
218
219 return DecoratedResultSet(
220 snap_jobs, lambda snap_job: cls(snap_job),
221 pre_iter_hook=preload_jobs)
222
223 @classmethod
191 def getBySnapAndID(cls, snap, job_id):224 def getBySnapAndID(cls, snap, job_id):
192 """See `ISnapRequestBuildsJobSource`."""225 """See `ISnapRequestBuildsJobSource`."""
193 snap_job = IStore(SnapJob).find(226 snap_job = IStore(SnapJob).find(
@@ -201,6 +234,31 @@
201 (job_id, snap))234 (job_id, snap))
202 return cls(snap_job)235 return cls(snap_job)
203236
237 @classmethod
238 def findBuildsForJobs(cls, jobs, user=None):
239 """See `ISnapRequestBuildsJobSource`."""
240 build_ids = {
241 job.job_id: removeSecurityProxy(job).metadata.get("builds") or []
242 for job in jobs}
243 all_build_ids = set(chain.from_iterable(build_ids.values()))
244 if all_build_ids:
245 all_builds = {
246 build.id: build for build in IStore(SnapBuild).find(
247 SnapBuild,
248 SnapBuild.id.is_in(all_build_ids),
249 SnapBuild.archive_id == Archive.id,
250 Archive._enabled == True,
251 get_enabled_archive_filter(
252 user, include_public=True, include_subscribed=True))
253 }
254 else:
255 all_builds = {}
256 return {
257 job.job_id: [
258 all_builds[build_id] for build_id in build_ids[job.job_id]
259 if build_id in all_builds]
260 for job in jobs}
261
204 def getOperationDescription(self):262 def getOperationDescription(self):
205 return "requesting builds of %s" % self.snap.name263 return "requesting builds of %s" % self.snap.name
206264
@@ -261,11 +319,11 @@
261 def builds(self):319 def builds(self):
262 """See `ISnapRequestBuildsJob`."""320 """See `ISnapRequestBuildsJob`."""
263 build_ids = self.metadata.get("builds")321 build_ids = self.metadata.get("builds")
264 if build_ids is None:322 if build_ids:
265 return EmptyResultSet()
266 else:
267 return IStore(SnapBuild).find(323 return IStore(SnapBuild).find(
268 SnapBuild, SnapBuild.id.is_in(build_ids))324 SnapBuild, SnapBuild.id.is_in(build_ids))
325 else:
326 return EmptyResultSet()
269327
270 @builds.setter328 @builds.setter
271 def builds(self, builds):329 def builds(self, builds):
272330
=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt 2018-04-30 16:48:47 +0000
+++ lib/lp/snappy/templates/snap-index.pt 2018-09-13 15:15:44 +0000
@@ -156,6 +156,18 @@
156 </tr>156 </tr>
157 </thead>157 </thead>
158 <tbody>158 <tbody>
159 <tal:snap-build-requests repeat="request context/pending_build_requests">
160 <tr tal:attributes="id string:request-${request/id}">
161 <td colspan="3"
162 tal:attributes="class string:request_status ${request/status/name}">
163 <span tal:replace="structure request/image:icon"/>
164 <tal:title replace="request/status/title"/> build request
165 </td>
166 <td>
167 <tal:archive replace="structure request/archive/fmt:link"/>
168 </td>
169 </tr>
170 </tal:snap-build-requests>
159 <tal:snap-builds repeat="build view/builds">171 <tal:snap-builds repeat="build view/builds">
160 <tr tal:attributes="id string:build-${build/id}">172 <tr tal:attributes="id string:build-${build/id}">
161 <td tal:attributes="class string:build_status ${build/status/name}">173 <td tal:attributes="class string:build_status ${build/status/name}">
162174
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2018-09-10 11:18:42 +0000
+++ lib/lp/snappy/tests/test_snap.py 2018-09-13 15:15:44 +0000
@@ -12,6 +12,7 @@
12 timedelta,12 timedelta,
13 )13 )
14import json14import json
15from operator import attrgetter
15from textwrap import dedent16from textwrap import dedent
16from urlparse import urlsplit17from urlparse import urlsplit
1718
@@ -176,7 +177,9 @@
176 self.assertThat(177 self.assertThat(
177 self.factory.makeSnap(),178 self.factory.makeSnap(),
178 DoesNotSnapshot(179 DoesNotSnapshot(
179 ["builds", "completed_builds", "pending_builds"], ISnapView))180 ["pending_build_requests",
181 "builds", "completed_builds", "pending_builds"],
182 ISnapView))
180183
181 def test_initial_date_last_modified(self):184 def test_initial_date_last_modified(self):
182 # The initial value of date_last_modified is date_created.185 # The initial value of date_last_modified is date_created.
@@ -424,7 +427,8 @@
424 snap=Equals(snap),427 snap=Equals(snap),
425 status=Equals(SnapBuildRequestStatus.PENDING),428 status=Equals(SnapBuildRequestStatus.PENDING),
426 error_message=Is(None),429 error_message=Is(None),
427 builds=AfterPreprocessing(set, MatchesSetwise())))430 builds=AfterPreprocessing(set, MatchesSetwise()),
431 archive=Equals(snap.distro_series.main_archive)))
428 [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()432 [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()
429 self.assertThat(job, MatchesStructure(433 self.assertThat(job, MatchesStructure(
430 job_id=Equals(request.id),434 job_id=Equals(request.id),
@@ -744,6 +748,172 @@
744 1, 5)748 1, 5)
745 self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))749 self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
746750
751 def test_getBuildSummaries(self):
752 snap1 = self.factory.makeSnap()
753 snap2 = self.factory.makeSnap()
754 request11 = self.factory.makeSnapBuildRequest(snap=snap1)
755 request12 = self.factory.makeSnapBuildRequest(snap=snap1)
756 request2 = self.factory.makeSnapBuildRequest(snap=snap2)
757 self.factory.makeSnapBuildRequest()
758 build11 = self.factory.makeSnapBuild(snap=snap1)
759 build12 = self.factory.makeSnapBuild(snap=snap1)
760 build2 = self.factory.makeSnapBuild(snap=snap2)
761 self.factory.makeSnapBuild()
762 summary1 = snap1.getBuildSummaries(
763 request_ids=[request11.id, request12.id],
764 build_ids=[build11.id, build12.id])
765 summary2 = snap2.getBuildSummaries(
766 request_ids=[request2.id], build_ids=[build2.id])
767 request_summary_matcher = MatchesDict({
768 "status": Equals("PENDING"),
769 "error_message": Is(None),
770 "builds": Equals([]),
771 })
772 build_summary_matcher = MatchesDict({
773 "status": Equals("NEEDSBUILD"),
774 "buildstate": Equals("Needs building"),
775 "when_complete": Is(None),
776 "when_complete_estimate": Is(False),
777 "build_log_url": Is(None),
778 "build_log_size": Is(None),
779 })
780 self.assertThat(summary1, MatchesDict({
781 "requests": MatchesDict({
782 request11.id: request_summary_matcher,
783 request12.id: request_summary_matcher,
784 }),
785 "builds": MatchesDict({
786 build11.id: build_summary_matcher,
787 build12.id: build_summary_matcher,
788 }),
789 }))
790 self.assertThat(summary2, MatchesDict({
791 "requests": MatchesDict({request2.id: request_summary_matcher}),
792 "builds": MatchesDict({build2.id: build_summary_matcher}),
793 }))
794
795 def test_getBuildSummaries_empty_input(self):
796 snap = self.factory.makeSnap()
797 self.factory.makeSnapBuildRequest(snap=snap)
798 self.assertEqual(
799 {"requests": {}, "builds": {}},
800 snap.getBuildSummaries(request_ids=None, build_ids=None))
801 self.assertEqual(
802 {"requests": {}, "builds": {}},
803 snap.getBuildSummaries(request_ids=[], build_ids=[]))
804 self.assertEqual(
805 {"requests": {}, "builds": {}},
806 snap.getBuildSummaries(request_ids=(), build_ids=()))
807
808 def test_getBuildSummaries_not_matching_snap(self):
809 # getBuildSummaries does not return information for other snaps.
810 snap1 = self.factory.makeSnap()
811 snap2 = self.factory.makeSnap()
812 self.factory.makeSnapBuildRequest(snap=snap1)
813 self.factory.makeSnapBuild(snap=snap1)
814 request2 = self.factory.makeSnapBuildRequest(snap=snap2)
815 build2 = self.factory.makeSnapBuild(snap=snap2)
816 summary1 = snap1.getBuildSummaries(
817 request_ids=[request2.id], build_ids=[build2.id])
818 self.assertEqual({"requests": {}, "builds": {}}, summary1)
819
820 def test_getBuildSummaries_request_error_message_field(self):
821 # The error_message field for a build request should be None unless
822 # the build request failed.
823 snap = self.factory.makeSnap()
824 request = self.factory.makeSnapBuildRequest(snap=snap)
825 self.assertIsNone(request.error_message)
826 summary = snap.getBuildSummaries(request_ids=[request.id])
827 self.assertIsNone(summary["requests"][request.id]["error_message"])
828 job = removeSecurityProxy(request)._job
829 removeSecurityProxy(job).error_message = "Boom"
830 summary = snap.getBuildSummaries(request_ids=[request.id])
831 self.assertEqual(
832 "Boom", summary["requests"][request.id]["error_message"])
833
834 def test_getBuildSummaries_request_builds_field(self):
835 # The builds field should be an empty list unless the build request
836 # has completed and produced builds.
837 self.useFixture(GitHostingFixture(blob=dedent("""\
838 architectures:
839 - build-on: mips64el
840 - build-on: riscv64
841 """)))
842 job = self.makeRequestBuildsJob(["mips64el", "riscv64", "sh4"])
843 snap = job.snap
844 request = snap.getBuildRequest(job.job_id)
845 self.assertEqual([], list(request.builds))
846 summary = snap.getBuildSummaries(request_ids=[request.id])
847 self.assertEqual([], summary["requests"][request.id]["builds"])
848 with person_logged_in(job.requester):
849 with dbuser(config.ISnapRequestBuildsJobSource.dbuser):
850 JobRunner([job]).runAll()
851 summary = snap.getBuildSummaries(request_ids=[request.id])
852 expected_snap_url = "/~%s/+snap/%s" % (snap.owner.name, snap.name)
853 builds = sorted(request.builds, key=attrgetter("id"), reverse=True)
854 expected_builds = [
855 {
856 "self_link": expected_snap_url + "/+build/%d" % build.id,
857 "id": build.id,
858 "distro_arch_series_link": "/%s/%s/%s" % (
859 snap.distro_series.distribution.name,
860 snap.distro_series.name,
861 build.distro_arch_series.architecturetag),
862 "architecture_tag": build.distro_arch_series.architecturetag,
863 "archive_link": (
864 '<a href="/%s" class="sprite distribution">%s</a>' % (
865 build.archive.distribution.name,
866 build.archive.displayname)),
867 "status": "NEEDSBUILD",
868 "buildstate": "Needs building",
869 "when_complete": None,
870 "when_complete_estimate": False,
871 "build_log_url": None,
872 "build_log_size": None,
873 } for build in builds]
874 self.assertEqual(
875 expected_builds, summary["requests"][request.id]["builds"])
876
877 def test_getBuildSummaries_query_count(self):
878 # The DB query count remains constant regardless of the number of
879 # requests and the number of builds resulting from them.
880 self.useFixture(GitHostingFixture(blob=dedent("""\
881 architectures:
882 - build-on: mips64el
883 - build-on: riscv64
884 """)))
885 job = self.makeRequestBuildsJob(["mips64el", "riscv64", "sh4"])
886 snap = job.snap
887 request_ids = []
888 build_ids = []
889
890 def create_items():
891 request = self.factory.makeSnapBuildRequest(
892 snap=snap, archive=self.factory.makeArchive())
893 request_ids.append(request.id)
894 job = removeSecurityProxy(request)._job
895 with person_logged_in(snap.owner.teamowner):
896 # Using the normal job runner interferes with SQL statement
897 # recording, so we run the job by hand.
898 job.start()
899 job.run()
900 job.complete()
901 # XXX cjwatson 2018-06-20: Queued builds with
902 # BuildQueueStatus.WAITING incur extra queries per build due to
903 # estimating start times. For the moment, we dodge this by
904 # starting the builds.
905 for build in job.builds:
906 build.buildqueue_record.markAsBuilding(
907 self.factory.makeBuilder())
908 build_ids.append(self.factory.makeSnapBuild(
909 snap=snap, archive=self.factory.makeArchive()).id)
910
911 recorder1, recorder2 = record_two_runs(
912 lambda: snap.getBuildSummaries(
913 request_ids=request_ids, build_ids=build_ids),
914 create_items, 1, 5)
915 self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
916
747917
748class TestSnapDeleteWithBuilds(TestCaseWithFactory):918class TestSnapDeleteWithBuilds(TestCaseWithFactory):
749919
750920
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2018-08-23 09:30:24 +0000
+++ lib/lp/testing/factory.py 2018-09-13 15:15:44 +0000
@@ -4728,6 +4728,19 @@
4728 IStore(snap).flush()4728 IStore(snap).flush()
4729 return snap4729 return snap
47304730
4731 def makeSnapBuildRequest(self, snap=None, requester=None, archive=None,
4732 pocket=PackagePublishingPocket.UPDATES,
4733 channels=None):
4734 """Make a new SnapBuildRequest."""
4735 if snap is None:
4736 snap = self.makeSnap()
4737 if requester is None:
4738 requester = snap.owner.teamowner
4739 if archive is None:
4740 archive = snap.distro_series.main_archive
4741 return snap.requestBuilds(
4742 requester, archive, pocket, channels=channels)
4743
4731 def makeSnapBuild(self, requester=None, registrant=None, snap=None,4744 def makeSnapBuild(self, requester=None, registrant=None, snap=None,
4732 archive=None, distroarchseries=None, pocket=None,4745 archive=None, distroarchseries=None, pocket=None,
4733 channels=None, date_created=DEFAULT,4746 channels=None, date_created=DEFAULT,