Merge lp:~cjwatson/launchpad/copy-ddtp into lp:launchpad

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: no longer in the source branch.
Merged at revision: 15598
Proposed branch: lp:~cjwatson/launchpad/copy-ddtp
Merge into: lp:launchpad
Diff against target: 225 lines (+85/-19)
7 files modified
lib/lp/archivepublisher/ddtp_tarball.py (+12/-2)
lib/lp/archivepublisher/debian_installer.py (+5/-2)
lib/lp/archivepublisher/dist_upgrader.py (+5/-2)
lib/lp/archivepublisher/tests/test_ddtp_tarball.py (+32/-1)
lib/lp/archivepublisher/uefi.py (+5/-2)
lib/lp/soyuz/scripts/custom_uploads_copier.py (+2/-3)
lib/lp/soyuz/scripts/tests/test_custom_uploads_copier.py (+24/-7)
To merge this branch: bzr merge lp:~cjwatson/launchpad/copy-ddtp
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+111688@code.launchpad.net

Commit message

Teach CustomUploadsCopier to copy ddtp-tarball files.

Description of the change

== Summary ==

Bug 827941 reports that there's another custom upload type, namely ddtp-tarball, that needs to be accounted for in CustomUploadsCopier so that it gets copied to new distroseries. (Bug 672314 has an example of a concrete problem that I think has this as its root cause.)

== Proposed fix ==

At one point this would have been fiddly, but following https://code.launchpad.net/~cjwatson/launchpad/custom-upload-parsing/+merge/107656 it's pretty easy: implement DdtpTarballUpload.getSeriesKey and add an item to CustomUploadsCopier.copyable_types. Job done.

== LOC Rationale ==

+46. However, this is part of an arc as follows:

  https://code.launchpad.net/~cjwatson/launchpad/pcj-reupload/+merge/111124 (+180)
  https://code.launchpad.net/~cjwatson/launchpad/copy-custom-uploads/+merge/111653 (+66)
  this branch (+46)
  a probable future branch to fix P-a-s handling in direct copies (should be <100)
  removal of unembargo-package, delayed copies, ubuntu-security celebrity, etc. (at least -600)

Even fairly conservatively, this arc should end up well in the black.

== Tests ==

bin/test -vvct test_ddtp_tarball -t test_custom_uploads_copier

== Demo and Q/A ==

It would be best to wait until after https://code.launchpad.net/~cjwatson/launchpad/copy-custom-uploads/+merge/111653 has landed. Once that's done, the same QA procedure as in that MP should work, except that the package to upload is whatever Michael uses to upload translations and the paths are dists/precise-updates/*/i18n/.

I won't be able to land and QA this until the issue I spotted in https://bugs.launchpad.net/launchpad/+bug/827941/comments/3 is fixed, because the current way that DDTP tarballs are uploaded is really not amenable to the package copier, owing to not having an SPPH. However, I believe this code will be fine once the uploads are fixed to be less evil (and we might even be able to delete a bit of code from archiveuploader as a result) so I might as well get this in for review anyway.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

In ddtp_tarball.py ( line 23 of the diff) there is this line:

    _, component, _ = tarfile_path.split("_")

Since you are interested in underscores in the file name and not
the entire path, os.path.basename(tarfile_path).split("_") would be
better.

It would also be good to have some tests with underscores in the path
that show that the method handles them correctly.

Using the ValueError as an way of determining that the destructuring
assignment failed seems a bit indirect as far as input validation goes.

Why is getSeriesKey a classmethod when "cls" isn't used? Maybe a
staticmethod is more appropriate.

How about this for getSeriesKey?

@staticmethod
def getSeriesKey(tarfile_path):
    fn = os.path.basename(tarfile_path)
    if fn.count('_') != 2:
        # The file name does not match the pattern, so no series key
        # can be extracted.
        return None
    return fn.split('_')[1]

Revision history for this message
Colin Watson (cjwatson) wrote :

On Mon, Jun 25, 2012 at 06:25:23PM -0000, Benji York wrote:
> In ddtp_tarball.py ( line 23 of the diff) there is this line:
>
> _, component, _ = tarfile_path.split("_")
>
> Since you are interested in underscores in the file name and not
> the entire path, os.path.basename(tarfile_path).split("_") would be
> better.

