Merge lp:~cjwatson/launchpad/custom-upload-parsing into lp:launchpad

Proposed by Colin Watson on 2012-05-28
Status: Merged
Approved by: William Grant on 2012-05-30
Approved revision: no longer in the source branch.
Merged at revision: 15332
Proposed branch: lp:~cjwatson/launchpad/custom-upload-parsing
Merge into: lp:launchpad
Diff against target: 696 lines (+197/-168)
13 files modified
lib/lp/archivepublisher/customupload.py (+45/-11)
lib/lp/archivepublisher/ddtp_tarball.py (+11/-6)
lib/lp/archivepublisher/debian_installer.py (+18/-22)
lib/lp/archivepublisher/dist_upgrader.py (+19/-21)
lib/lp/archivepublisher/tests/test_customupload.py (+4/-4)
lib/lp/archivepublisher/tests/test_debian_installer.py (+24/-3)
lib/lp/archivepublisher/tests/test_dist_upgrader.py (+23/-3)
lib/lp/archiveuploader/nascentuploadfile.py (+2/-1)
lib/lp/soyuz/enums.py (+3/-3)
lib/lp/soyuz/model/queue.py (+1/-1)
lib/lp/soyuz/scripts/custom_uploads_copier.py (+22/-44)
lib/lp/soyuz/scripts/tests/test_custom_uploads_copier.py (+23/-47)
lib/lp/soyuz/tests/test_distroseriesqueue_debian_installer.py (+2/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/custom-upload-parsing
Reviewer Review Type Date Requested Status
William Grant code 2012-05-28 Approve on 2012-05-30
Review via email: mp+107656@code.launchpad.net

Commit Message

Redesign the CustomUpload interface to allow pushing semi-duplicated parsing code down from CustomUploadsCopier.

Description of the Change

== Summary ==

In bug 827973, Jeroen noted that the custom uploads copier has code that is rather similar to the filename parsing in custom upload implementations, and that these should be unified.

== Proposed fix ==

Redesign the custom upload interface to support both its previous uses and the custom upload copier.

Note that this should make a subsequent fix for bug 827941 trivial.

== Implementation details ==

The custom upload constructor now takes no parameters, with these being passed in later. This makes the new filename parsing methods implemented by each custom upload type (setTargetDirectory and getSeriesKey) make somewhat more sense, and avoids inelegant passing of None in tests.

I discarded the business where extractNameFields uses a default architecture of "all". Firstly, this should now be handled by custom upload implementations if at all. But secondly and more importantly, I know of no real-world case where it matters. The copier only handles debian-installer and dist-upgrader uploads right now. debian-installer uploads always have an architecture (e.g. https://launchpadlibrarian.net/106301545/debian-installer_20101020ubuntu144_i386.changes), and dist-upgrader uploads always have "all" (e.g. https://launchpadlibrarian.net/103130380/update-manager_0.156.14.1_i386.changes). When we add support for ddtp-tarball uploads, they will simply return a different key (the component) rather than worrying about a default architecture.

I pushed the check for conflicts in the filesystem down into CustomUpload. The ddtp-tarball implementation overrides this, since it doesn't care.

== LOC Rationale ==

+25 (my first attempt was +119, but its interface was the wrong shape). However, this is part of an arc of work building on r15312, which was -199, so I'd like to claim credit against that.

== Tests ==

bin/test -vvct test_customupload -t test_debian_installer -t test_dist_upgrader -t test_ddtp_tarball -t test_distroseriesqueue -t test_custom_uploads_copier

== Demo and Q/A ==

None needed; this is refactoring.

To post a comment you must log in.
William Grant (wgrant) wrote :

Looks reasonable to me, although I'd personally prefer to turn getSeriesKey into a classmethod and merge setTargetDirectory back into __init__.

review: Approve (code)
Colin Watson (cjwatson) wrote :

I've made the classmethod change, thanks. I prefer to keep setTargetDirectory separate, though, in order that all processing failures happen during process() rather than some of them happening during the constructor instead; this makes more sense to me, and it makes it easier to consolidate the previously-duplicated *AlreadyExists code too.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/archivepublisher/customupload.py'
2--- lib/lp/archivepublisher/customupload.py 2012-01-06 11:08:30 +0000
3+++ lib/lp/archivepublisher/customupload.py 2012-05-30 10:30:27 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Infrastructure for handling custom uploads.
10@@ -71,7 +71,7 @@
11
12 class CustomUploadTarballBadFile(CustomUploadError):
13 """A file was found which resolves outside the immediate tree.
14-
15+
16 This can happen if someone embeds ../file in the tar, for example.
17 """
18 def __init__(self, tarfile_path, file_name):
19@@ -80,30 +80,64 @@
20 CustomUploadError.__init__(self, message)
21
22
23+class CustomUploadAlreadyExists(CustomUploadError):
24+ """A build for this type, architecture, and version already exists."""
25+ def __init__(self, custom_type, arch, version):
26+ message = ('%s build %s for architecture %s already exists' %
27+ (custom_type, arch, version))
28+ CustomUploadError.__init__(self, message)
29+
30+
31 class CustomUpload:
32 """Base class for custom upload handlers"""
33
34- # The following should be overridden by subclasses, probably in
35- # their __init__
36- targetdir = None
37- version = None
38+ # This should be set as a class property on each subclass.
39+ custom_type = None
40
41- def __init__(self, archive_root, tarfile_path, distroseries):
42- self.archive_root = archive_root
43- self.tarfile_path = tarfile_path
44- self.distroseries = distroseries
45+ def __init__(self):
46+ self.targetdir = None
47+ self.version = None
48+ self.arch = None
49
50 self.tmpdir = None
51
52- def process(self):
53+ def process(self, archive_root, tarfile_path, distroseries):
54 """Process the upload and install it into the archive."""
55+ self.tarfile_path = tarfile_path
56 try:
57+ self.setTargetDirectory(archive_root, tarfile_path, distroseries)
58+ self.checkForConflicts()
59 self.extract()
60 self.installFiles()
61 self.fixCurrentSymlink()
62 finally:
63 self.cleanup()
64
65+ def setTargetDirectory(self, archive_root, tarfile_path, distroseries):
66+ """Set self.targetdir based on parameters.
67+
68+ This should also set self.version and self.arch (if applicable) as a
69+ side-effect.
70+ """
71+ raise NotImplementedError
72+
73+ @classmethod
74+ def getSeriesKey(cls, tarfile_path):
75+ """Get a unique key for instances of this custom upload type.
76+
77+ The key should differ for any uploads that may be published
78+ simultaneously, but should be identical for (e.g.) different
79+ versions of the same type of upload on the same architecture in the
80+ same series. Returns None on failure to parse tarfile_path.
81+ """
82+ raise NotImplementedError
83+
84+ def checkForConflicts(self):
85+ """Check for conflicts with existing publications in the archive."""
86+ if os.path.exists(os.path.join(self.targetdir, self.version)):
87+ raise CustomUploadAlreadyExists(
88+ self.custom_type, self.arch, self.version)
89+
90 def verifyBeforeExtracting(self, tar):
91 """Verify the tarball before extracting it.
92
93
94=== modified file 'lib/lp/archivepublisher/ddtp_tarball.py'
95--- lib/lp/archivepublisher/ddtp_tarball.py 2012-04-16 23:02:44 +0000
96+++ lib/lp/archivepublisher/ddtp_tarball.py 2012-05-30 10:30:27 +0000
97@@ -1,4 +1,4 @@
98-# Copyright 2009 Canonical Ltd. This software is licensed under the
99+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
100 # GNU Affero General Public License version 3 (see the file LICENSE).
101
102 """The processing of translated packages descriptions (ddtp) tarballs.
103@@ -42,14 +42,20 @@
104
105 Old contents will be preserved.
106 """
107- def __init__(self, archive_root, tarfile_path, distroseries):
108- CustomUpload.__init__(self, archive_root, tarfile_path, distroseries)
109+ custom_type = "ddtp-tarball"
110
111+ def setTargetDirectory(self, archive_root, tarfile_path, distroseries):
112 tarfile_base = os.path.basename(tarfile_path)
113 name, component, self.version = tarfile_base.split('_')
114+ self.arch = None
115+
116 self.targetdir = os.path.join(archive_root, 'dists',
117 distroseries, component)
118
119+ def checkForConflicts(self):
120+ # We just overwrite older files, so no conflicts are possible.
121+ pass
122+
123 def shouldInstall(self, filename):
124 # Ignore files outside of the i18n subdirectory
125 return filename.startswith('i18n/')
126@@ -66,6 +72,5 @@
127 Raises CustomUploadError (or some subclass thereof) if
128 anything goes wrong.
129 """
130- upload = DdtpTarballUpload(archive_root, tarfile_path, distroseries)
131- upload.process()
132-
133+ upload = DdtpTarballUpload()
134+ upload.process(archive_root, tarfile_path, distroseries)
135
136=== modified file 'lib/lp/archivepublisher/debian_installer.py'
137--- lib/lp/archivepublisher/debian_installer.py 2012-05-23 23:02:08 +0000
138+++ lib/lp/archivepublisher/debian_installer.py 2012-05-30 10:30:27 +0000
139@@ -8,23 +8,15 @@
140
141 __metaclass__ = type
142
143-__all__ = ['process_debian_installer']
144+__all__ = [
145+ 'DebianInstallerUpload',
146+ 'process_debian_installer',
147+ ]
148
149 import os
150 import shutil
151
152-from lp.archivepublisher.customupload import (
153- CustomUpload,
154- CustomUploadError,
155- )
156-
157-
158-class DebianInstallerAlreadyExists(CustomUploadError):
159- """A build for this type, architecture, and version already exists."""
160- def __init__(self, arch, version):
161- message = ('installer build %s for architecture %s already exists' %
162- (arch, version))
163- CustomUploadError.__init__(self, message)
164+from lp.archivepublisher.customupload import CustomUpload
165
166
167 class DebianInstallerUpload(CustomUpload):
168@@ -46,20 +38,24 @@
169
170 A 'current' symbolic link points to the most recent version.
171 """
172- def __init__(self, archive_root, tarfile_path, distroseries):
173- CustomUpload.__init__(self, archive_root, tarfile_path, distroseries)
174+ custom_type = "installer"
175
176+ def setTargetDirectory(self, archive_root, tarfile_path, distroseries):
177 tarfile_base = os.path.basename(tarfile_path)
178- components = tarfile_base.split('_')
179- self.version = components[1]
180- self.arch = components[2].split('.')[0]
181+ _, self.version, self.arch = tarfile_base.split("_")
182+ self.arch = self.arch.split(".")[0]
183
184 self.targetdir = os.path.join(
185 archive_root, 'dists', distroseries, 'main',
186 'installer-%s' % self.arch)
187
188- if os.path.exists(os.path.join(self.targetdir, self.version)):
189- raise DebianInstallerAlreadyExists(self.arch, self.version)
190+ @classmethod
191+ def getSeriesKey(cls, tarfile_path):
192+ try:
193+ _, _, arch = os.path.basename(tarfile_path).split("_")
194+ return arch.split(".")[0]
195+ except ValueError:
196+ return None
197
198 def extract(self):
199 CustomUpload.extract(self)
200@@ -81,5 +77,5 @@
201 Raises CustomUploadError (or some subclass thereof) if anything goes
202 wrong.
203 """
204- upload = DebianInstallerUpload(archive_root, tarfile_path, distroseries)
205- upload.process()
206+ upload = DebianInstallerUpload()
207+ upload.process(archive_root, tarfile_path, distroseries)
208
209=== modified file 'lib/lp/archivepublisher/dist_upgrader.py'
210--- lib/lp/archivepublisher/dist_upgrader.py 2010-08-20 20:31:18 +0000
211+++ lib/lp/archivepublisher/dist_upgrader.py 2012-05-30 10:30:27 +0000
212@@ -1,11 +1,14 @@
213-# Copyright 2009 Canonical Ltd. This software is licensed under the
214+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
215 # GNU Affero General Public License version 3 (see the file LICENSE).
216
217 """The processing of dist-upgrader tarballs."""
218
219 __metaclass__ = type
220
221-__all__ = ['process_dist_upgrader']
222+__all__ = [
223+ 'DistUpgraderUpload',
224+ 'process_dist_upgrader',
225+ ]
226
227 import os
228
229@@ -19,14 +22,6 @@
230 )
231
232
233-class DistUpgraderAlreadyExists(CustomUploadError):
234- """A build for this type, version already exists."""
235- def __init__(self, arch, version):
236- message = ('dist-upgrader build %s for architecture %s already exists'%
237- (arch, version))
238- CustomUploadError.__init__(self, message)
239-
240-
241 class DistUpgraderBadVersion(CustomUploadError):
242 def __init__(self, tarfile_path, exc):
243 message = "bad version found in '%s': %s" % (tarfile_path, str(exc))
244@@ -60,20 +55,23 @@
245
246 A 'current' symbolic link points to the most recent version.
247 """
248- def __init__(self, archive_root, tarfile_path, distroseries):
249- CustomUpload.__init__(self, archive_root, tarfile_path, distroseries)
250+ custom_type = "dist-upgrader"
251
252+ def setTargetDirectory(self, archive_root, tarfile_path, distroseries):
253 tarfile_base = os.path.basename(tarfile_path)
254- name, self.version, arch = tarfile_base.split('_')
255- arch = arch.split('.')[0]
256+ name, self.version, self.arch = tarfile_base.split("_")
257+ self.arch = self.arch.split(".")[0]
258
259 self.targetdir = os.path.join(archive_root, 'dists', distroseries,
260- 'main', 'dist-upgrader-%s' % arch)
261+ 'main', 'dist-upgrader-%s' % self.arch)
262
263- # Make sure the target version doesn't already exist. If it does, raise
264- # DistUpgraderAlreadyExists.
265- if os.path.exists(os.path.join(self.targetdir, self.version)):
266- raise DistUpgraderAlreadyExists(arch, self.version)
267+ @classmethod
268+ def getSeriesKey(cls, tarfile_path):
269+ try:
270+ _, _, arch = os.path.basename(tarfile_path).split("_")
271+ return arch.split(".")[0]
272+ except ValueError:
273+ return None
274
275 def shouldInstall(self, filename):
276 """ Install files from a dist-upgrader tarball.
277@@ -104,5 +102,5 @@
278 Raises CustomUploadError (or some subclass thereof) if anything goes
279 wrong.
280 """
281- upload = DistUpgraderUpload(archive_root, tarfile_path, distroseries)
282- upload.process()
283+ upload = DistUpgraderUpload()
284+ upload.process(archive_root, tarfile_path, distroseries)
285
286=== modified file 'lib/lp/archivepublisher/tests/test_customupload.py'
287--- lib/lp/archivepublisher/tests/test_customupload.py 2012-02-21 22:46:28 +0000
288+++ lib/lp/archivepublisher/tests/test_customupload.py 2012-05-30 10:30:27 +0000
289@@ -46,7 +46,7 @@
290 """
291 # Setup a bogus `CustomUpload` object with the 'targetdir' pointing
292 # to the directory created for the test.
293- custom_processor = CustomUpload(None, None, None)
294+ custom_processor = CustomUpload()
295 custom_processor.targetdir = self.test_dir
296
297 # Let's create 4 entries named as valid versions.
298@@ -79,10 +79,10 @@
299 def setUp(self):
300 TestCase.setUp(self)
301 self.tarfile_path = "/tmp/_verify_extract"
302- self.tarfile_name = os.path.join(
303- self.tarfile_path, "test_tarfile.tar")
304- self.custom_processor = CustomUpload(None, self.tarfile_name, None)
305+ self.tarfile_name = os.path.join(self.tarfile_path, "test_tarfile.tar")
306+ self.custom_processor = CustomUpload()
307 self.custom_processor.tmpdir = self.makeTemporaryDirectory()
308+ self.custom_processor.tarfile_path = self.tarfile_name
309
310 def createTarfile(self):
311 self.tar_fileobj = cStringIO.StringIO()
312
313=== modified file 'lib/lp/archivepublisher/tests/test_debian_installer.py'
314--- lib/lp/archivepublisher/tests/test_debian_installer.py 2012-05-25 13:26:08 +0000
315+++ lib/lp/archivepublisher/tests/test_debian_installer.py 2012-05-30 10:30:27 +0000
316@@ -9,9 +9,12 @@
317
318 import os
319
320-from lp.archivepublisher.customupload import CustomUploadBadUmask
321+from lp.archivepublisher.customupload import (
322+ CustomUploadAlreadyExists,
323+ CustomUploadBadUmask,
324+ )
325 from lp.archivepublisher.debian_installer import (
326- DebianInstallerAlreadyExists,
327+ DebianInstallerUpload,
328 process_debian_installer,
329 )
330 from lp.services.tarfile_helpers import LaunchpadWriteTarFile
331@@ -69,7 +72,7 @@
332 # If the target directory already exists, processing fails.
333 self.openArchive()
334 os.makedirs(self.getInstallerPath("."))
335- self.assertRaises(DebianInstallerAlreadyExists, self.process)
336+ self.assertRaises(CustomUploadAlreadyExists, self.process)
337
338 def test_bad_umask(self):
339 # The umask must be 022 to avoid incorrect permissions.
340@@ -145,3 +148,21 @@
341 0644, os.stat(self.getInstallerPath(filename)).st_mode & 0777)
342 self.assertEqual(
343 0755, os.stat(self.getInstallerPath(directory)).st_mode & 0777)
344+
345+ def test_getSeriesKey_extracts_architecture(self):
346+ # getSeriesKey extracts the architecture from an upload's filename.
347+ self.openArchive()
348+ self.assertEqual(
349+ self.arch, DebianInstallerUpload.getSeriesKey(self.path))
350+
351+ def test_getSeriesKey_returns_None_on_mismatch(self):
352+ # getSeriesKey returns None if the filename does not match the
353+ # expected pattern.
354+ self.assertIsNone(DebianInstallerUpload.getSeriesKey("argh_1.0.jpg"))
355+
356+ def test_getSeriesKey_refuses_names_with_wrong_number_of_fields(self):
357+ # getSeriesKey requires exactly three fields.
358+ self.assertIsNone(DebianInstallerUpload.getSeriesKey(
359+ "package_1.0.tar.gz"))
360+ self.assertIsNone(DebianInstallerUpload.getSeriesKey(
361+ "one_two_three_four_5.tar.gz"))
362
363=== modified file 'lib/lp/archivepublisher/tests/test_dist_upgrader.py'
364--- lib/lp/archivepublisher/tests/test_dist_upgrader.py 2012-05-25 13:27:41 +0000
365+++ lib/lp/archivepublisher/tests/test_dist_upgrader.py 2012-05-30 10:30:27 +0000
366@@ -9,10 +9,13 @@
367
368 import os
369
370-from lp.archivepublisher.customupload import CustomUploadBadUmask
371+from lp.archivepublisher.customupload import (
372+ CustomUploadAlreadyExists,
373+ CustomUploadBadUmask,
374+ )
375 from lp.archivepublisher.dist_upgrader import (
376- DistUpgraderAlreadyExists,
377 DistUpgraderBadVersion,
378+ DistUpgraderUpload,
379 process_dist_upgrader,
380 )
381 from lp.services.tarfile_helpers import LaunchpadWriteTarFile
382@@ -55,7 +58,7 @@
383 self.openArchive("20060302.0120")
384 self.archive.add_file("20060302.0120/hello", "world")
385 os.makedirs(os.path.join(self.getUpgraderPath(), "20060302.0120"))
386- self.assertRaises(DistUpgraderAlreadyExists, self.process)
387+ self.assertRaises(CustomUploadAlreadyExists, self.process)
388
389 def test_bad_umask(self):
390 # The umask must be 022 to avoid incorrect permissions.
391@@ -84,3 +87,20 @@
392 self.openArchive("20070219.1234")
393 self.archive.add_file("foobar/foobar/dapper.tar.gz", "")
394 self.assertRaises(DistUpgraderBadVersion, self.process)
395+
396+ def test_getSeriesKey_extracts_architecture(self):
397+ # getSeriesKey extracts the architecture from an upload's filename.
398+ self.openArchive("20060302.0120")
399+ self.assertEqual("all", DistUpgraderUpload.getSeriesKey(self.path))
400+
401+ def test_getSeriesKey_returns_None_on_mismatch(self):
402+ # getSeriesKey returns None if the filename does not match the
403+ # expected pattern.
404+ self.assertIsNone(DistUpgraderUpload.getSeriesKey("argh_1.0.jpg"))
405+
406+ def test_getSeriesKey_refuses_names_with_wrong_number_of_fields(self):
407+ # getSeriesKey requires exactly three fields.
408+ self.assertIsNone(DistUpgraderUpload.getSeriesKey(
409+ "package_1.0.tar.gz"))
410+ self.assertIsNone(DistUpgraderUpload.getSeriesKey(
411+ "one_two_three_four_5.tar.gz"))
412
413=== modified file 'lib/lp/archiveuploader/nascentuploadfile.py'
414--- lib/lp/archiveuploader/nascentuploadfile.py 2012-04-27 11:50:45 +0000
415+++ lib/lp/archiveuploader/nascentuploadfile.py 2012-05-30 10:30:27 +0000
416@@ -254,7 +254,8 @@
417 results in new archive files.
418 """
419
420- # This is a marker as per the comment in dbschema.py: ##CUSTOMFORMAT##
421+ # This is a marker as per the comment in lib/lp/soyuz/enums.py:
422+ ##CUSTOMFORMAT##
423 # Essentially if you change anything to do with custom formats, grep for
424 # the marker in the codebase and make sure the same changes are made
425 # everywhere which needs them.
426
427=== modified file 'lib/lp/soyuz/enums.py'
428--- lib/lp/soyuz/enums.py 2012-04-24 17:24:32 +0000
429+++ lib/lp/soyuz/enums.py 2012-05-30 10:30:27 +0000
430@@ -1,4 +1,4 @@
431-# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
432+# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
433 # GNU Affero General Public License version 3 (see the file LICENSE).
434
435 """Enumerations used in the lp/soyuz modules."""
436@@ -423,8 +423,8 @@
437
438
439 # If you change this (add items, change the meaning, whatever) search for
440-# the token ##CUSTOMFORMAT## e.g. database/queue.py or nascentupload.py and
441-# update the stuff marked with it.
442+# the token ##CUSTOMFORMAT## e.g. queue.py or nascentupload.py and update
443+# the stuff marked with it.
444 class PackageUploadCustomFormat(DBEnumeratedType):
445 """Custom formats valid for the upload queue
446
447
448=== modified file 'lib/lp/soyuz/model/queue.py'
449--- lib/lp/soyuz/model/queue.py 2012-05-14 03:12:44 +0000
450+++ lib/lp/soyuz/model/queue.py 2012-05-30 10:30:27 +0000
451@@ -1238,7 +1238,7 @@
452
453 def publish(self, logger=None):
454 """See `IPackageUploadCustom`."""
455- # This is a marker as per the comment in dbschema.py.
456+ # This is a marker as per the comment in lib/lp/soyuz/enums.py:
457 ##CUSTOMFORMAT##
458 # Essentially, if you alter anything to do with what custom formats
459 # are, what their tags are, or anything along those lines, you should
460
461=== modified file 'lib/lp/soyuz/scripts/custom_uploads_copier.py'
462--- lib/lp/soyuz/scripts/custom_uploads_copier.py 2011-08-17 12:24:47 +0000
463+++ lib/lp/soyuz/scripts/custom_uploads_copier.py 2012-05-30 10:30:27 +0000
464@@ -1,4 +1,4 @@
465-# Copyright 2011 Canonical Ltd. This software is licensed under the
466+# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
467 # GNU Affero General Public License version 3 (see the file LICENSE).
468
469 """Copy latest custom uploads into a distribution release series.
470@@ -13,10 +13,11 @@
471 ]
472
473 from operator import attrgetter
474-import re
475
476 from zope.component import getUtility
477
478+from lp.archivepublisher.debian_installer import DebianInstallerUpload
479+from lp.archivepublisher.dist_upgrader import DistUpgraderUpload
480 from lp.registry.interfaces.pocket import PackagePublishingPocket
481 from lp.services.database.bulk import load_referencing
482 from lp.soyuz.enums import PackageUploadCustomFormat
483@@ -30,10 +31,16 @@
484 class CustomUploadsCopier:
485 """Copy `PackageUploadCustom` objects into a new `DistroSeries`."""
486
487- copyable_types = [
488- PackageUploadCustomFormat.DEBIAN_INSTALLER,
489- PackageUploadCustomFormat.DIST_UPGRADER,
490- ]
491+ # This is a marker as per the comment in lib/lp/soyuz/enums.py:
492+ ##CUSTOMFORMAT##
493+ # Essentially, if you alter anything to do with what custom formats are,
494+ # what their tags are, or anything along those lines, you should grep
495+ # for the marker in the source tree and fix it up in every place so
496+ # marked.
497+ copyable_types = {
498+ PackageUploadCustomFormat.DEBIAN_INSTALLER: DebianInstallerUpload,
499+ PackageUploadCustomFormat.DIST_UPGRADER: DistUpgraderUpload,
500+ }
501
502 def __init__(self, target_series):
503 self.target_series = target_series
504@@ -45,46 +52,16 @@
505 def getCandidateUploads(self, source_series):
506 """Find custom uploads that may need copying."""
507 uploads = source_series.getPackageUploads(
508- custom_type=self.copyable_types)
509+ custom_type=self.copyable_types.keys())
510 load_referencing(PackageUploadCustom, uploads, ['packageuploadID'])
511 customs = sum([list(upload.customfiles) for upload in uploads], [])
512 customs = filter(self.isCopyable, customs)
513 customs.sort(key=attrgetter('id'), reverse=True)
514 return customs
515
516- def extractNameFields(self, filename):
517- """Get the relevant fields out of `filename`.
518-
519- Scans filenames of any of these forms:
520-
521- <package>_<version>_<architecture>.tar.<compression_suffix>
522- <package>_<version>.tar[.<compression_suffix>]
523-
524- Versions may contain dots, dashes etc. but no underscores.
525-
526- :return: A tuple of (<architecture>, version); or None if the
527- filename does not match the expected pattern. If no
528- architecture is found in the filename, it defaults to 'all'.
529- """
530- # XXX JeroenVemreulen 2011-08-17, bug=827973: Push this down
531- # into the CustomUpload-derived classes, and share it with their
532- # constructors.
533- regex_parts = {
534- 'package': "[^_]+",
535- 'version': "[^_]+",
536- 'arch': "[^._]+",
537- }
538- filename_regex = (
539- "%(package)s_(%(version)s)(?:_(%(arch)s))?.tar" % regex_parts)
540- match = re.match(filename_regex, filename)
541- if match is None:
542- return None
543- default_arch = 'all'
544- fields = match.groups(default_arch)
545- if len(fields) != 2:
546- return None
547- version, architecture = fields
548- return (architecture, version)
549+ def extractSeriesKey(self, custom_type, filename):
550+ """Get the relevant fields out of `filename` for `custom_type`."""
551+ return custom_type.getSeriesKey(filename)
552
553 def getKey(self, upload):
554 """Get an indexing key for `upload`."""
555@@ -92,12 +69,13 @@
556 # translations tarballs, we'll have to include the component
557 # name as well.
558 custom_format = upload.customformat
559- name_fields = self.extractNameFields(upload.libraryfilealias.filename)
560- if name_fields is None:
561+ series_key = self.extractSeriesKey(
562+ self.copyable_types[custom_format],
563+ upload.libraryfilealias.filename)
564+ if series_key is None:
565 return None
566 else:
567- arch, version = name_fields
568- return (custom_format, arch)
569+ return (custom_format, series_key)
570
571 def getLatestUploads(self, source_series):
572 """Find the latest uploads.
573
574=== modified file 'lib/lp/soyuz/scripts/tests/test_custom_uploads_copier.py'
575--- lib/lp/soyuz/scripts/tests/test_custom_uploads_copier.py 2012-01-01 02:58:52 +0000
576+++ lib/lp/soyuz/scripts/tests/test_custom_uploads_copier.py 2012-05-30 10:30:27 +0000
577@@ -1,4 +1,4 @@
578-# Copyright 2011 Canonical Ltd. This software is licensed under the
579+# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
580 # GNU Affero General Public License version 3 (see the file LICENSE).
581
582 """Test copying of custom package uploads for a new `DistroSeries`."""
583@@ -64,12 +64,8 @@
584 # isCopyable checks a custom upload's customformat field to
585 # determine whether the upload is a candidate for copying. It
586 # approves only those whose customformats are in copyable_types.
587- class FakePackageUploadCustom:
588- def __init__(self, customformat):
589- self.customformat = customformat
590-
591 uploads = [
592- FakePackageUploadCustom(custom_type)
593+ FakeUpload(custom_type, None)
594 for custom_type in PackageUploadCustomFormat.items]
595
596 copier = CustomUploadsCopier(FakeDistroSeries())
597@@ -78,48 +74,27 @@
598 CustomUploadsCopier.copyable_types,
599 [upload.customformat for upload in copied_uploads])
600
601- def test_extractNameFields_extracts_architecture_and_version(self):
602- # extractNameFields picks up the architecture and version out
603- # of an upload's filename field.
604- # XXX JeroenVermeulen 2011-08-17, bug=827941: For ddtp
605- # translations tarballs, we'll have to include the component
606- # name as well.
607- package_name = self.factory.getUniqueString('package')
608- version = self.makeVersion()
609- architecture = self.factory.getUniqueString('arch')
610- filename = '%s_%s_%s.tar.gz' % (package_name, version, architecture)
611- copier = CustomUploadsCopier(FakeDistroSeries())
612- self.assertEqual(
613- (architecture, version), copier.extractNameFields(filename))
614-
615- def test_extractNameFields_does_not_require_architecture(self):
616- # When extractNameFields does not see an architecture, it
617- # defaults to 'all'.
618- package_name = self.factory.getUniqueString('package')
619- version = self.makeVersion()
620- filename = '%s_%s.tar.gz' % (package_name, version)
621- copier = CustomUploadsCopier(FakeDistroSeries())
622- self.assertEqual(
623- ('all', version), copier.extractNameFields(filename))
624-
625- def test_extractNameFields_returns_None_on_mismatch(self):
626- # If the filename does not match the expected pattern,
627- # extractNameFields returns None.
628- copier = CustomUploadsCopier(FakeDistroSeries())
629- self.assertIs(None, copier.extractNameFields('argh_1.0.jpg'))
630-
631- def test_extractNameFields_ignores_names_with_too_many_fields(self):
632- # As one particularly nasty case that might break
633- # extractNameFields, a name with more underscore-seprated fields
634- # than the search pattern allows for is sensibly rejected.
635- copier = CustomUploadsCopier(FakeDistroSeries())
636- self.assertIs(
637- None, copier.extractNameFields('one_two_three_four_5.tar.gz'))
638+ def test_getKey_calls_correct_custom_upload_method(self):
639+ # getKey calls the getSeriesKey method on the correct custom upload.
640+ class FakeCustomUpload:
641+ @classmethod
642+ def getSeriesKey(cls, tarfile_path):
643+ return "dummy"
644+
645+ copier = CustomUploadsCopier(FakeDistroSeries())
646+ copier.copyable_types = {
647+ PackageUploadCustomFormat.DEBIAN_INSTALLER: FakeCustomUpload,
648+ }
649+ custom_format, series_key = copier.getKey(
650+ FakeUpload(PackageUploadCustomFormat.DEBIAN_INSTALLER, "anything"))
651+ self.assertEqual(
652+ PackageUploadCustomFormat.DEBIAN_INSTALLER, custom_format)
653+ self.assertEqual("dummy", series_key)
654
655 def test_getKey_returns_None_on_name_mismatch(self):
656- # If extractNameFields returns None, getKey also returns None.
657+ # If extractSeriesKey returns None, getKey also returns None.
658 copier = CustomUploadsCopier(FakeDistroSeries())
659- copier.extractNameFields = FakeMethod()
660+ copier.extractSeriesKey = FakeMethod()
661 self.assertIs(
662 None,
663 copier.getKey(FakeUpload(
664@@ -142,8 +117,9 @@
665 package_name = self.factory.getUniqueString("package")
666 if version is None:
667 version = self.makeVersion()
668- filename = "%s.tar.gz" % '_'.join(
669- filter(None, [package_name, version, arch]))
670+ if arch is None:
671+ arch = self.factory.getUniqueString()
672+ filename = "%s.tar.gz" % '_'.join([package_name, version, arch])
673 package_upload = self.factory.makeCustomPackageUpload(
674 distroseries=distroseries, custom_type=custom_type,
675 filename=filename)
676
677=== modified file 'lib/lp/soyuz/tests/test_distroseriesqueue_debian_installer.py'
678--- lib/lp/soyuz/tests/test_distroseriesqueue_debian_installer.py 2012-05-25 13:26:08 +0000
679+++ lib/lp/soyuz/tests/test_distroseriesqueue_debian_installer.py 2012-05-30 10:30:27 +0000
680@@ -11,7 +11,7 @@
681
682 import transaction
683
684-from lp.archivepublisher.debian_installer import DebianInstallerAlreadyExists
685+from lp.archivepublisher.customupload import CustomUploadAlreadyExists
686 from lp.archiveuploader.nascentupload import NascentUpload
687 from lp.archiveuploader.tests import (
688 datadir,
689@@ -69,6 +69,6 @@
690 "main", "installer-i386", "20070214ubuntu1"))
691 self.assertFalse(upload.queue_root.realiseUpload(self.logger))
692 self.assertRaises(
693- DebianInstallerAlreadyExists,
694+ CustomUploadAlreadyExists,
695 upload.queue_root.customfiles[0].publish, self.logger)
696 self.assertEqual("ACCEPTED", upload.queue_root.status.name)