Merge ~cjwatson/launchpad:oci-recipe-build-navigation into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: caa7cbd9e146ab1fcffca2db5ccefb587a5944fa
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:oci-recipe-build-navigation
Merge into: launchpad:master
Diff against target: 268 lines (+111/-15)
7 files modified
lib/lp/oci/browser/ocirecipebuild.py (+20/-1)
lib/lp/oci/browser/tests/test_ocirecipebuild.py (+17/-0)
lib/lp/oci/interfaces/ocirecipebuild.py (+13/-2)
lib/lp/oci/model/ocirecipebuild.py (+22/-4)
lib/lp/oci/model/ocirecipebuildbehaviour.py (+1/-1)
lib/lp/oci/templates/ocirecipebuild-index.pt (+20/-0)
lib/lp/oci/tests/test_ocirecipebuild.py (+18/-7)
Reviewer Review Type Date Requested Status
Thiago F. Pappacena (community) Approve
Review via email: mp+382242@code.launchpad.net

Commit message

Add OCI recipe build file navigation

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Mostly, LGTM. I've just added 2 minor comments.

review: Approve
3e50d91... by Colin Watson

Fix missing parameter in interface

a677549... by Colin Watson

Make OCIRecipeBuild.getFileByName more precise

5974a3e... by Colin Watson

Fix OCIRecipeBuildBehaviour.getLogFileName

The comment says it should have a .txt suffix, so let's actually add
one.

Revision history for this message
Colin Watson (cjwatson) :
caa7cbd... by Colin Watson

