Merge lp:~cjwatson/launchpad/sourcefileurls-include-meta into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17861
Proposed branch: lp:~cjwatson/launchpad/sourcefileurls-include-meta
Merge into: lp:launchpad
Diff against target: 436 lines (+210/-50)
5 files modified
lib/lp/soyuz/browser/tests/test_publishing_webservice.py (+100/-24)
lib/lp/soyuz/interfaces/publishing.py (+7/-3)
lib/lp/soyuz/model/publishing.py (+19/-6)
lib/lp/soyuz/tests/test_publishing_models.py (+72/-14)
lib/lp/testing/__init__.py (+12/-3)
To merge this branch: bzr merge lp:~cjwatson/launchpad/sourcefileurls-include-meta
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+276928@code.launchpad.net

Commit message

Add include_meta option to SPPH.sourceFileUrls, paralleling BPPH.binaryFileUrls.

Description of the change

Add include_meta option to SPPH.sourceFileUrls, paralleling BPPH.binaryFileUrls.

This is mainly to support dgit, which needs to be able to fetch .dsc files from Launchpad and verify their contents without having to fetch a full Sources file (even if one were available for the .dsc in question) and its trust chain.

The include_meta option is rather weird, in that it causes the method in question to return a completely different type, and had I been doing this from scratch I would probably have added a new getSourceFiles method instead and deprecated sourceFileUrls over time. However, BPPH.binaryFileUrls(include_meta=True) already existed (https://code.launchpad.net/~michael.nelson/launchpad/include-binary-size/+merge/150831), and other things being equal I prefer new interfaces to look like existing ones. I tried to make the implementations a little more in line with each other as well.

While I was here, I also added sha256 to BPPH.binaryFileUrls(include_meta=True), to drag things a little further into the modern world. I didn't see any point in exporting sha1 in the new SPPH.sourceFileUrls(include_meta=True) interface.

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

I'd lean toward consistency wrt. SHA-1, but it doesn't really matter.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/soyuz/browser/tests/test_publishing_webservice.py'
2--- lib/lp/soyuz/browser/tests/test_publishing_webservice.py 2015-11-26 13:31:45 +0000
3+++ lib/lp/soyuz/browser/tests/test_publishing_webservice.py 2015-12-02 13:21:01 +0000
4@@ -3,20 +3,84 @@
5
6 """Test webservice methods related to the publisher."""
7
8-from testtools.matchers import IsInstance
9+from functools import partial
10
11-from lp.services.database.sqlbase import flush_database_caches
12+from lp.services.librarian.browser import ProxiedLibraryFileAlias
13 from lp.services.webapp.interfaces import OAuthPermission
14 from lp.testing import (
15 api_url,
16+ login_person,
17 person_logged_in,
18- RequestTimelineCollector,
19+ record_two_runs,
20 TestCaseWithFactory,
21 )
22 from lp.testing.layers import LaunchpadFunctionalLayer
23+from lp.testing.matchers import HasQueryCount
24 from lp.testing.pages import webservice_for_person
25
26
27+class SourcePackagePublishingHistoryWebserviceTests(TestCaseWithFactory):
28+
29+ layer = LaunchpadFunctionalLayer
30+
31+ def make_spph_for(self, person):
32+ with person_logged_in(person):
33+ spr = self.factory.makeSourcePackageRelease()
34+ self.factory.makeSourcePackageReleaseFile(sourcepackagerelease=spr)
35+ spph = self.factory.makeSourcePackagePublishingHistory(
36+ sourcepackagerelease=spr)
37+ return spph, api_url(spph)
38+
39+ def test_sourceFileUrls(self):
40+ person = self.factory.makePerson()
41+ webservice = webservice_for_person(
42+ person, permission=OAuthPermission.READ_PUBLIC)
43+ spph, url = self.make_spph_for(person)
44+
45+ response = webservice.named_get(
46+ url, 'sourceFileUrls', api_version='devel')
47+
48+ self.assertEqual(200, response.status)
49+ urls = response.jsonBody()
50+ with person_logged_in(person):
51+ sprf = spph.sourcepackagerelease.files[0]
52+ expected_urls = [
53+ ProxiedLibraryFileAlias(
54+ sprf.libraryfile, spph.archive).http_url]
55+ self.assertEqual(expected_urls, urls)
56+
57+ def test_sourceFileUrls_include_meta(self):
58+ person = self.factory.makePerson()
59+ webservice = webservice_for_person(
60+ person, permission=OAuthPermission.READ_PUBLIC)
61+ spph, url = self.make_spph_for(person)
62+
63+ def create_file():
64+ self.factory.makeSourcePackageReleaseFile(
65+ sourcepackagerelease=spph.sourcepackagerelease)
66+
67+ def get_urls():
68+ return webservice.named_get(
69+ url, 'sourceFileUrls', include_meta=True, api_version='devel')
70+
71+ recorder1, recorder2 = record_two_runs(
72+ get_urls, create_file, 2,
73+ login_method=partial(login_person, person), record_request=True)
74+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
75+
76+ response = get_urls()
77+ self.assertEqual(200, response.status)
78+ info = response.jsonBody()
79+ with person_logged_in(person):
80+ expected_info = [{
81+ "url": ProxiedLibraryFileAlias(
82+ sprf.libraryfile, spph.archive).http_url,
83+ "size": sprf.libraryfile.content.filesize,
84+ "sha256": sprf.libraryfile.content.sha256,
85+ } for sprf in spph.sourcepackagerelease.files]
86+ self.assertContentEqual(expected_info, info)
87+
88+
89 class BinaryPackagePublishingHistoryWebserviceTests(TestCaseWithFactory):
90
91 layer = LaunchpadFunctionalLayer
92@@ -33,36 +97,48 @@
93 person = self.factory.makePerson()
94 webservice = webservice_for_person(
95 person, permission=OAuthPermission.READ_PUBLIC)
96+ bpph, url = self.make_bpph_for(person)
97
98 response = webservice.named_get(
99- self.make_bpph_for(person)[1], 'binaryFileUrls',
100- api_version='devel')
101+ url, 'binaryFileUrls', api_version='devel')
102
103 self.assertEqual(200, response.status)
104 urls = response.jsonBody()
105- self.assertEqual(1, len(urls))
106- self.assertTrue(urls[0], IsInstance(unicode))
107+ with person_logged_in(person):
108+ bpf = bpph.binarypackagerelease.files[0]
109+ expected_urls = [
110+ ProxiedLibraryFileAlias(
111+ bpf.libraryfile, bpph.archive).http_url]
112+ self.assertEqual(expected_urls, urls)
113
114 def test_binaryFileUrls_include_meta(self):
115 person = self.factory.makePerson()
116 webservice = webservice_for_person(
117 person, permission=OAuthPermission.READ_PUBLIC)
118-
119 bpph, url = self.make_bpph_for(person)
120- query_counts = []
121- for i in range(3):
122- flush_database_caches()
123- with RequestTimelineCollector() as collector:
124- response = webservice.named_get(
125- url, 'binaryFileUrls', include_meta=True,
126- api_version='devel')
127- query_counts.append(collector.count)
128- with person_logged_in(person):
129- self.factory.makeBinaryPackageFile(
130- binarypackagerelease=bpph.binarypackagerelease)
131- self.assertEqual(query_counts[0], query_counts[-1])
132-
133+
134+ def create_file():
135+ self.factory.makeBinaryPackageFile(
136+ binarypackagerelease=bpph.binarypackagerelease)
137+
138+ def get_urls():
139+ return webservice.named_get(
140+ url, 'binaryFileUrls', include_meta=True, api_version='devel')
141+
142+ recorder1, recorder2 = record_two_runs(
143+ get_urls, create_file, 2,
144+ login_method=partial(login_person, person), record_request=True)
145+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
146+
147+ response = get_urls()
148 self.assertEqual(200, response.status)
149- urls = response.jsonBody()
150- self.assertEqual(3, len(urls))
151- self.assertThat(urls[0], IsInstance(dict))
152+ info = response.jsonBody()
153+ with person_logged_in(person):
154+ expected_info = [{
155+ "url": ProxiedLibraryFileAlias(
156+ bpf.libraryfile, bpph.archive).http_url,
157+ "size": bpf.libraryfile.content.filesize,
158+ "sha1": bpf.libraryfile.content.sha1,
159+ "sha256": bpf.libraryfile.content.sha256,
160+ } for bpf in bpph.binarypackagerelease.files]
161+ self.assertContentEqual(expected_info, info)
162
163=== modified file 'lib/lp/soyuz/interfaces/publishing.py'
164--- lib/lp/soyuz/interfaces/publishing.py 2015-04-08 10:35:22 +0000
165+++ lib/lp/soyuz/interfaces/publishing.py 2015-12-02 13:21:01 +0000
166@@ -578,9 +578,13 @@
167 """
168
169 @export_read_operation()
170- def sourceFileUrls():
171+ @operation_parameters(
172+ include_meta=Bool(title=_("Include Metadata"), required=False))
173+ def sourceFileUrls(include_meta=False):
174 """URLs for this source publication's uploaded source files.
175
176+ :param include_meta: Return a list of dicts with keys url, size, and
177+ sha256 for each URL instead of a simple list.
178 :return: A collection of URLs for this source.
179 """
180
181@@ -869,8 +873,8 @@
182 def binaryFileUrls(include_meta=False):
183 """URLs for this binary publication's binary files.
184
185- :param include_meta: Return a list of dicts with keys url, size
186- and sha1 for each url instead of a simple list.
187+ :param include_meta: Return a list of dicts with keys url, size,
188+ sha1, and sha256 for each URL instead of a simple list.
189 :return: A collection of URLs for this binary.
190 """
191
192
193=== modified file 'lib/lp/soyuz/model/publishing.py'
194--- lib/lp/soyuz/model/publishing.py 2015-07-08 16:05:11 +0000
195+++ lib/lp/soyuz/model/publishing.py 2015-12-02 13:21:01 +0000
196@@ -643,11 +643,23 @@
197 return getUtility(
198 IPublishingSet).getBuildStatusSummaryForSourcePublication(self)
199
200- def sourceFileUrls(self):
201+ def sourceFileUrls(self, include_meta=False):
202 """See `ISourcePackagePublishingHistory`."""
203+ sources = Store.of(self).find(
204+ (LibraryFileAlias, LibraryFileContent),
205+ LibraryFileContent.id == LibraryFileAlias.contentID,
206+ LibraryFileAlias.id == SourcePackageReleaseFile.libraryfileID,
207+ SourcePackageReleaseFile.sourcepackagerelease ==
208+ SourcePackageRelease.id,
209+ SourcePackageRelease.id == self.sourcepackagereleaseID)
210 source_urls = proxied_urls(
211- [file.libraryfile for file in self.sourcepackagerelease.files],
212- self.archive)
213+ [source for source, _ in sources], self.archive)
214+ if include_meta:
215+ meta = [
216+ (content.filesize, content.sha256) for _, content in sources]
217+ return [
218+ dict(url=url, size=size, sha256=sha256)
219+ for url, (size, sha256) in zip(source_urls, meta)]
220 return source_urls
221
222 def binaryFileUrls(self):
223@@ -1064,9 +1076,10 @@
224 [binary for binary, _ in binaries], self.archive)
225 if include_meta:
226 meta = [
227- (content.filesize, content.sha1) for _, content in binaries]
228- return [dict(url=url, size=size, sha1=sha1)
229- for url, (size, sha1) in zip(binary_urls, meta)]
230+ (content.filesize, content.sha1, content.sha256)
231+ for _, content in binaries]
232+ return [dict(url=url, size=size, sha1=sha1, sha256=sha256)
233+ for url, (size, sha1, sha256) in zip(binary_urls, meta)]
234 return binary_urls
235
236
237
238=== modified file 'lib/lp/soyuz/tests/test_publishing_models.py'
239--- lib/lp/soyuz/tests/test_publishing_models.py 2015-05-14 13:23:47 +0000
240+++ lib/lp/soyuz/tests/test_publishing_models.py 2015-12-02 13:21:01 +0000
241@@ -8,6 +8,7 @@
242
243 from lp.app.errors import NotFoundError
244 from lp.buildmaster.enums import BuildStatus
245+from lp.registry.interfaces.sourcepackage import SourcePackageFileType
246 from lp.services.database.constants import UTC_NOW
247 from lp.services.librarian.browser import ProxiedLibraryFileAlias
248 from lp.services.webapp.publisher import canonical_url
249@@ -154,12 +155,68 @@
250 spph = self.factory.makeSourcePackagePublishingHistory()
251 self.assertRaises(NotFoundError, spph.getFileByName, 'not-changelog')
252
253+ def getURLsForSPPH(self, spph, include_meta=False):
254+ spr = spph.sourcepackagerelease
255+ archive = spph.archive
256+ urls = [ProxiedLibraryFileAlias(f.libraryfile, archive).http_url
257+ for f in spr.files]
258+
259+ if include_meta:
260+ meta = [(
261+ f.libraryfile.content.filesize,
262+ f.libraryfile.content.sha256,
263+ ) for f in spr.files]
264+
265+ return [dict(url=url, size=size, sha256=sha256)
266+ for url, (size, sha256) in zip(urls, meta)]
267+ return urls
268+
269+ def makeSPPH(self, num_files=1):
270+ archive = self.factory.makeArchive(private=False)
271+ spr = self.factory.makeSourcePackageRelease(archive=archive)
272+ filetypes = [
273+ SourcePackageFileType.DSC, SourcePackageFileType.ORIG_TARBALL]
274+ for count in range(num_files):
275+ self.factory.makeSourcePackageReleaseFile(
276+ sourcepackagerelease=spr, filetype=filetypes[count % 2])
277+ return self.factory.makeSourcePackagePublishingHistory(
278+ sourcepackagerelease=spr, archive=archive)
279+
280+ def test_sourceFileUrls_no_files(self):
281+ spph = self.makeSPPH(num_files=0)
282+
283+ urls = spph.sourceFileUrls()
284+
285+ self.assertContentEqual([], urls)
286+
287+ def test_sourceFileUrls_one_file(self):
288+ spph = self.makeSPPH(num_files=1)
289+
290+ urls = spph.sourceFileUrls()
291+
292+ self.assertContentEqual(self.getURLsForSPPH(spph), urls)
293+
294+ def test_sourceFileUrls_two_files(self):
295+ spph = self.makeSPPH(num_files=2)
296+
297+ urls = spph.sourceFileUrls()
298+
299+ self.assertContentEqual(self.getURLsForSPPH(spph), urls)
300+
301+ def test_sourceFileUrls_include_meta(self):
302+ spph = self.makeSPPH(num_files=2)
303+
304+ urls = spph.sourceFileUrls(include_meta=True)
305+
306+ self.assertContentEqual(
307+ self.getURLsForSPPH(spph, include_meta=True), urls)
308+
309
310 class TestBinaryPackagePublishingHistory(TestCaseWithFactory):
311
312 layer = LaunchpadFunctionalLayer
313
314- def get_urls_for_bpph(self, bpph, include_meta=False):
315+ def getURLsForBPPH(self, bpph, include_meta=False):
316 bpr = bpph.binarypackagerelease
317 archive = bpph.archive
318 urls = [ProxiedLibraryFileAlias(f.libraryfile, archive).http_url
319@@ -169,13 +226,14 @@
320 meta = [(
321 f.libraryfile.content.filesize,
322 f.libraryfile.content.sha1,
323+ f.libraryfile.content.sha256,
324 ) for f in bpr.files]
325
326- return [dict(url=url, size=size, sha1=sha1)
327- for url, (size, sha1) in zip(urls, meta)]
328+ return [dict(url=url, size=size, sha1=sha1, sha256=sha256)
329+ for url, (size, sha1, sha256) in zip(urls, meta)]
330 return urls
331
332- def make_bpph(self, num_binaries=1):
333+ def makeBPPH(self, num_binaries=1):
334 archive = self.factory.makeArchive(private=False)
335 bpr = self.factory.makeBinaryPackageRelease()
336 filetypes = [BinaryPackageFileType.DEB, BinaryPackageFileType.DDEB]
337@@ -186,40 +244,40 @@
338 binarypackagerelease=bpr, archive=archive)
339
340 def test_binaryFileUrls_no_binaries(self):
341- bpph = self.make_bpph(num_binaries=0)
342+ bpph = self.makeBPPH(num_binaries=0)
343
344 urls = bpph.binaryFileUrls()
345
346 self.assertContentEqual([], urls)
347
348 def test_binaryFileUrls_one_binary(self):
349- bpph = self.make_bpph(num_binaries=1)
350+ bpph = self.makeBPPH(num_binaries=1)
351
352 urls = bpph.binaryFileUrls()
353
354- self.assertContentEqual(self.get_urls_for_bpph(bpph), urls)
355+ self.assertContentEqual(self.getURLsForBPPH(bpph), urls)
356
357 def test_binaryFileUrls_two_binaries(self):
358- bpph = self.make_bpph(num_binaries=2)
359+ bpph = self.makeBPPH(num_binaries=2)
360
361 urls = bpph.binaryFileUrls()
362
363- self.assertContentEqual(self.get_urls_for_bpph(bpph), urls)
364+ self.assertContentEqual(self.getURLsForBPPH(bpph), urls)
365
366 def test_binaryFileUrls_include_meta(self):
367- bpph = self.make_bpph(num_binaries=2)
368+ bpph = self.makeBPPH(num_binaries=2)
369
370 urls = bpph.binaryFileUrls(include_meta=True)
371
372 self.assertContentEqual(
373- self.get_urls_for_bpph(bpph, include_meta=True), urls)
374+ self.getURLsForBPPH(bpph, include_meta=True), urls)
375
376 def test_binaryFileUrls_removed(self):
377 # binaryFileUrls returns URLs even if the files have been removed
378 # from the published archive.
379- bpph = self.make_bpph(num_binaries=2)
380- expected_urls = self.get_urls_for_bpph(bpph)
381- expected_urls_meta = self.get_urls_for_bpph(bpph, include_meta=True)
382+ bpph = self.makeBPPH(num_binaries=2)
383+ expected_urls = self.getURLsForBPPH(bpph)
384+ expected_urls_meta = self.getURLsForBPPH(bpph, include_meta=True)
385 self.assertContentEqual(expected_urls, bpph.binaryFileUrls())
386 self.assertContentEqual(
387 expected_urls_meta, bpph.binaryFileUrls(include_meta=True))
388
389=== modified file 'lib/lp/testing/__init__.py'
390--- lib/lp/testing/__init__.py 2015-10-04 01:28:19 +0000
391+++ lib/lp/testing/__init__.py 2015-12-02 13:21:01 +0000
392@@ -425,7 +425,8 @@
393
394
395 def record_two_runs(tested_method, item_creator, first_round_number,
396- second_round_number=None, login_method=None):
397+ second_round_number=None, login_method=None,
398+ record_request=False):
399 """A helper that returns the two storm statement recorders
400 obtained when running tested_method after having run the
401 method {item_creator} {first_round_number} times and then
402@@ -435,9 +436,17 @@
403 If {login_method} is not None, it is called before each batch of
404 {item_creator} calls.
405
406+ If {record_request} is True, `RequestTimelineCollector` is used to get
407+ the query counts, so {tested_method} should make a web request.
408+ Otherwise, `StormStatementRecorder` is used to get the query count.
409+
410 :return: a tuple containing the two recorders obtained by the successive
411 runs.
412 """
413+ if record_request:
414+ recorder_factory = RequestTimelineCollector
415+ else:
416+ recorder_factory = StormStatementRecorder
417 if login_method is not None:
418 login_method()
419 for i in range(first_round_number):
420@@ -449,7 +458,7 @@
421 if queryInteraction() is not None:
422 clear_permission_cache()
423 getUtility(ILaunchpadCelebrities).clearCache()
424- with StormStatementRecorder() as recorder1:
425+ with recorder_factory() as recorder1:
426 tested_method()
427 # Run {item_creator} {second_round_number} more times.
428 if second_round_number is None:
429@@ -463,7 +472,7 @@
430 if queryInteraction() is not None:
431 clear_permission_cache()
432 getUtility(ILaunchpadCelebrities).clearCache()
433- with StormStatementRecorder() as recorder2:
434+ with recorder_factory() as recorder2:
435 tested_method()
436 return recorder1, recorder2
437