Yes, indeed. Much of this is changing anyway as a result of feedback on
https://code.launchpad.net/~cjwatson/launchpad/custom-uefi/+merge/111626;
as a result I think I'm going to put this branch on temporary hold until
that one's ready to land, rather than doing much the same work in two
branches. (For instance recent work on that branch means that
getSeriesKey has to be a classmethod, although a helper method it calls
can probably become a staticmethod.)

> Using the ValueError as an way of determining that the destructuring
> assignment failed seems a bit indirect as far as input validation goes.
[...]
> @staticmethod
> def getSeriesKey(tarfile_path):
> fn = os.path.basename(tarfile_path)
> if fn.count('_') != 2:
> # The file name does not match the pattern, so no series key
> # can be extracted.
> return None
> return fn.split('_')[1]

I don't much like the look-before-you-leap pattern here, TBH; it seems
easy for it to get out of sync as code mutates over time, especially
since "fn.count('_') != 2" and "fn.split('_')[1]" are spelled just
differently enough to make it easy to forget to change one of them.

Revision history for this message
Colin Watson (cjwatson) wrote :

I think this is ready for review again now, since custom-uefi has landed so I was able to rephrase this a little in terms of that.

Revision history for this message
Benji York (benji) wrote :

I still think the look-before-you-leap approach is better here, but there's no reason to hold this branch up on that.

review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) wrote :

I'm putting together a compromise that's a bit more explicit about the input validation without duplicating checking and parsing logic in quite the same way.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/archivepublisher/ddtp_tarball.py'
2--- lib/lp/archivepublisher/ddtp_tarball.py 2012-06-28 16:32:27 +0000
3+++ lib/lp/archivepublisher/ddtp_tarball.py 2012-07-10 11:27:22 +0000
4@@ -49,8 +49,11 @@
5
6 @staticmethod
7 def parsePath(tarfile_path):
8- name, component, version = os.path.basename(tarfile_path).split("_")
9- return name, component, version
10+ tarfile_base = os.path.basename(tarfile_path)
11+ bits = tarfile_base.split("_")
12+ if len(bits) != 3:
13+ raise ValueError("%s is not NAME_COMPONENT_VERSION" % tarfile_base)
14+ return tuple(bits)
15
16 def setTargetDirectory(self, pubconf, tarfile_path, distroseries):
17 _, component, self.version = self.parsePath(tarfile_path)
18@@ -58,6 +61,13 @@
19 self.targetdir = os.path.join(
20 pubconf.archiveroot, 'dists', distroseries, component)
21
22+ @classmethod
23+ def getSeriesKey(cls, tarfile_path):
24+ try:
25+ return cls.parsePath(tarfile_path)[1]
26+ except ValueError:
27+ return None
28+
29 def checkForConflicts(self):
30 # We just overwrite older files, so no conflicts are possible.
31 pass
32
33=== modified file 'lib/lp/archivepublisher/debian_installer.py'
34--- lib/lp/archivepublisher/debian_installer.py 2012-06-28 16:32:27 +0000
35+++ lib/lp/archivepublisher/debian_installer.py 2012-07-10 11:27:22 +0000
36@@ -42,8 +42,11 @@
37
38 @staticmethod
39 def parsePath(tarfile_path):
40- base, version, arch = os.path.basename(tarfile_path).split("_")
41- return base, version, arch.split(".")[0]
42+ tarfile_base = os.path.basename(tarfile_path)
43+ bits = tarfile_base.split("_")
44+ if len(bits) != 3:
45+ raise ValueError("%s is not BASE_VERSION_ARCH" % tarfile_base)
46+ return bits[0], bits[1], bits[2].split(".")[0]
47
48 def setTargetDirectory(self, pubconf, tarfile_path, distroseries):
49 _, self.version, self.arch = self.parsePath(tarfile_path)
50
51=== modified file 'lib/lp/archivepublisher/dist_upgrader.py'
52--- lib/lp/archivepublisher/dist_upgrader.py 2012-07-03 17:09:00 +0000
53+++ lib/lp/archivepublisher/dist_upgrader.py 2012-07-10 11:27:22 +0000
54@@ -59,8 +59,11 @@
55
56 @staticmethod
57 def parsePath(tarfile_path):
58- name, version, arch = os.path.basename(tarfile_path).split("_")
59- return name, version, arch.split(".")[0]
60+ tarfile_base = os.path.basename(tarfile_path)
61+ bits = tarfile_base.split("_")
62+ if len(bits) != 3:
63+ raise ValueError("%s is not NAME_VERSION_ARCH" % tarfile_base)
64+ return bits[0], bits[1], bits[2].split(".")[0]
65
66 def setTargetDirectory(self, pubconf, tarfile_path, distroseries):
67 _, self.version, self.arch = self.parsePath(tarfile_path)
68
69=== modified file 'lib/lp/archivepublisher/tests/test_ddtp_tarball.py'
70--- lib/lp/archivepublisher/tests/test_ddtp_tarball.py 2012-06-28 16:32:27 +0000
71+++ lib/lp/archivepublisher/tests/test_ddtp_tarball.py 2012-07-10 11:27:22 +0000
72@@ -9,7 +9,10 @@
73
74 import os
75
76-from lp.archivepublisher.ddtp_tarball import process_ddtp_tarball
77+from lp.archivepublisher.ddtp_tarball import (
78+ DdtpTarballUpload,
79+ process_ddtp_tarball,
80+ )
81 from lp.services.tarfile_helpers import LaunchpadWriteTarFile
82 from lp.testing import TestCase
83
84@@ -107,3 +110,31 @@
85 self.assertEqual("", ca_file.read())
86 self.assertEqual(1, os.stat(bn).st_nlink)
87 self.assertEqual(1, os.stat(ca).st_nlink)
88+
89+ def test_parsePath_handles_underscore_in_directory(self):
90+ # parsePath is not misled by an underscore in the directory name.
91+ self.assertEqual(
92+ # XXX cjwatson 2012-07-03: .tar.gz is not stripped off the end
93+ # of the version due to something of an ambiguity in the design;
94+ # how should translations_main_1.0.1.tar.gz be parsed? In
95+ # practice this doesn't matter because DdtpTarballUpload never
96+ # uses the version for anything.
97+ ("translations", "main", "1.tar.gz"),
98+ DdtpTarballUpload.parsePath(
99+ "/dir_with_underscores/translations_main_1.tar.gz"))
100+
101+ def test_getSeriesKey_extracts_component(self):
102+ # getSeriesKey extracts the component from an upload's filename.
103+ self.openArchive("20060728")
104+ self.assertEqual("main", DdtpTarballUpload.getSeriesKey(self.path))
105+
106+ def test_getSeriesKey_returns_None_on_mismatch(self):
107+ # getSeriesKey returns None if the filename does not match the
108+ # expected pattern.
109+ self.assertIsNone(DdtpTarballUpload.getSeriesKey("argh_1.0.jpg"))
110+
111+ def test_getSeriesKey_refuses_names_with_wrong_number_of_fields(self):
112+ # getSeriesKey requires exactly three fields.
113+ self.assertIsNone(DdtpTarballUpload.getSeriesKey("package_1.0.tar.gz"))
114+ self.assertIsNone(DdtpTarballUpload.getSeriesKey(
115+ "one_two_three_four_5.tar.gz"))
116
117=== modified file 'lib/lp/archivepublisher/uefi.py'
118--- lib/lp/archivepublisher/uefi.py 2012-06-28 16:32:27 +0000
119+++ lib/lp/archivepublisher/uefi.py 2012-07-10 11:27:22 +0000
120@@ -69,8 +69,11 @@
121
122 @staticmethod
123 def parsePath(tarfile_path):
124- loader_type, version, arch = os.path.basename(tarfile_path).split("_")
125- return loader_type, version, arch.split(".")[0]
126+ tarfile_base = os.path.basename(tarfile_path)
127+ bits = tarfile_base.split("_")
128+ if len(bits) != 3:
129+ raise ValueError("%s is not TYPE_VERSION_ARCH" % tarfile_base)
130+ return bits[0], bits[1], bits[2].split(".")[0]
131
132 def setTargetDirectory(self, pubconf, tarfile_path, distroseries):
133 if pubconf.uefiroot is None:
134
135=== modified file 'lib/lp/soyuz/scripts/custom_uploads_copier.py'
136--- lib/lp/soyuz/scripts/custom_uploads_copier.py 2012-07-03 17:09:00 +0000
137+++ lib/lp/soyuz/scripts/custom_uploads_copier.py 2012-07-10 11:27:22 +0000
138@@ -16,6 +16,7 @@
139
140 from zope.component import getUtility
141
142+from lp.archivepublisher.ddtp_tarball import DdtpTarballUpload
143 from lp.archivepublisher.debian_installer import DebianInstallerUpload
144 from lp.archivepublisher.dist_upgrader import DistUpgraderUpload
145 from lp.archivepublisher.uefi import UefiUpload
146@@ -41,6 +42,7 @@
147 copyable_types = {
148 PackageUploadCustomFormat.DEBIAN_INSTALLER: DebianInstallerUpload,
149 PackageUploadCustomFormat.DIST_UPGRADER: DistUpgraderUpload,
150+ PackageUploadCustomFormat.DDTP_TARBALL: DdtpTarballUpload,
151 PackageUploadCustomFormat.UEFI: UefiUpload,
152 }
153
154@@ -70,9 +72,6 @@
155
156 def getKey(self, upload):
157 """Get an indexing key for `upload`."""
158- # XXX JeroenVermeulen 2011-08-17, bug=827941: For ddtp
159- # translations tarballs, we'll have to include the component
160- # name as well.
161 custom_format = upload.customformat
162 series_key = self.extractSeriesKey(
163 self.copyable_types[custom_format],
164
165=== modified file 'lib/lp/soyuz/scripts/tests/test_custom_uploads_copier.py'
166--- lib/lp/soyuz/scripts/tests/test_custom_uploads_copier.py 2012-06-22 17:26:53 +0000
167+++ lib/lp/soyuz/scripts/tests/test_custom_uploads_copier.py 2012-07-10 11:27:22 +0000
168@@ -111,16 +111,22 @@
169
170 def makeUpload(self, distroseries=None, pocket=None,
171 custom_type=PackageUploadCustomFormat.DEBIAN_INSTALLER,
172- version=None, arch=None):
173+ version=None, arch=None, component=None):
174 """Create a `PackageUploadCustom`."""
175 if distroseries is None:
176 distroseries = self.factory.makeDistroSeries()
177 package_name = self.factory.getUniqueString("package")
178 if version is None:
179 version = self.makeVersion()
180- if arch is None:
181- arch = self.factory.getUniqueString()
182- filename = "%s.tar.gz" % '_'.join([package_name, version, arch])
183+ if custom_type == PackageUploadCustomFormat.DDTP_TARBALL:
184+ if component is None:
185+ component = self.factory.getUniqueString()
186+ filename = "%s.tar.gz" % "_".join(
187+ [package_name, component, version])
188+ else:
189+ if arch is None:
190+ arch = self.factory.getUniqueString()
191+ filename = "%s.tar.gz" % "_".join([package_name, version, arch])
192 package_upload = self.factory.makeCustomPackageUpload(
193 distroseries=distroseries, pocket=pocket, custom_type=custom_type,
194 filename=filename)
195@@ -230,9 +236,6 @@
196 def test_getKey_includes_format_and_architecture(self):
197 # The key returned by getKey consists of custom upload type,
198 # and architecture.
199- # XXX JeroenVermeulen 2011-08-17, bug=827941: To support
200- # ddtp-translations uploads, this will have to include the
201- # component name as well.
202 source_series = self.factory.makeDistroSeries()
203 upload = self.makeUpload(
204 source_series, custom_type=PackageUploadCustomFormat.DIST_UPGRADER,
205@@ -244,6 +247,20 @@
206 )
207 self.assertEqual(expected_key, copier.getKey(upload))
208
209+ def test_getKey_ddtp_includes_format_and_component(self):
210+ # The key returned by getKey for a ddtp-tarball upload consists of
211+ # custom upload type, and component.
212+ source_series = self.factory.makeDistroSeries()
213+ upload = self.makeUpload(
214+ source_series, custom_type=PackageUploadCustomFormat.DDTP_TARBALL,
215+ component='restricted')
216+ copier = CustomUploadsCopier(FakeDistroSeries())
217+ expected_key = (
218+ PackageUploadCustomFormat.DDTP_TARBALL,
219+ 'restricted',
220+ )
221+ self.assertEqual(expected_key, copier.getKey(upload))
222+
223 def test_getLatestUploads_indexes_uploads_by_key(self):
224 # getLatestUploads returns a dict of uploads, indexed by keys
225 # returned by getKey.