Update IOCIRecipeBuildView.getFileByName docstring

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/oci/browser/ocirecipebuild.py b/lib/lp/oci/browser/ocirecipebuild.py
2index dfed462..5d58f5e 100644
3--- a/lib/lp/oci/browser/ocirecipebuild.py
4+++ b/lib/lp/oci/browser/ocirecipebuild.py
5@@ -21,6 +21,11 @@ from lp.app.browser.launchpadform import (
6 LaunchpadFormView,
7 )
8 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
9+from lp.services.librarian.browser import (
10+ FileNavigationMixin,
11+ ProxiedLibraryFileAlias,
12+ )
13+from lp.services.propertycache import cachedproperty
14 from lp.services.webapp import (
15 canonical_url,
16 ContextMenu,
17@@ -31,7 +36,7 @@ from lp.services.webapp import (
18 from lp.soyuz.interfaces.binarypackagebuild import IBuildRescoreForm
19
20
21-class OCIRecipeBuildNavigation(Navigation):
22+class OCIRecipeBuildNavigation(Navigation, FileNavigationMixin):
23
24 usedfor = IOCIRecipeBuild
25
26@@ -70,6 +75,20 @@ class OCIRecipeBuildView(LaunchpadFormView):
27
28 page_title = label
29
30+ @cachedproperty
31+ def files(self):
32+ """Return `LibraryFileAlias`es for files produced by this build."""
33+ if not self.context.was_built:
34+ return None
35+
36+ return [
37+ ProxiedLibraryFileAlias(alias, self.context)
38+ for _, alias, _ in self.context.getFiles() if not alias.deleted]
39+
40+ @cachedproperty
41+ def has_files(self):
42+ return bool(self.files)
43+
44 @property
45 def next_url(self):
46 return canonical_url(self.context)
47diff --git a/lib/lp/oci/browser/tests/test_ocirecipebuild.py b/lib/lp/oci/browser/tests/test_ocirecipebuild.py
48index 9c6eae3..c366055 100644
49--- a/lib/lp/oci/browser/tests/test_ocirecipebuild.py
50+++ b/lib/lp/oci/browser/tests/test_ocirecipebuild.py
51@@ -16,6 +16,7 @@ from testtools.matchers import StartsWith
52 import transaction
53 from zope.component import getUtility
54 from zope.security.interfaces import Unauthorized
55+from zope.security.proxy import removeSecurityProxy
56 from zope.testbrowser.browser import LinkNotFoundError
57
58 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
59@@ -39,6 +40,7 @@ from lp.testing.pages import (
60 find_main_content,
61 find_tags_by_class,
62 )
63+from lp.testing.views import create_initialized_view
64
65
66 class TestCanonicalUrlForOCIRecipeBuild(TestCaseWithFactory):
67@@ -90,6 +92,21 @@ class TestOCIRecipeBuildView(BrowserTestCase):
68 recipe.owner.display_name),
69 self.getMainText(build))
70
71+ def test_files(self):
72+ # OCIRecipeBuildView.files returns all the associated files.
73+ build = self.factory.makeOCIRecipeBuild(status=BuildStatus.FULLYBUILT)
74+ oci_file = self.factory.makeOCIFile(build=build)
75+ build_view = create_initialized_view(build, "+index")
76+ self.assertEqual(
77+ [oci_file.library_file.filename],
78+ [lfa.filename for lfa in build_view.files])
79+ # Deleted files won't be included.
80+ self.assertFalse(oci_file.library_file.deleted)
81+ removeSecurityProxy(oci_file.library_file).content = None
82+ self.assertTrue(oci_file.library_file.deleted)
83+ build_view = create_initialized_view(build, "+index")
84+ self.assertEqual([], build_view.files)
85+
86
87 class TestOCIRecipeBuildOperations(BrowserTestCase):
88
89diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py
90index 752e61d..90bb829 100644
91--- a/lib/lp/oci/interfaces/ocirecipebuild.py
92+++ b/lib/lp/oci/interfaces/ocirecipebuild.py
93@@ -57,13 +57,24 @@ class IOCIRecipeBuildView(IPackageBuild):
94 "The date when the build completed or is estimated to complete."),
95 readonly=True)
96
97- def getByFileName():
98- """Retrieve a file by filename
99+ def getFiles():
100+ """Retrieve the build's `IOCIFile` records.
101
102 :return: A result set of (`IOCIFile`, `ILibraryFileAlias`,
103 `ILibraryFileContent`).
104 """
105
106+ def getFileByName(filename):
107+ """Return the corresponding `ILibraryFileAlias` in this context.
108+
109+ The `filename` may be that of the build log, the upload log, or any
110+ of this build's `OCIFile`s.
111+
112+ :param filename: The filename to look up.
113+ :raises NotFoundError: if no file exists with the given name.
114+ :return: The corresponding `ILibraryFileAlias`.
115+ """
116+
117 def getLayerFileByDigest(layer_file_digest):
118 """Retrieve a layer file by the digest.
119
120diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
121index 21c1cce..595da53 100644
122--- a/lib/lp/oci/model/ocirecipebuild.py
123+++ b/lib/lp/oci/model/ocirecipebuild.py
124@@ -15,11 +15,13 @@ __all__ = [
125 from datetime import timedelta
126
127 import pytz
128+from storm.expr import LeftJoin
129 from storm.locals import (
130 Bool,
131 DateTime,
132 Desc,
133 Int,
134+ Or,
135 Reference,
136 Store,
137 Storm,
138@@ -230,15 +232,31 @@ class OCIRecipeBuild(PackageBuildMixin, Storm):
139 durations.sort()
140 return durations[len(durations) // 2]
141
142- def getByFileName(self, filename):
143+ def getFiles(self):
144+ """See `IOCIRecipeBuild`."""
145 result = Store.of(self).find(
146 (OCIFile, LibraryFileAlias, LibraryFileContent),
147 OCIFile.build == self.id,
148 LibraryFileAlias.id == OCIFile.library_file_id,
149- LibraryFileContent.id == LibraryFileAlias.contentID,
150+ LibraryFileContent.id == LibraryFileAlias.contentID)
151+ return result.order_by([LibraryFileAlias.filename, OCIFile.id])
152+
153+ def getFileByName(self, filename):
154+ """See `IOCIRecipeBuild`."""
155+ origin = [
156+ LibraryFileAlias,
157+ LeftJoin(OCIFile, LibraryFileAlias.id == OCIFile.library_file_id),
158+ ]
159+ file_object = Store.of(self).using(*origin).find(
160+ LibraryFileAlias,
161+ Or(
162+ LibraryFileAlias.id.is_in((self.log_id, self.upload_log_id)),
163+ OCIFile.build == self.id),
164 LibraryFileAlias.filename == filename).one()
165- if result is not None:
166- return result
167+
168+ if file_object is not None and file_object.filename == filename:
169+ return file_object
170+
171 raise NotFoundError(filename)
172
173 @cachedproperty
174diff --git a/lib/lp/oci/model/ocirecipebuildbehaviour.py b/lib/lp/oci/model/ocirecipebuildbehaviour.py
175index f0debfa..dbef696 100644
176--- a/lib/lp/oci/model/ocirecipebuildbehaviour.py
177+++ b/lib/lp/oci/model/ocirecipebuildbehaviour.py
178@@ -51,7 +51,7 @@ class OCIRecipeBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase):
179
180 # Examples:
181 # buildlog_oci_ubuntu_wily_amd64_name_FULLYBUILT.txt
182- return 'buildlog_oci_%s_%s_%s_%s_%s' % (
183+ return 'buildlog_oci_%s_%s_%s_%s_%s.txt' % (
184 series.distribution.name, series.name,
185 self.build.processor.name, self.build.recipe.name,
186 self.build.status.name)
187diff --git a/lib/lp/oci/templates/ocirecipebuild-index.pt b/lib/lp/oci/templates/ocirecipebuild-index.pt
188index b9ff8c1..2f88103 100644
189--- a/lib/lp/oci/templates/ocirecipebuild-index.pt
190+++ b/lib/lp/oci/templates/ocirecipebuild-index.pt
191@@ -30,6 +30,10 @@
192 </div>
193 </div> <!-- yui-g -->
194
195+ <div id="files" class="portlet" tal:condition="view/has_files">
196+ <div metal:use-macro="template/macros/files"/>
197+ </div>
198+
199 <div id="buildlog" class="portlet"
200 tal:condition="context/status/enumvalue:BUILDING">
201 <div metal:use-macro="template/macros/buildlog"/>
202@@ -143,6 +147,22 @@
203 </div>
204 </metal:macro>
205
206+ <metal:macro define-macro="files">
207+ <tal:comment replace="nothing">
208+ Files section.
209+ </tal:comment>
210+ <h2>Built files</h2>
211+ <p>Files resulting from this build:</p>
212+ <ul>
213+ <li tal:repeat="file view/files">
214+ <a class="sprite download"
215+ tal:content="file/filename"
216+ tal:attributes="href file/http_url"/>
217+ (<span tal:replace="file/content/filesize/fmt:bytes"/>)
218+ </li>
219+ </ul>
220+ </metal:macro>
221+
222 <metal:macro define-macro="buildlog">
223 <tal:comment replace="nothing">
224 Buildlog section.
225diff --git a/lib/lp/oci/tests/test_ocirecipebuild.py b/lib/lp/oci/tests/test_ocirecipebuild.py
226index 5ae8b78..400d78d 100644
227--- a/lib/lp/oci/tests/test_ocirecipebuild.py
228+++ b/lib/lp/oci/tests/test_ocirecipebuild.py
229@@ -68,21 +68,32 @@ class TestOCIRecipeBuild(TestCaseWithFactory):
230 def test_addFile(self):
231 lfa = self.factory.makeLibraryFileAlias()
232 self.build.addFile(lfa)
233- _, result_lfa, _ = self.build.getByFileName(lfa.filename)
234+ result_lfa = self.build.getFileByName(lfa.filename)
235 self.assertEqual(result_lfa, lfa)
236
237- def test_getByFileName(self):
238+ def test_getFileByName(self):
239 files = [self.factory.makeOCIFile(build=self.build) for x in range(3)]
240- result, _, _ = self.build.getByFileName(
241- files[0].library_file.filename)
242- self.assertEqual(result, files[0])
243+ result = self.build.getFileByName(files[0].library_file.filename)
244+ self.assertEqual(files[0].library_file, result)
245
246- def test_getByFileName_missing(self):
247+ def test_getFileByName_missing(self):
248 self.assertRaises(
249 NotFoundError,
250- self.build.getByFileName,
251+ self.build.getFileByName,
252 "missing")
253
254+ def test_getFileByName_logs(self):
255+ # getFileByName returns the logs when requested by name.
256+ self.build.setLog(
257+ self.factory.makeLibraryFileAlias(filename="buildlog.txt.gz"))
258+ self.assertEqual(
259+ self.build.log, self.build.getFileByName("buildlog.txt.gz"))
260+ self.assertRaises(NotFoundError, self.build.getFileByName, "foo")
261+ self.build.storeUploadLog("uploaded")
262+ self.assertEqual(
263+ self.build.upload_log,
264+ self.build.getFileByName(self.build.upload_log.filename))
265+
266 def test_getLayerFileByDigest(self):
267 files = [self.factory.makeOCIFile(
268 build=self.build, layer_file_digest=six.text_type(x))

Subscribers

People subscribed via source and target branches

to status/vote changes: