Merge lp:~stevenk/launchpad/destroy-pppd into lp:launchpad
- destroy-pppd
- Merge into devel
Proposed by
Steve Kowalik
Status: | Merged |
---|---|
Approved by: | William Grant |
Approved revision: | no longer in the source branch. |
Merged at revision: | 16719 |
Proposed branch: | lp:~stevenk/launchpad/destroy-pppd |
Merge into: | lp:launchpad |
Prerequisite: | lp:~stevenk/launchpad/packagediff-job |
Diff against target: |
666 lines (+52/-397) 10 files modified
cronscripts/process-pending-packagediffs.py (+0/-35) lib/lp/soyuz/doc/package-diff.txt (+2/-27) lib/lp/soyuz/interfaces/packagediff.py (+0/-8) lib/lp/soyuz/model/packagediff.py (+0/-6) lib/lp/soyuz/scripts/packagediff.py (+0/-62) lib/lp/soyuz/scripts/tests/test_processpendingpackagediffs.py (+0/-106) lib/lp/soyuz/stories/soyuz/xx-package-diff.txt (+1/-14) lib/lp/soyuz/tests/soyuz.py (+0/-81) lib/lp/soyuz/tests/test_packagediff.py (+48/-24) lib/lp/soyuz/tests/test_packagediffjob.py (+1/-34) |
To merge this branch: | bzr merge lp:~stevenk/launchpad/destroy-pppd |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+172945@code.launchpad.net |
Commit message
Destroy the process-
Description of the change
Destroy the process-
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
1 | === removed file 'cronscripts/process-pending-packagediffs.py' | |||
2 | --- cronscripts/process-pending-packagediffs.py 2013-01-07 02:40:55 +0000 | |||
3 | +++ cronscripts/process-pending-packagediffs.py 1970-01-01 00:00:00 +0000 | |||
4 | @@ -1,35 +0,0 @@ | |||
5 | 1 | #!/usr/bin/python -S | ||
6 | 2 | # | ||
7 | 3 | # Copyright 2009 Canonical Ltd. This software is licensed under the | ||
8 | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
9 | 5 | |||
10 | 6 | """Process pending PackageDiffs. | ||
11 | 7 | |||
12 | 8 | Process a optionally limited set of pending PackageDiffs. | ||
13 | 9 | |||
14 | 10 | By default it process up to 50 diffs each run, which is enough to catch | ||
15 | 11 | up on 1 hour of uploads (on average). | ||
16 | 12 | |||
17 | 13 | However users might benefit for more frequently runs since the diff ETA | ||
18 | 14 | relative to the upload will be shorter. | ||
19 | 15 | |||
20 | 16 | The cycle time needs to be balanced with the run time to produce the shortest | ||
21 | 17 | diff ETA and to not overlap much, for instance, if it has to diff monster | ||
22 | 18 | sources like openoffice or firefox. | ||
23 | 19 | |||
24 | 20 | Experiments with the cycle time will be safe enough and won't sink the host | ||
25 | 21 | performance, since the lock file is exclusive. | ||
26 | 22 | """ | ||
27 | 23 | |||
28 | 24 | __metaclass__ = type | ||
29 | 25 | |||
30 | 26 | import _pythonpath | ||
31 | 27 | |||
32 | 28 | from lp.services.config import config | ||
33 | 29 | from lp.soyuz.scripts.packagediff import ProcessPendingPackageDiffs | ||
34 | 30 | |||
35 | 31 | |||
36 | 32 | if __name__ == '__main__': | ||
37 | 33 | script = ProcessPendingPackageDiffs( | ||
38 | 34 | 'process-pending-packagediffs', dbuser=config.uploader.dbuser) | ||
39 | 35 | script.lock_and_run() | ||
40 | 36 | 0 | ||
41 | === modified file 'lib/lp/soyuz/doc/package-diff.txt' | |||
42 | --- lib/lp/soyuz/doc/package-diff.txt 2013-01-08 07:54:50 +0000 | |||
43 | +++ lib/lp/soyuz/doc/package-diff.txt 2013-07-31 00:41:29 +0000 | |||
44 | @@ -117,8 +117,7 @@ | |||
45 | 117 | Before starting let's enable the universe component and add the i386 | 117 | Before starting let's enable the universe component and add the i386 |
46 | 118 | chroot in hoary in order to be able to accept the NEW packages. | 118 | chroot in hoary in order to be able to accept the NEW packages. |
47 | 119 | 119 | ||
50 | 120 | >>> from lp.soyuz.model.component import ( | 120 | >>> from lp.soyuz.model.component import ComponentSelection |
49 | 121 | ... ComponentSelection) | ||
51 | 122 | >>> from lp.services.librarian.model import LibraryFileAlias | 121 | >>> from lp.services.librarian.model import LibraryFileAlias |
52 | 123 | >>> from lp.soyuz.interfaces.component import IComponentSet | 122 | >>> from lp.soyuz.interfaces.component import IComponentSet |
53 | 124 | 123 | ||
54 | @@ -154,8 +153,7 @@ | |||
55 | 154 | 153 | ||
56 | 155 | >>> packager.buildUpstream() | 154 | >>> packager.buildUpstream() |
57 | 156 | >>> packager.buildSource(signed=False) | 155 | >>> packager.buildSource(signed=False) |
60 | 157 | >>> biscuit_one_pub = packager.uploadSourceVersion( | 156 | >>> biscuit_one_pub = packager.uploadSourceVersion('1.0-1', policy='sync') |
59 | 158 | ... '1.0-1', policy='sync') | ||
61 | 159 | 157 | ||
62 | 160 | >>> len(biscuit_one_pub.sourcepackagerelease.package_diffs) | 158 | >>> len(biscuit_one_pub.sourcepackagerelease.package_diffs) |
63 | 161 | 0 | 159 | 0 |
64 | @@ -436,29 +434,6 @@ | |||
65 | 436 | biscuit diff from 1.0-1 to 1.0-8 True -6 | 434 | biscuit diff from 1.0-1 to 1.0-8 True -6 |
66 | 437 | pmount diff from 0.1-1 to 0.1-2 False -7 | 435 | pmount diff from 0.1-1 to 0.1-2 False -7 |
67 | 438 | 436 | ||
68 | 439 | Or only the PackageDiffs not yet fullfilled. | ||
69 | 440 | |||
70 | 441 | >>> print_diffs(packagediff_set.getPendingDiffs()) | ||
71 | 442 | pmount diff from 0.1-1 to 0.1-2 False 0 | ||
72 | 443 | biscuit diff from 1.0-8 to 1.0-9 False 2 | ||
73 | 444 | biscuit diff from 1.0-8 to 1.0-12 False 3 | ||
74 | 445 | biscuit diff from 1.0-9 to 1.0-10 False 4 | ||
75 | 446 | biscuit diff from 1.0-8 to 1.0-11 False 5 | ||
76 | 447 | biscuit diff from 1.0-8 (in Ubuntu) to 1.0-2 False 6 | ||
77 | 448 | biscuit diff from 1.0-2 to 1.0-3 False 7 | ||
78 | 449 | |||
79 | 450 | Note that the iteration over all PackageDiffs is sorted by descending | ||
80 | 451 | database ID, i.e. newest first, and getPendingDiffs() results are | ||
81 | 452 | ordered by ascending database IDs, oldest first. | ||
82 | 453 | |||
83 | 454 | getPendingDiffs() results can optionally be limited. | ||
84 | 455 | |||
85 | 456 | >>> packagediff_set.getPendingDiffs().count() | ||
86 | 457 | 7 | ||
87 | 458 | |||
88 | 459 | >>> packagediff_set.getPendingDiffs(limit=2).count() | ||
89 | 460 | 2 | ||
90 | 461 | |||
91 | 462 | All package diffs targeting a set of source package releases can also | 437 | All package diffs targeting a set of source package releases can also |
92 | 463 | be requested. The results are ordered by the source package release | 438 | be requested. The results are ordered by the source package release |
93 | 464 | ID: | 439 | ID: |
94 | 465 | 440 | ||
95 | === modified file 'lib/lp/soyuz/interfaces/packagediff.py' | |||
96 | --- lib/lp/soyuz/interfaces/packagediff.py 2013-01-07 02:40:55 +0000 | |||
97 | +++ lib/lp/soyuz/interfaces/packagediff.py 2013-07-31 00:41:29 +0000 | |||
98 | @@ -92,14 +92,6 @@ | |||
99 | 92 | def get(diff_id): | 92 | def get(diff_id): |
100 | 93 | """Retrieve a `PackageDiff` for the given id.""" | 93 | """Retrieve a `PackageDiff` for the given id.""" |
101 | 94 | 94 | ||
102 | 95 | def getPendingDiffs(limit=None): | ||
103 | 96 | """Return all pending `PackageDiff` records. | ||
104 | 97 | |||
105 | 98 | :param limit: optional results limitation. | ||
106 | 99 | |||
107 | 100 | :return a `SelectResult` ordered by id respecting the given limit. | ||
108 | 101 | """ | ||
109 | 102 | |||
110 | 103 | def getDiffsToReleases(sprs, preload_for_display=False): | 95 | def getDiffsToReleases(sprs, preload_for_display=False): |
111 | 104 | """Return all diffs that targetting a set of source package releases. | 96 | """Return all diffs that targetting a set of source package releases. |
112 | 105 | 97 | ||
113 | 106 | 98 | ||
114 | === modified file 'lib/lp/soyuz/model/packagediff.py' | |||
115 | --- lib/lp/soyuz/model/packagediff.py 2013-07-25 12:39:54 +0000 | |||
116 | +++ lib/lp/soyuz/model/packagediff.py 2013-07-31 00:41:29 +0000 | |||
117 | @@ -264,12 +264,6 @@ | |||
118 | 264 | """See `IPackageDiffSet`.""" | 264 | """See `IPackageDiffSet`.""" |
119 | 265 | return PackageDiff.get(diff_id) | 265 | return PackageDiff.get(diff_id) |
120 | 266 | 266 | ||
121 | 267 | def getPendingDiffs(self, limit=None): | ||
122 | 268 | return IStore(PackageDiff).find( | ||
123 | 269 | PackageDiff, | ||
124 | 270 | PackageDiff.status == PackageDiffStatus.PENDING).order_by( | ||
125 | 271 | PackageDiff.id).config(limit=limit) | ||
126 | 272 | |||
127 | 273 | def getDiffsToReleases(self, sprs, preload_for_display=False): | 267 | def getDiffsToReleases(self, sprs, preload_for_display=False): |
128 | 274 | """See `IPackageDiffSet`.""" | 268 | """See `IPackageDiffSet`.""" |
129 | 275 | from lp.registry.model.distribution import Distribution | 269 | from lp.registry.model.distribution import Distribution |
130 | 276 | 270 | ||
131 | === removed file 'lib/lp/soyuz/scripts/packagediff.py' | |||
132 | --- lib/lp/soyuz/scripts/packagediff.py 2010-08-20 20:31:18 +0000 | |||
133 | +++ lib/lp/soyuz/scripts/packagediff.py 1970-01-01 00:00:00 +0000 | |||
134 | @@ -1,62 +0,0 @@ | |||
135 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | ||
136 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
137 | 3 | |||
138 | 4 | """PackageDiff cronscript class.""" | ||
139 | 5 | |||
140 | 6 | __metaclass__ = type | ||
141 | 7 | |||
142 | 8 | __all__ = [ | ||
143 | 9 | 'ProcessPendingPackageDiffs', | ||
144 | 10 | ] | ||
145 | 11 | |||
146 | 12 | from zope.component import getUtility | ||
147 | 13 | |||
148 | 14 | from lp.services.scripts.base import ( | ||
149 | 15 | LaunchpadCronScript, | ||
150 | 16 | LaunchpadScriptFailure, | ||
151 | 17 | ) | ||
152 | 18 | from lp.soyuz.interfaces.packagediff import IPackageDiffSet | ||
153 | 19 | |||
154 | 20 | |||
155 | 21 | class ProcessPendingPackageDiffs(LaunchpadCronScript): | ||
156 | 22 | |||
157 | 23 | def add_my_options(self): | ||
158 | 24 | # 50 diffs seems to be more them enough to process all uploaded | ||
159 | 25 | # source packages for 1 hour (average upload rate) for ubuntu | ||
160 | 26 | # primary archive, security and PPAs in general. | ||
161 | 27 | self.parser.add_option( | ||
162 | 28 | "-l", "--limit", type="int", default=50, | ||
163 | 29 | help="Maximum number of requests to be processed in this run.") | ||
164 | 30 | |||
165 | 31 | self.parser.add_option( | ||
166 | 32 | "-n", "--dry-run", | ||
167 | 33 | dest="dryrun", action="store_true", default=False, | ||
168 | 34 | help="Whether or not to commit the transaction.") | ||
169 | 35 | |||
170 | 36 | def main(self): | ||
171 | 37 | """Process pending `PackageDiff` records. | ||
172 | 38 | |||
173 | 39 | Collect up to the maximum number of pending `PackageDiff` records | ||
174 | 40 | available and process them. | ||
175 | 41 | |||
176 | 42 | Processed diffs results are commited individually. | ||
177 | 43 | """ | ||
178 | 44 | if self.args: | ||
179 | 45 | raise LaunchpadScriptFailure("Unhandled arguments %r" % self.args) | ||
180 | 46 | |||
181 | 47 | packagediff_set = getUtility(IPackageDiffSet) | ||
182 | 48 | |||
183 | 49 | pending_diffs = packagediff_set.getPendingDiffs( | ||
184 | 50 | limit=self.options.limit) | ||
185 | 51 | self.logger.debug( | ||
186 | 52 | 'Considering %s diff requests' % pending_diffs.count()) | ||
187 | 53 | |||
188 | 54 | # Iterate over all pending packagediffs. | ||
189 | 55 | for packagediff in pending_diffs: | ||
190 | 56 | self.logger.debug( | ||
191 | 57 | 'Performing package diff for %s from %s' % ( | ||
192 | 58 | packagediff.from_source.name, packagediff.title)) | ||
193 | 59 | packagediff.performDiff() | ||
194 | 60 | if not self.options.dryrun: | ||
195 | 61 | self.logger.debug('Commiting the transaction.') | ||
196 | 62 | self.txn.commit() | ||
197 | 63 | 0 | ||
198 | === removed file 'lib/lp/soyuz/scripts/tests/test_processpendingpackagediffs.py' | |||
199 | --- lib/lp/soyuz/scripts/tests/test_processpendingpackagediffs.py 2012-01-01 02:58:52 +0000 | |||
200 | +++ lib/lp/soyuz/scripts/tests/test_processpendingpackagediffs.py 1970-01-01 00:00:00 +0000 | |||
201 | @@ -1,106 +0,0 @@ | |||
202 | 1 | # Copyright 2009-2010 Canonical Ltd. This software is licensed under the | ||
203 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
204 | 3 | |||
205 | 4 | __metaclass__ = type | ||
206 | 5 | |||
207 | 6 | import os | ||
208 | 7 | import subprocess | ||
209 | 8 | import sys | ||
210 | 9 | |||
211 | 10 | from lp.services.config import config | ||
212 | 11 | from lp.services.log.logger import BufferLogger | ||
213 | 12 | from lp.soyuz.scripts.packagediff import ProcessPendingPackageDiffs | ||
214 | 13 | from lp.soyuz.tests.soyuz import TestPackageDiffsBase | ||
215 | 14 | from lp.testing.layers import LaunchpadZopelessLayer | ||
216 | 15 | |||
217 | 16 | |||
218 | 17 | class TestProcessPendingPackageDiffsScript(TestPackageDiffsBase): | ||
219 | 18 | """Test the process-pending-packagediffs.py script.""" | ||
220 | 19 | layer = LaunchpadZopelessLayer | ||
221 | 20 | dbuser = config.uploader.dbuser | ||
222 | 21 | |||
223 | 22 | def runProcessPendingPackageDiffs(self, extra_args=None): | ||
224 | 23 | """Run process-pending-packagediffs.py. | ||
225 | 24 | |||
226 | 25 | Returns a tuple of the process's return code, stdout output and | ||
227 | 26 | stderr output.""" | ||
228 | 27 | if extra_args is None: | ||
229 | 28 | extra_args = [] | ||
230 | 29 | script = os.path.join( | ||
231 | 30 | config.root, "cronscripts", "process-pending-packagediffs.py") | ||
232 | 31 | args = [sys.executable, script] | ||
233 | 32 | args.extend(extra_args) | ||
234 | 33 | process = subprocess.Popen( | ||
235 | 34 | args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
236 | 35 | stdout, stderr = process.communicate() | ||
237 | 36 | return (process.returncode, stdout, stderr) | ||
238 | 37 | |||
239 | 38 | def testSimpleScriptRun(self): | ||
240 | 39 | """Try a simple processing-pending-packagediffs.py run.""" | ||
241 | 40 | returncode, out, err = self.runProcessPendingPackageDiffs() | ||
242 | 41 | if returncode != 0: | ||
243 | 42 | print "\nStdout:\n%s\nStderr\n%s\n" % (out, err) | ||
244 | 43 | self.assertEqual(0, returncode) | ||
245 | 44 | |||
246 | 45 | self.layer.txn.abort() | ||
247 | 46 | |||
248 | 47 | # The pending PackageDiff request was processed. | ||
249 | 48 | self.assertEqual(self.getPendingDiffs().count(), 0) | ||
250 | 49 | |||
251 | 50 | def getDiffProcessor(self, limit=None): | ||
252 | 51 | """Return a `ProcessPendingPackageDiffs` instance. | ||
253 | 52 | |||
254 | 53 | :param limit: if passed, it will be used as the 'limit' script | ||
255 | 54 | argument. | ||
256 | 55 | |||
257 | 56 | :return the initialized script object using `BufferLogger` and | ||
258 | 57 | the given parameters. | ||
259 | 58 | """ | ||
260 | 59 | test_args = [] | ||
261 | 60 | if limit is not None: | ||
262 | 61 | test_args.append('-l %s' % limit) | ||
263 | 62 | |||
264 | 63 | diff_processor = ProcessPendingPackageDiffs( | ||
265 | 64 | name='process-pending-packagediffs', test_args=test_args) | ||
266 | 65 | diff_processor.logger = BufferLogger() | ||
267 | 66 | diff_processor.txn = self.layer.txn | ||
268 | 67 | return diff_processor | ||
269 | 68 | |||
270 | 69 | def testSimpleRun(self): | ||
271 | 70 | """Simple run of the script class. | ||
272 | 71 | |||
273 | 72 | The only diff available is processed after its run. | ||
274 | 73 | """ | ||
275 | 74 | # Setup a DiffProcessor. | ||
276 | 75 | diff_processor = self.getDiffProcessor() | ||
277 | 76 | diff_processor.main() | ||
278 | 77 | |||
279 | 78 | # The pending PackageDiff request was processed. | ||
280 | 79 | # See doc/package-diff.txt for more information. | ||
281 | 80 | pending_diffs = self.getPendingDiffs() | ||
282 | 81 | self.assertEqual(pending_diffs.count(), 0) | ||
283 | 82 | |||
284 | 83 | def testLimitedRun(self): | ||
285 | 84 | """Run the script with a limited scope. | ||
286 | 85 | |||
287 | 86 | Check if a limited run of the script only processes up to 'limit' | ||
288 | 87 | pending diff records and exits. | ||
289 | 88 | """ | ||
290 | 89 | # Setup a DiffProcessor limited to one request per run. | ||
291 | 90 | diff_processor = self.getDiffProcessor(limit=1) | ||
292 | 91 | |||
293 | 92 | # Upload a new source version, so we have two pending PackageDiff | ||
294 | 93 | # records to process. | ||
295 | 94 | self.packager.buildVersion('1.0-3', changelog_text="biscuits") | ||
296 | 95 | self.packager.buildSource(include_orig=False) | ||
297 | 96 | self.packager.uploadSourceVersion('1.0-3', suite="warty-updates") | ||
298 | 97 | self.assertEqual(self.getPendingDiffs().count(), 2) | ||
299 | 98 | |||
300 | 99 | # The first processor run will process only one PackageDiff, | ||
301 | 100 | # the other will remain. | ||
302 | 101 | diff_processor.main() | ||
303 | 102 | self.assertEqual(self.getPendingDiffs().count(), 1) | ||
304 | 103 | |||
305 | 104 | # The next run process the remaining one. | ||
306 | 105 | diff_processor.main() | ||
307 | 106 | self.assertEqual(self.getPendingDiffs().count(), 0) | ||
308 | 107 | 0 | ||
309 | === modified file 'lib/lp/soyuz/stories/soyuz/xx-package-diff.txt' | |||
310 | --- lib/lp/soyuz/stories/soyuz/xx-package-diff.txt 2011-12-30 06:14:56 +0000 | |||
311 | +++ lib/lp/soyuz/stories/soyuz/xx-package-diff.txt 2013-07-31 00:41:29 +0000 | |||
312 | @@ -73,24 +73,13 @@ | |||
313 | 73 | >>> diff_one = biscuit_one_pub.sourcepackagerelease.requestDiffTo( | 73 | >>> diff_one = biscuit_one_pub.sourcepackagerelease.requestDiffTo( |
314 | 74 | ... requester=name16, | 74 | ... requester=name16, |
315 | 75 | ... to_sourcepackagerelease=biscuit_two_pub.sourcepackagerelease) | 75 | ... to_sourcepackagerelease=biscuit_two_pub.sourcepackagerelease) |
316 | 76 | |||
317 | 77 | >>> diff_two = biscuit_two_pub.sourcepackagerelease.requestDiffTo( | 76 | >>> diff_two = biscuit_two_pub.sourcepackagerelease.requestDiffTo( |
318 | 78 | ... requester=name16, | 77 | ... requester=name16, |
319 | 79 | ... to_sourcepackagerelease=biscuit_three_pub.sourcepackagerelease) | 78 | ... to_sourcepackagerelease=biscuit_three_pub.sourcepackagerelease) |
320 | 80 | |||
321 | 81 | >>> diff_three = biscuit_three_pub.sourcepackagerelease.requestDiffTo( | 79 | >>> diff_three = biscuit_three_pub.sourcepackagerelease.requestDiffTo( |
322 | 82 | ... requester=name16, | 80 | ... requester=name16, |
323 | 83 | ... to_sourcepackagerelease=biscuit_four_pub.sourcepackagerelease) | 81 | ... to_sourcepackagerelease=biscuit_four_pub.sourcepackagerelease) |
324 | 84 | 82 | ||
325 | 85 | >>> from lp.soyuz.interfaces.packagediff import IPackageDiffSet | ||
326 | 86 | >>> diff_set = getUtility(IPackageDiffSet) | ||
327 | 87 | >>> diffs = list(diff_set.getPendingDiffs()) | ||
328 | 88 | >>> for diff in diffs: | ||
329 | 89 | ... print diff.title | ||
330 | 90 | diff from 1.0-1 to 1.0-2 | ||
331 | 91 | diff from 1.0-2 to 1.0-3 | ||
332 | 92 | diff from 1.0-3 (in Ubuntu) to 1.0-4 | ||
333 | 93 | |||
334 | 94 | Perform some diffs in advance, the first diff in ubuntu and the diff | 83 | Perform some diffs in advance, the first diff in ubuntu and the diff |
335 | 95 | in PPA will be performed, the second diff in ubuntu will be performed | 84 | in PPA will be performed, the second diff in ubuntu will be performed |
336 | 96 | later. | 85 | later. |
337 | @@ -166,9 +155,7 @@ | |||
338 | 166 | the missing link is rendered. | 155 | the missing link is rendered. |
339 | 167 | 156 | ||
340 | 168 | >>> login('foo.bar@canonical.com') | 157 | >>> login('foo.bar@canonical.com') |
344 | 169 | >>> diff_set = getUtility(IPackageDiffSet) | 158 | >>> perform_fake_diff(diff_two, 'biscuit_1.0-2_1.0-3.diff.gz') |
342 | 170 | >>> [diff] = diff_set.getPendingDiffs() | ||
343 | 171 | >>> perform_fake_diff(diff, 'biscuit_1.0-2_1.0-3.diff.gz') | ||
345 | 172 | >>> transaction.commit() | 159 | >>> transaction.commit() |
346 | 173 | >>> logout() | 160 | >>> logout() |
347 | 174 | 161 | ||
348 | 175 | 162 | ||
349 | === modified file 'lib/lp/soyuz/tests/soyuz.py' | |||
350 | --- lib/lp/soyuz/tests/soyuz.py 2012-07-12 23:47:04 +0000 | |||
351 | +++ lib/lp/soyuz/tests/soyuz.py 2013-07-31 00:41:29 +0000 | |||
352 | @@ -7,36 +7,21 @@ | |||
353 | 7 | 7 | ||
354 | 8 | __all__ = [ | 8 | __all__ = [ |
355 | 9 | 'SoyuzTestHelper', | 9 | 'SoyuzTestHelper', |
356 | 10 | 'TestPackageDiffsBase', | ||
357 | 11 | ] | 10 | ] |
358 | 12 | 11 | ||
359 | 13 | import unittest | ||
360 | 14 | |||
361 | 15 | from zope.component import getUtility | 12 | from zope.component import getUtility |
362 | 16 | 13 | ||
363 | 17 | from lp.registry.interfaces.distribution import IDistributionSet | 14 | from lp.registry.interfaces.distribution import IDistributionSet |
364 | 18 | from lp.registry.interfaces.person import IPersonSet | 15 | from lp.registry.interfaces.person import IPersonSet |
365 | 19 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 16 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
366 | 20 | from lp.services.config import config | ||
367 | 21 | from lp.services.librarian.model import LibraryFileAlias | ||
368 | 22 | from lp.soyuz.enums import PackagePublishingStatus | 17 | from lp.soyuz.enums import PackagePublishingStatus |
369 | 23 | from lp.soyuz.interfaces.packagediff import IPackageDiffSet | ||
370 | 24 | from lp.soyuz.model.publishing import ( | 18 | from lp.soyuz.model.publishing import ( |
371 | 25 | BinaryPackagePublishingHistory, | 19 | BinaryPackagePublishingHistory, |
372 | 26 | SourcePackagePublishingHistory, | 20 | SourcePackagePublishingHistory, |
373 | 27 | ) | 21 | ) |
374 | 28 | from lp.soyuz.tests.fakepackager import FakePackager | ||
375 | 29 | from lp.testing.dbuser import dbuser | ||
376 | 30 | from lp.testing.gpgkeys import import_public_test_keys | ||
377 | 31 | from lp.testing.layers import LaunchpadZopelessLayer | ||
378 | 32 | from lp.testing.sampledata import ( | 22 | from lp.testing.sampledata import ( |
379 | 33 | BUILDD_ADMIN_USERNAME, | 23 | BUILDD_ADMIN_USERNAME, |
380 | 34 | CHROOT_LIBRARYFILEALIAS, | ||
381 | 35 | I386_ARCHITECTURE_NAME, | ||
382 | 36 | LAUNCHPAD_DBUSER_NAME, | ||
383 | 37 | UBUNTU_DISTRIBUTION_NAME, | 24 | UBUNTU_DISTRIBUTION_NAME, |
384 | 38 | WARTY_DISTROSERIES_NAME, | ||
385 | 39 | WARTY_UPDATES_SUITE_NAME, | ||
386 | 40 | ) | 25 | ) |
387 | 41 | 26 | ||
388 | 42 | 27 | ||
389 | @@ -137,69 +122,3 @@ | |||
390 | 137 | Return True if the lists matches, otherwise False. | 122 | Return True if the lists matches, otherwise False. |
391 | 138 | """ | 123 | """ |
392 | 139 | return [p.id for p in expected] == [r.id for r in given] | 124 | return [p.id for p in expected] == [r.id for r in given] |
393 | 140 | |||
394 | 141 | |||
395 | 142 | class TestPackageDiffsBase(unittest.TestCase): | ||
396 | 143 | """Base class facilitating tests related to package diffs.""" | ||
397 | 144 | layer = LaunchpadZopelessLayer | ||
398 | 145 | dbuser = config.uploader.dbuser | ||
399 | 146 | |||
400 | 147 | def setUp(self): | ||
401 | 148 | """Setup proper DB connection and contents for tests | ||
402 | 149 | |||
403 | 150 | Connect to the DB as the 'uploader' user (same user used in the | ||
404 | 151 | script), upload the test packages (see `uploadTestPackages`) and | ||
405 | 152 | commit the transaction. | ||
406 | 153 | |||
407 | 154 | Store the `FakePackager` object used in the test uploads as `packager` | ||
408 | 155 | so the tests can reuse it if necessary. | ||
409 | 156 | """ | ||
410 | 157 | super(TestPackageDiffsBase, self).setUp() | ||
411 | 158 | with dbuser(LAUNCHPAD_DBUSER_NAME): | ||
412 | 159 | fake_chroot = LibraryFileAlias.get(CHROOT_LIBRARYFILEALIAS) | ||
413 | 160 | ubuntu = getUtility(IDistributionSet).getByName( | ||
414 | 161 | UBUNTU_DISTRIBUTION_NAME) | ||
415 | 162 | warty = ubuntu.getSeries(WARTY_DISTROSERIES_NAME) | ||
416 | 163 | warty[I386_ARCHITECTURE_NAME].addOrUpdateChroot(fake_chroot) | ||
417 | 164 | |||
418 | 165 | self.packager = self.uploadTestPackages() | ||
419 | 166 | self.layer.txn.commit() | ||
420 | 167 | |||
421 | 168 | def uploadTestPackages(self): | ||
422 | 169 | """Upload packages for testing `PackageDiff` generation script. | ||
423 | 170 | |||
424 | 171 | Upload zeca_1.0-1 and zeca_1.0-2 sources, so a `PackageDiff` between | ||
425 | 172 | them is created. | ||
426 | 173 | |||
427 | 174 | Assert there is not pending `PackageDiff` in the DB before uploading | ||
428 | 175 | the package and also assert that there is one after the uploads. | ||
429 | 176 | |||
430 | 177 | :return: the FakePackager object used to generate and upload the test, | ||
431 | 178 | packages, so the tests can upload subsequent version if necessary. | ||
432 | 179 | """ | ||
433 | 180 | # No pending PackageDiff available in sampledata. | ||
434 | 181 | self.assertEqual(self.getPendingDiffs().count(), 0) | ||
435 | 182 | |||
436 | 183 | import_public_test_keys() | ||
437 | 184 | # Use FakePackager to upload a base package to ubuntu. | ||
438 | 185 | packager = FakePackager( | ||
439 | 186 | 'zeca', '1.0', 'foo.bar@canonical.com-passwordless.sec') | ||
440 | 187 | packager.buildUpstream() | ||
441 | 188 | packager.buildSource() | ||
442 | 189 | packager.uploadSourceVersion('1.0-1', suite=WARTY_UPDATES_SUITE_NAME) | ||
443 | 190 | |||
444 | 191 | # Upload a new version of the source, so a PackageDiff can | ||
445 | 192 | # be created. | ||
446 | 193 | packager.buildVersion('1.0-2', changelog_text="cookies") | ||
447 | 194 | packager.buildSource(include_orig=False) | ||
448 | 195 | packager.uploadSourceVersion('1.0-2', suite=WARTY_UPDATES_SUITE_NAME) | ||
449 | 196 | |||
450 | 197 | # Check if there is exactly one pending PackageDiff record and | ||
451 | 198 | # It's the one we have just created. | ||
452 | 199 | self.assertEqual(self.getPendingDiffs().count(), 1) | ||
453 | 200 | |||
454 | 201 | return packager | ||
455 | 202 | |||
456 | 203 | def getPendingDiffs(self): | ||
457 | 204 | """Pending `PackageDiff` available.""" | ||
458 | 205 | return getUtility(IPackageDiffSet).getPendingDiffs() | ||
459 | 206 | 125 | ||
460 | === modified file 'lib/lp/soyuz/tests/test_packagediff.py' | |||
461 | --- lib/lp/soyuz/tests/test_packagediff.py 2013-07-23 17:57:52 +0000 | |||
462 | +++ lib/lp/soyuz/tests/test_packagediff.py 2013-07-31 00:41:29 +0000 | |||
463 | @@ -6,7 +6,9 @@ | |||
464 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
465 | 7 | 7 | ||
466 | 8 | from datetime import datetime | 8 | from datetime import datetime |
467 | 9 | import os.path | ||
468 | 9 | 10 | ||
469 | 11 | import transaction | ||
470 | 10 | from zope.security.proxy import removeSecurityProxy | 12 | from zope.security.proxy import removeSecurityProxy |
471 | 11 | 13 | ||
472 | 12 | from lp.services.config import config | 14 | from lp.services.config import config |
473 | @@ -16,13 +18,44 @@ | |||
474 | 16 | from lp.services.job.model.job import Job | 18 | from lp.services.job.model.job import Job |
475 | 17 | from lp.soyuz.enums import PackageDiffStatus | 19 | from lp.soyuz.enums import PackageDiffStatus |
476 | 18 | from lp.soyuz.model.archive import Archive | 20 | from lp.soyuz.model.archive import Archive |
477 | 19 | from lp.soyuz.tests.soyuz import TestPackageDiffsBase | ||
478 | 20 | from lp.testing import TestCaseWithFactory | 21 | from lp.testing import TestCaseWithFactory |
479 | 21 | from lp.testing.dbuser import dbuser | 22 | from lp.testing.dbuser import dbuser |
480 | 22 | from lp.testing.layers import LaunchpadZopelessLayer | 23 | from lp.testing.layers import LaunchpadZopelessLayer |
481 | 23 | 24 | ||
482 | 24 | 25 | ||
484 | 25 | class TestPackageDiffs(TestPackageDiffsBase, TestCaseWithFactory): | 26 | def create_proper_job(factory): |
485 | 27 | archive = factory.makeArchive() | ||
486 | 28 | foo_dash1 = factory.makeSourcePackageRelease(archive=archive) | ||
487 | 29 | foo_dash15 = factory.makeSourcePackageRelease(archive=archive) | ||
488 | 30 | suite_dir = 'lib/lp/archiveuploader/tests/data/suite' | ||
489 | 31 | files = { | ||
490 | 32 | '%s/foo_1.0-1/foo_1.0-1.diff.gz' % suite_dir: None, | ||
491 | 33 | '%s/foo_1.0-1/foo_1.0-1.dsc' % suite_dir: None, | ||
492 | 34 | '%s/foo_1.0-1/foo_1.0.orig.tar.gz' % suite_dir: None, | ||
493 | 35 | '%s/foo_1.0-1.5/foo_1.0-1.5.diff.gz' % suite_dir: None, | ||
494 | 36 | '%s/foo_1.0-1.5/foo_1.0-1.5.dsc' % suite_dir: None} | ||
495 | 37 | for name in files: | ||
496 | 38 | filename = os.path.split(name)[-1] | ||
497 | 39 | with open(name, 'r') as content: | ||
498 | 40 | files[name] = factory.makeLibraryFileAlias( | ||
499 | 41 | filename=filename, content=content.read()) | ||
500 | 42 | transaction.commit() | ||
501 | 43 | dash1_files = ( | ||
502 | 44 | '%s/foo_1.0-1/foo_1.0-1.diff.gz' % suite_dir, | ||
503 | 45 | '%s/foo_1.0-1/foo_1.0-1.dsc' % suite_dir, | ||
504 | 46 | '%s/foo_1.0-1/foo_1.0.orig.tar.gz' % suite_dir) | ||
505 | 47 | dash15_files = ( | ||
506 | 48 | '%s/foo_1.0-1/foo_1.0.orig.tar.gz' % suite_dir, | ||
507 | 49 | '%s/foo_1.0-1.5/foo_1.0-1.5.diff.gz' % suite_dir, | ||
508 | 50 | '%s/foo_1.0-1.5/foo_1.0-1.5.dsc' % suite_dir) | ||
509 | 51 | for name in dash1_files: | ||
510 | 52 | foo_dash1.addFile(files[name]) | ||
511 | 53 | for name in dash15_files: | ||
512 | 54 | foo_dash15.addFile(files[name]) | ||
513 | 55 | return foo_dash1.requestDiffTo(factory.makePerson(), foo_dash15) | ||
514 | 56 | |||
515 | 57 | |||
516 | 58 | class TestPackageDiffs(TestCaseWithFactory): | ||
517 | 26 | """Test package diffs.""" | 59 | """Test package diffs.""" |
518 | 27 | layer = LaunchpadZopelessLayer | 60 | layer = LaunchpadZopelessLayer |
519 | 28 | dbuser = config.uploader.dbuser | 61 | dbuser = config.uploader.dbuser |
520 | @@ -30,7 +63,7 @@ | |||
521 | 30 | def test_packagediff_working(self): | 63 | def test_packagediff_working(self): |
522 | 31 | # Test the case where none of the files required for the diff are | 64 | # Test the case where none of the files required for the diff are |
523 | 32 | # expired in the librarian and where everything works as expected. | 65 | # expired in the librarian and where everything works as expected. |
525 | 33 | [diff] = self.getPendingDiffs() | 66 | diff = create_proper_job(self.factory) |
526 | 34 | self.assertEqual(0, removeSecurityProxy(diff)._countDeletedLFAs()) | 67 | self.assertEqual(0, removeSecurityProxy(diff)._countDeletedLFAs()) |
527 | 35 | diff.performDiff() | 68 | diff.performDiff() |
528 | 36 | self.assertEqual(PackageDiffStatus.COMPLETED, diff.status) | 69 | self.assertEqual(PackageDiffStatus.COMPLETED, diff.status) |
529 | @@ -39,11 +72,7 @@ | |||
530 | 39 | """Expire the files associated with the given source package in the | 72 | """Expire the files associated with the given source package in the |
531 | 40 | librarian.""" | 73 | librarian.""" |
532 | 41 | assert expire or delete | 74 | assert expire or delete |
538 | 42 | store = IStore(Archive) | 75 | query = "UPDATE LibraryFileAlias lfa SET " |
534 | 43 | query = """ | ||
535 | 44 | UPDATE LibraryFileAlias lfa | ||
536 | 45 | SET | ||
537 | 46 | """ | ||
539 | 47 | if expire: | 76 | if expire: |
540 | 48 | query += "expires = %s" % sqlvalues(datetime.utcnow()) | 77 | query += "expires = %s" % sqlvalues(datetime.utcnow()) |
541 | 49 | if expire and delete: | 78 | if expire and delete: |
542 | @@ -59,45 +88,36 @@ | |||
543 | 59 | AND sprf.libraryfile = lfa.id | 88 | AND sprf.libraryfile = lfa.id |
544 | 60 | """ % sqlvalues(source.id) | 89 | """ % sqlvalues(source.id) |
545 | 61 | with dbuser('launchpad'): | 90 | with dbuser('launchpad'): |
547 | 62 | store.execute(query) | 91 | IStore(Archive).execute(query) |
548 | 63 | 92 | ||
549 | 64 | def test_packagediff_with_expired_and_deleted_lfas(self): | 93 | def test_packagediff_with_expired_and_deleted_lfas(self): |
550 | 65 | # Test the case where files required for the diff are expired *and* | 94 | # Test the case where files required for the diff are expired *and* |
551 | 66 | # deleted in the librarian causing a package diff failure. | 95 | # deleted in the librarian causing a package diff failure. |
555 | 67 | [diff] = self.getPendingDiffs() | 96 | diff = create_proper_job(self.factory) |
553 | 68 | # Expire and delete the files associated with the 'from_source' | ||
554 | 69 | # package. | ||
556 | 70 | self.expireLFAsForSource(diff.from_source) | 97 | self.expireLFAsForSource(diff.from_source) |
559 | 71 | # The helper method now finds 3 expired files. | 98 | self.assertEqual(4, removeSecurityProxy(diff)._countDeletedLFAs()) |
558 | 72 | self.assertEqual(3, removeSecurityProxy(diff)._countDeletedLFAs()) | ||
560 | 73 | diff.performDiff() | 99 | diff.performDiff() |
561 | 74 | # The diff fails due to the presence of expired files. | ||
562 | 75 | self.assertEqual(PackageDiffStatus.FAILED, diff.status) | 100 | self.assertEqual(PackageDiffStatus.FAILED, diff.status) |
563 | 76 | 101 | ||
564 | 77 | def test_packagediff_with_expired_but_not_deleted_lfas(self): | 102 | def test_packagediff_with_expired_but_not_deleted_lfas(self): |
565 | 78 | # Test the case where files required for the diff are expired but | 103 | # Test the case where files required for the diff are expired but |
566 | 79 | # not deleted in the librarian still allowing the package diff to be | 104 | # not deleted in the librarian still allowing the package diff to be |
567 | 80 | # performed. | 105 | # performed. |
569 | 81 | [diff] = self.getPendingDiffs() | 106 | diff = create_proper_job(self.factory) |
570 | 82 | # Expire but don't delete the files associated with the 'from_source' | 107 | # Expire but don't delete the files associated with the 'from_source' |
571 | 83 | # package. | 108 | # package. |
572 | 84 | self.expireLFAsForSource(diff.from_source, expire=True, delete=False) | 109 | self.expireLFAsForSource(diff.from_source, expire=True, delete=False) |
573 | 85 | # The helper method now finds no expired files. | ||
574 | 86 | self.assertEqual(0, removeSecurityProxy(diff)._countDeletedLFAs()) | 110 | self.assertEqual(0, removeSecurityProxy(diff)._countDeletedLFAs()) |
575 | 87 | diff.performDiff() | 111 | diff.performDiff() |
576 | 88 | # The diff succeeds as expected. | ||
577 | 89 | self.assertEqual(PackageDiffStatus.COMPLETED, diff.status) | 112 | self.assertEqual(PackageDiffStatus.COMPLETED, diff.status) |
578 | 90 | 113 | ||
579 | 91 | def test_packagediff_with_deleted_but_not_expired_lfas(self): | 114 | def test_packagediff_with_deleted_but_not_expired_lfas(self): |
580 | 92 | # Test the case where files required for the diff have been | 115 | # Test the case where files required for the diff have been |
581 | 93 | # deleted explicitly, not through expiry. | 116 | # deleted explicitly, not through expiry. |
584 | 94 | [diff] = self.getPendingDiffs() | 117 | diff = create_proper_job(self.factory) |
583 | 95 | # Delete the files associated with the 'from_source' package. | ||
585 | 96 | self.expireLFAsForSource(diff.from_source, expire=False, delete=True) | 118 | self.expireLFAsForSource(diff.from_source, expire=False, delete=True) |
588 | 97 | # The helper method now finds 3 expired files. | 119 | self.assertEqual(4, removeSecurityProxy(diff)._countDeletedLFAs()) |
587 | 98 | self.assertEqual(3, removeSecurityProxy(diff)._countDeletedLFAs()) | ||
589 | 99 | diff.performDiff() | 120 | diff.performDiff() |
590 | 100 | # The diff fails due to the presence of expired files. | ||
591 | 101 | self.assertEqual(PackageDiffStatus.FAILED, diff.status) | 121 | self.assertEqual(PackageDiffStatus.FAILED, diff.status) |
592 | 102 | 122 | ||
593 | 103 | def test_packagediff_private_with_copied_spr(self): | 123 | def test_packagediff_private_with_copied_spr(self): |
594 | @@ -128,7 +148,11 @@ | |||
595 | 128 | self.assertFalse(diff.private) | 148 | self.assertFalse(diff.private) |
596 | 129 | 149 | ||
597 | 130 | def test_job_created(self): | 150 | def test_job_created(self): |
599 | 131 | # The setup code already creates a packagediff. | 151 | # Requesting a package diff creates a PackageDiffJob. |
600 | 152 | ppa = self.factory.makeArchive() | ||
601 | 153 | from_spr = self.factory.makeSourcePackageRelease(archive=ppa) | ||
602 | 154 | to_spr = self.factory.makeSourcePackageRelease(archive=ppa) | ||
603 | 155 | from_spr.requestDiffTo(ppa.owner, to_spr) | ||
604 | 132 | [job] = IStore(Job).find( | 156 | [job] = IStore(Job).find( |
605 | 133 | Job, Job.base_job_type == JobType.GENERATE_PACKAGE_DIFF) | 157 | Job, Job.base_job_type == JobType.GENERATE_PACKAGE_DIFF) |
606 | 134 | self.assertIsNot(None, job) | 158 | self.assertIsNot(None, job) |
607 | 135 | 159 | ||
608 | === modified file 'lib/lp/soyuz/tests/test_packagediffjob.py' | |||
609 | --- lib/lp/soyuz/tests/test_packagediffjob.py 2013-07-23 17:57:52 +0000 | |||
610 | +++ lib/lp/soyuz/tests/test_packagediffjob.py 2013-07-31 00:41:29 +0000 | |||
611 | @@ -3,8 +3,6 @@ | |||
612 | 3 | 3 | ||
613 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
614 | 5 | 5 | ||
615 | 6 | import os.path | ||
616 | 7 | |||
617 | 8 | from testtools.content import text_content | 6 | from testtools.content import text_content |
618 | 9 | import transaction | 7 | import transaction |
619 | 10 | from zope.component import getUtility | 8 | from zope.component import getUtility |
620 | @@ -19,6 +17,7 @@ | |||
621 | 19 | IPackageDiffJobSource, | 17 | IPackageDiffJobSource, |
622 | 20 | ) | 18 | ) |
623 | 21 | from lp.soyuz.model.packagediffjob import PackageDiffJob | 19 | from lp.soyuz.model.packagediffjob import PackageDiffJob |
624 | 20 | from lp.soyuz.tests.test_packagediff import create_proper_job | ||
625 | 22 | from lp.services.features.testing import FeatureFixture | 21 | from lp.services.features.testing import FeatureFixture |
626 | 23 | from lp.services.job.interfaces.job import JobStatus | 22 | from lp.services.job.interfaces.job import JobStatus |
627 | 24 | from lp.testing import ( | 23 | from lp.testing import ( |
628 | @@ -34,38 +33,6 @@ | |||
629 | 34 | ) | 33 | ) |
630 | 35 | 34 | ||
631 | 36 | 35 | ||
632 | 37 | def create_proper_job(factory): | ||
633 | 38 | archive = factory.makeArchive() | ||
634 | 39 | foo_dash1 = factory.makeSourcePackageRelease(archive=archive) | ||
635 | 40 | foo_dash15 = factory.makeSourcePackageRelease(archive=archive) | ||
636 | 41 | suite_dir = 'lib/lp/archiveuploader/tests/data/suite' | ||
637 | 42 | files = { | ||
638 | 43 | '%s/foo_1.0-1/foo_1.0-1.diff.gz' % suite_dir: None, | ||
639 | 44 | '%s/foo_1.0-1/foo_1.0-1.dsc' % suite_dir: None, | ||
640 | 45 | '%s/foo_1.0-1/foo_1.0.orig.tar.gz' % suite_dir: None, | ||
641 | 46 | '%s/foo_1.0-1.5/foo_1.0-1.5.diff.gz' % suite_dir: None, | ||
642 | 47 | '%s/foo_1.0-1.5/foo_1.0-1.5.dsc' % suite_dir: None} | ||
643 | 48 | for name in files: | ||
644 | 49 | filename = os.path.split(name)[-1] | ||
645 | 50 | with open(name, 'r') as content: | ||
646 | 51 | files[name] = factory.makeLibraryFileAlias( | ||
647 | 52 | filename=filename, content=content.read()) | ||
648 | 53 | transaction.commit() | ||
649 | 54 | dash1_files = ( | ||
650 | 55 | '%s/foo_1.0-1/foo_1.0-1.diff.gz' % suite_dir, | ||
651 | 56 | '%s/foo_1.0-1/foo_1.0-1.dsc' % suite_dir, | ||
652 | 57 | '%s/foo_1.0-1/foo_1.0.orig.tar.gz' % suite_dir) | ||
653 | 58 | dash15_files = ( | ||
654 | 59 | '%s/foo_1.0-1/foo_1.0.orig.tar.gz' % suite_dir, | ||
655 | 60 | '%s/foo_1.0-1.5/foo_1.0-1.5.diff.gz' % suite_dir, | ||
656 | 61 | '%s/foo_1.0-1.5/foo_1.0-1.5.dsc' % suite_dir) | ||
657 | 62 | for name in dash1_files: | ||
658 | 63 | foo_dash1.addFile(files[name]) | ||
659 | 64 | for name in dash15_files: | ||
660 | 65 | foo_dash15.addFile(files[name]) | ||
661 | 66 | return foo_dash1.requestDiffTo(factory.makePerson(), foo_dash15) | ||
662 | 67 | |||
663 | 68 | |||
664 | 69 | class TestPackageDiffJob(TestCaseWithFactory): | 36 | class TestPackageDiffJob(TestCaseWithFactory): |
665 | 70 | 37 | ||
666 | 71 | layer = LaunchpadZopelessLayer | 38 | layer = LaunchpadZopelessLayer |