Merge lp:~cjwatson/launchpad/refactor-run-parts-subprocess into lp:launchpad
- refactor-run-parts-subprocess
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 18552 |
Proposed branch: | lp:~cjwatson/launchpad/refactor-run-parts-subprocess |
Merge into: | lp:launchpad |
Diff against target: |
723 lines (+257/-289) 4 files modified
lib/lp/archivepublisher/run_parts.py (+87/-0) lib/lp/archivepublisher/scripts/publish_ftpmaster.py (+21/-116) lib/lp/archivepublisher/tests/test_publish_ftpmaster.py (+36/-173) lib/lp/archivepublisher/tests/test_run_parts.py (+113/-0) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/refactor-run-parts-subprocess |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+336298@code.launchpad.net |
Commit message
Refactor publish-ftpmaster to use subprocess.call rather than os.system.
Description of the change
As usual, avoiding unnecessary use of the shell makes things safer and simpler.
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 | === added file 'lib/lp/archivepublisher/run_parts.py' |
2 | --- lib/lp/archivepublisher/run_parts.py 1970-01-01 00:00:00 +0000 |
3 | +++ lib/lp/archivepublisher/run_parts.py 2018-01-18 15:33:21 +0000 |
4 | @@ -0,0 +1,87 @@ |
5 | +# Copyright 2011-2018 Canonical Ltd. This software is licensed under the |
6 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
7 | + |
8 | +"""Publisher support for running programs from a plug-in directory.""" |
9 | + |
10 | +from __future__ import absolute_import, print_function, unicode_literals |
11 | + |
12 | +__metaclass__ = type |
13 | +__all__ = [ |
14 | + 'execute_subprocess', |
15 | + 'find_run_parts_dir', |
16 | + 'run_parts', |
17 | + ] |
18 | + |
19 | +import os |
20 | +try: |
21 | + from shlex import quote as shell_quote |
22 | +except ImportError: |
23 | + from pipes import quote as shell_quote |
24 | +import subprocess |
25 | + |
26 | +from lp.services.config import config |
27 | +from lp.services.scripts.base import LaunchpadScriptFailure |
28 | +from lp.services.utils import file_exists |
29 | + |
30 | + |
31 | +def find_run_parts_dir(distribution_name, parts): |
32 | + """Find the requested run-parts directory, if it exists.""" |
33 | + run_parts_location = config.archivepublisher.run_parts_location |
34 | + if not run_parts_location: |
35 | + return None |
36 | + |
37 | + parts_dir = os.path.join(run_parts_location, distribution_name, parts) |
38 | + if file_exists(parts_dir): |
39 | + return parts_dir |
40 | + else: |
41 | + return None |
42 | + |
43 | + |
44 | +def execute_subprocess(args, log=None, failure=None, **kwargs): |
45 | + """Run `args`, handling logging and failures. |
46 | + |
47 | + :param args: Program argument array. |
48 | + :param log: An optional logger. |
49 | + :param failure: Raise `failure` as an exception if the command returns a |
50 | + nonzero value. If omitted, nonzero return values are ignored. |
51 | + :param kwargs: Other keyword arguments passed on to `subprocess.call`. |
52 | + """ |
53 | + if log is not None: |
54 | + log.debug("Executing: %s", " ".join(shell_quote(arg) for arg in args)) |
55 | + retval = subprocess.call(args, **kwargs) |
56 | + if retval != 0: |
57 | + if log is not None: |
58 | + log.debug("Command returned %d.", retval) |
59 | + if failure is not None: |
60 | + if log is not None: |
61 | + log.debug("Command failed: %s", failure) |
62 | + raise failure |
63 | + |
64 | + |
65 | +def run_parts(distribution_name, parts, log=None, env=None): |
66 | + """Execute run-parts. |
67 | + |
68 | + :param distribution_name: The name of the distribution to execute |
69 | + run-parts scripts for. |
70 | + :param parts: The run-parts directory to execute: |
71 | + "publish-distro.d" or "finalize.d". |
72 | + :param log: An optional logger. |
73 | + :param env: A dict of additional environment variables to pass to the |
74 | + scripts in the run-parts directory, or None. |
75 | + """ |
76 | + parts_dir = find_run_parts_dir(distribution_name, parts) |
77 | + if parts_dir is None: |
78 | + if log is not None: |
79 | + log.debug("Skipping run-parts %s: not configured.", parts) |
80 | + return |
81 | + cmd = ["run-parts", "--", parts_dir] |
82 | + failure = LaunchpadScriptFailure( |
83 | + "Failure while executing run-parts %s." % parts_dir) |
84 | + full_env = dict(os.environ) |
85 | + if env is not None: |
86 | + full_env.update(env) |
87 | + scripts_dir = os.path.join(config.root, "cronscripts", "publishing") |
88 | + path_elements = full_env.get("PATH", "").split(os.pathsep) |
89 | + path_elements.append(scripts_dir) |
90 | + full_env["PATH"] = os.pathsep.join(path_elements) |
91 | + execute_subprocess(cmd, log=None, failure=failure, env=full_env) |
92 | |
93 | === modified file 'lib/lp/archivepublisher/scripts/publish_ftpmaster.py' |
94 | --- lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2016-12-05 22:16:25 +0000 |
95 | +++ lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2018-01-18 15:33:21 +0000 |
96 | @@ -1,4 +1,4 @@ |
97 | -# Copyright 2011-2016 Canonical Ltd. This software is licensed under the |
98 | +# Copyright 2011-2018 Canonical Ltd. This software is licensed under the |
99 | # GNU Affero General Public License version 3 (see the file LICENSE). |
100 | |
101 | """Master distro publishing script.""" |
102 | @@ -22,6 +22,10 @@ |
103 | cannot_modify_suite, |
104 | GLOBAL_PUBLISHER_LOCK, |
105 | ) |
106 | +from lp.archivepublisher.run_parts import ( |
107 | + execute_subprocess, |
108 | + run_parts, |
109 | + ) |
110 | from lp.archivepublisher.scripts.processaccepted import ProcessAccepted |
111 | from lp.archivepublisher.scripts.publishdistro import PublishDistro |
112 | from lp.registry.interfaces.distribution import IDistributionSet |
113 | @@ -30,7 +34,6 @@ |
114 | pocketsuffix, |
115 | ) |
116 | from lp.registry.interfaces.series import SeriesStatus |
117 | -from lp.services.config import config |
118 | from lp.services.database.bulk import load_related |
119 | from lp.services.osutils import ensure_directory_exists |
120 | from lp.services.scripts.base import ( |
121 | @@ -61,43 +64,6 @@ |
122 | if archive.purpose in ARCHIVES_TO_PUBLISH] |
123 | |
124 | |
125 | -def compose_shell_boolean(boolean_value): |
126 | - """Represent a boolean value as "yes" or "no".""" |
127 | - boolean_text = { |
128 | - True: "yes", |
129 | - False: "no", |
130 | - } |
131 | - return boolean_text[boolean_value] |
132 | - |
133 | - |
134 | -def shell_quote(literal): |
135 | - """Escape `literal` for use in a double-quoted shell string. |
136 | - |
137 | - This is a pretty naive substitution: it doesn't deal well with |
138 | - non-ASCII characters or special characters. |
139 | - """ |
140 | - # Characters that need backslash-escaping. Do the backslash itself |
141 | - # first; any escapes we introduced *before* the backslash would have |
142 | - # their own backslashes escaped afterwards when we got to the |
143 | - # backslash itself. |
144 | - special_characters = '\\"$`\n' |
145 | - for escapee in special_characters: |
146 | - literal = literal.replace(escapee, '\\' + escapee) |
147 | - return '"%s"' % literal |
148 | - |
149 | - |
150 | -def compose_env_string(*env_dicts): |
151 | - """Turn dict(s) into a series of shell parameter assignments. |
152 | - |
153 | - Values in later dicts override any values with the same respective |
154 | - keys in earlier dicts. |
155 | - """ |
156 | - env = {} |
157 | - for env_dict in env_dicts: |
158 | - env.update(env_dict) |
159 | - return ' '.join(['='.join(pair) for pair in env.iteritems()]) |
160 | - |
161 | - |
162 | def get_backup_dists(archive_config): |
163 | """Return the path of an archive's backup dists directory.""" |
164 | return os.path.join(archive_config.archiveroot + "-distscopy", "dists") |
165 | @@ -122,31 +88,6 @@ |
166 | return get_dists(archive_config) + ".in-progress" |
167 | |
168 | |
169 | -def extend_PATH(): |
170 | - """Produce env dict for extending $PATH. |
171 | - |
172 | - Adds the Launchpad source tree's cronscripts/publishing to the |
173 | - existing $PATH. |
174 | - |
175 | - :return: a dict suitable for passing to compose_env_string. |
176 | - """ |
177 | - scripts_dir = os.path.join(config.root, "cronscripts", "publishing") |
178 | - return {"PATH": '"$PATH":%s' % shell_quote(scripts_dir)} |
179 | - |
180 | - |
181 | -def find_run_parts_dir(distro, parts): |
182 | - """Find the requested run-parts directory, if it exists.""" |
183 | - run_parts_location = config.archivepublisher.run_parts_location |
184 | - if not run_parts_location: |
185 | - return |
186 | - |
187 | - parts_dir = os.path.join(run_parts_location, distro.name, parts) |
188 | - if file_exists(parts_dir): |
189 | - return parts_dir |
190 | - else: |
191 | - return None |
192 | - |
193 | - |
194 | def map_distro_pubconfigs(distro): |
195 | """Return dict mapping archive purpose for distro's pub configs. |
196 | |
197 | @@ -238,26 +179,6 @@ |
198 | "Distribution %s not found." % self.options.distribution) |
199 | self.distributions = [distro] |
200 | |
201 | - def executeShell(self, command_line, failure=None): |
202 | - """Run `command_line` through a shell. |
203 | - |
204 | - This won't just load an external program and run it; the command |
205 | - line goes through the full shell treatment including variable |
206 | - substitutions, output redirections, and so on. |
207 | - |
208 | - :param command_line: Shell command. |
209 | - :param failure: Raise `failure` as an exception if the shell |
210 | - command returns a nonzero value. If omitted, nonzero return |
211 | - values are ignored. |
212 | - """ |
213 | - self.logger.debug("Executing: %s" % command_line) |
214 | - retval = os.system(command_line) |
215 | - if retval != 0: |
216 | - self.logger.debug("Command returned %d.", retval) |
217 | - if failure is not None: |
218 | - self.logger.debug("Command failed: %s", failure) |
219 | - raise failure |
220 | - |
221 | def getConfigs(self): |
222 | """Set up configuration objects for archives to be published. |
223 | |
224 | @@ -370,8 +291,9 @@ |
225 | for purpose, archive_config in self.configs[distribution].iteritems(): |
226 | dists = get_dists(archive_config) |
227 | backup_dists = get_backup_dists(archive_config) |
228 | - self.executeShell( |
229 | - "rsync -aH --delete '%s/' '%s'" % (dists, backup_dists), |
230 | + execute_subprocess( |
231 | + ["rsync", "-aH", "--delete", "%s/" % dists, backup_dists], |
232 | + log=self.logger, |
233 | failure=LaunchpadScriptFailure( |
234 | "Failed to rsync new dists for %s." % purpose.title)) |
235 | |
236 | @@ -469,12 +391,13 @@ |
237 | """Execute the publish-distro hooks.""" |
238 | archive_config = self.configs[distribution][archive.purpose] |
239 | env = { |
240 | - 'ARCHIVEROOT': shell_quote(archive_config.archiveroot), |
241 | - 'DISTSROOT': shell_quote(get_backup_dists(archive_config)), |
242 | + 'ARCHIVEROOT': archive_config.archiveroot, |
243 | + 'DISTSROOT': get_backup_dists(archive_config), |
244 | } |
245 | if archive_config.overrideroot is not None: |
246 | - env["OVERRIDEROOT"] = shell_quote(archive_config.overrideroot) |
247 | - self.runParts(distribution, 'publish-distro.d', env) |
248 | + env["OVERRIDEROOT"] = archive_config.overrideroot |
249 | + run_parts( |
250 | + distribution.name, 'publish-distro.d', log=self.logger, env=env) |
251 | |
252 | def installDists(self, distribution): |
253 | """Put the new dists into place, as near-atomically as possible. |
254 | @@ -499,40 +422,22 @@ |
255 | def clearEmptyDirs(self, distribution): |
256 | """Clear out any redundant empty directories.""" |
257 | for archive_config in self.configs[distribution].itervalues(): |
258 | - self.executeShell( |
259 | - "find '%s' -type d -empty | xargs -r rmdir" |
260 | - % archive_config.archiveroot) |
261 | - |
262 | - def runParts(self, distribution, parts, env): |
263 | - """Execute run-parts. |
264 | - |
265 | - :param distribution: `Distribution` to execute run-parts scripts for. |
266 | - :param parts: The run-parts directory to execute: |
267 | - "publish-distro.d" or "finalize.d". |
268 | - :param env: A dict of environment variables to pass to the |
269 | - scripts in the run-parts directory. |
270 | - """ |
271 | - parts_dir = find_run_parts_dir(distribution, parts) |
272 | - if parts_dir is None: |
273 | - self.logger.debug("Skipping run-parts %s: not configured.", parts) |
274 | - return |
275 | - env_string = compose_env_string(env, extend_PATH()) |
276 | - self.executeShell( |
277 | - "env %s run-parts -- '%s'" % (env_string, parts_dir), |
278 | - failure=LaunchpadScriptFailure( |
279 | - "Failure while executing run-parts %s." % parts_dir)) |
280 | + execute_subprocess( |
281 | + ["find", archive_config.archiveroot, "-type", "d", "-empty", |
282 | + "-delete"], |
283 | + log=self.logger) |
284 | |
285 | def runFinalizeParts(self, distribution, security_only=False): |
286 | """Run the finalize.d parts to finalize publication.""" |
287 | - archive_roots = shell_quote(' '.join([ |
288 | + archive_roots = ' '.join([ |
289 | archive_config.archiveroot |
290 | - for archive_config in self.configs[distribution].itervalues()])) |
291 | + for archive_config in self.configs[distribution].itervalues()]) |
292 | |
293 | env = { |
294 | - 'SECURITY_UPLOAD_ONLY': compose_shell_boolean(security_only), |
295 | + 'SECURITY_UPLOAD_ONLY': 'yes' if security_only else 'no', |
296 | 'ARCHIVEROOTS': archive_roots, |
297 | } |
298 | - self.runParts(distribution, 'finalize.d', env) |
299 | + run_parts(distribution.name, 'finalize.d', log=self.logger, env=env) |
300 | |
301 | def publishSecurityUploads(self, distribution): |
302 | """Quickly process just the pending security uploads. |
303 | |
304 | === modified file 'lib/lp/archivepublisher/tests/test_publish_ftpmaster.py' |
305 | --- lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2016-12-05 22:16:25 +0000 |
306 | +++ lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2018-01-18 15:33:21 +0000 |
307 | @@ -1,4 +1,4 @@ |
308 | -# Copyright 2011-2016 Canonical Ltd. This software is licensed under the |
309 | +# Copyright 2011-2018 Canonical Ltd. This software is licensed under the |
310 | # GNU Affero General Public License version 3 (see the file LICENSE). |
311 | |
312 | """Test publish-ftpmaster cron script.""" |
313 | @@ -13,6 +13,8 @@ |
314 | from apt_pkg import TagFile |
315 | from fixtures import MonkeyPatch |
316 | from testtools.matchers import ( |
317 | + ContainsDict, |
318 | + Equals, |
319 | MatchesException, |
320 | MatchesStructure, |
321 | Not, |
322 | @@ -26,20 +28,16 @@ |
323 | from lp.archivepublisher.config import getPubConfig |
324 | from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet |
325 | from lp.archivepublisher.scripts.publish_ftpmaster import ( |
326 | - compose_env_string, |
327 | - compose_shell_boolean, |
328 | - find_run_parts_dir, |
329 | get_working_dists, |
330 | newer_mtime, |
331 | PublishFTPMaster, |
332 | - shell_quote, |
333 | ) |
334 | +from lp.archivepublisher.tests.test_run_parts import RunPartsMixin |
335 | from lp.registry.interfaces.pocket import ( |
336 | PackagePublishingPocket, |
337 | pocketsuffix, |
338 | ) |
339 | from lp.registry.interfaces.series import SeriesStatus |
340 | -from lp.services.config import config |
341 | from lp.services.database.interfaces import IMasterStore |
342 | from lp.services.log.logger import ( |
343 | BufferLogger, |
344 | @@ -60,10 +58,7 @@ |
345 | TestCaseWithFactory, |
346 | ) |
347 | from lp.testing.fakemethod import FakeMethod |
348 | -from lp.testing.layers import ( |
349 | - LaunchpadZopelessLayer, |
350 | - ZopelessDatabaseLayer, |
351 | - ) |
352 | +from lp.testing.layers import LaunchpadZopelessLayer |
353 | |
354 | |
355 | def path_exists(*path_components): |
356 | @@ -133,26 +128,6 @@ |
357 | class HelpersMixin: |
358 | """Helpers for the PublishFTPMaster tests.""" |
359 | |
360 | - def enableRunParts(self, parts_directory=None): |
361 | - """Set up for run-parts execution. |
362 | - |
363 | - :param parts_directory: Base location for the run-parts directories. |
364 | - If omitted, a temporary directory will be used. |
365 | - """ |
366 | - if parts_directory is None: |
367 | - parts_directory = self.makeTemporaryDirectory() |
368 | - os.makedirs(os.path.join( |
369 | - parts_directory, "ubuntu", "publish-distro.d")) |
370 | - os.makedirs(os.path.join(parts_directory, "ubuntu", "finalize.d")) |
371 | - self.parts_directory = parts_directory |
372 | - |
373 | - config.push("run-parts", dedent("""\ |
374 | - [archivepublisher] |
375 | - run_parts_location: %s |
376 | - """ % parts_directory)) |
377 | - |
378 | - self.addCleanup(config.pop, "run-parts") |
379 | - |
380 | def makeDistroWithPublishDirectory(self): |
381 | """Create a `Distribution` for testing. |
382 | |
383 | @@ -178,44 +153,6 @@ |
384 | self.makeTemporaryDirectory()) |
385 | |
386 | |
387 | -class TestPublishFTPMasterHelpers(TestCase): |
388 | - |
389 | - def test_compose_env_string_iterates_env_dict(self): |
390 | - env = { |
391 | - "A": "1", |
392 | - "B": "2", |
393 | - } |
394 | - env_string = compose_env_string(env) |
395 | - self.assertIn(env_string, ["A=1 B=2", "B=2 A=1"]) |
396 | - |
397 | - def test_compose_env_string_combines_env_dicts(self): |
398 | - env1 = {"A": "1"} |
399 | - env2 = {"B": "2"} |
400 | - env_string = compose_env_string(env1, env2) |
401 | - self.assertIn(env_string, ["A=1 B=2", "B=2 A=1"]) |
402 | - |
403 | - def test_compose_env_string_overrides_repeated_keys(self): |
404 | - self.assertEqual("A=2", compose_env_string({"A": "1"}, {"A": "2"})) |
405 | - |
406 | - def test_shell_quote_quotes_string(self): |
407 | - self.assertEqual('"x"', shell_quote("x")) |
408 | - |
409 | - def test_shell_quote_escapes_string(self): |
410 | - self.assertEqual('"\\\\"', shell_quote("\\")) |
411 | - |
412 | - def test_shell_quote_does_not_escape_its_own_escapes(self): |
413 | - self.assertEqual('"\\$"', shell_quote("$")) |
414 | - |
415 | - def test_shell_quote_escapes_entire_string(self): |
416 | - self.assertEqual('"\\$\\$\\$"', shell_quote("$$$")) |
417 | - |
418 | - def test_compose_shell_boolean_shows_True_as_yes(self): |
419 | - self.assertEqual("yes", compose_shell_boolean(True)) |
420 | - |
421 | - def test_compose_shell_boolean_shows_False_as_no(self): |
422 | - self.assertEqual("no", compose_shell_boolean(False)) |
423 | - |
424 | - |
425 | class TestNewerMtime(TestCase): |
426 | |
427 | def setUp(self): |
428 | @@ -256,34 +193,8 @@ |
429 | self.assertTrue(newer_mtime(self.a, self.b)) |
430 | |
431 | |
432 | -class TestFindRunPartsDir(TestCaseWithFactory, HelpersMixin): |
433 | - layer = ZopelessDatabaseLayer |
434 | - |
435 | - def test_find_run_parts_dir_finds_runparts_directory(self): |
436 | - self.enableRunParts() |
437 | - ubuntu = getUtility(ILaunchpadCelebrities).ubuntu |
438 | - self.assertEqual( |
439 | - os.path.join( |
440 | - config.root, self.parts_directory, "ubuntu", "finalize.d"), |
441 | - find_run_parts_dir(ubuntu, "finalize.d")) |
442 | - |
443 | - def test_find_run_parts_dir_ignores_blank_config(self): |
444 | - self.enableRunParts("") |
445 | - ubuntu = getUtility(ILaunchpadCelebrities).ubuntu |
446 | - self.assertIs(None, find_run_parts_dir(ubuntu, "finalize.d")) |
447 | - |
448 | - def test_find_run_parts_dir_ignores_none_config(self): |
449 | - self.enableRunParts("none") |
450 | - ubuntu = getUtility(ILaunchpadCelebrities).ubuntu |
451 | - self.assertIs(None, find_run_parts_dir(ubuntu, "finalize.d")) |
452 | - |
453 | - def test_find_run_parts_dir_ignores_nonexistent_directory(self): |
454 | - self.enableRunParts() |
455 | - distro = self.factory.makeDistribution() |
456 | - self.assertIs(None, find_run_parts_dir(distro, "finalize.d")) |
457 | - |
458 | - |
459 | -class TestPublishFTPMasterScript(TestCaseWithFactory, HelpersMixin): |
460 | +class TestPublishFTPMasterScript( |
461 | + TestCaseWithFactory, RunPartsMixin, HelpersMixin): |
462 | layer = LaunchpadZopelessLayer |
463 | |
464 | # Location of shell script. |
465 | @@ -552,12 +463,14 @@ |
466 | script = self.makeScript(distro) |
467 | script.setUp() |
468 | script.setUpDirs() |
469 | - script.runParts = FakeMethod() |
470 | + run_parts_fixture = self.useFixture(MonkeyPatch( |
471 | + "lp.archivepublisher.scripts.publish_ftpmaster.run_parts", |
472 | + FakeMethod())) |
473 | script.publishDistroArchive(distro, distro.main_archive) |
474 | - self.assertEqual(1, script.runParts.call_count) |
475 | - args, kwargs = script.runParts.calls[0] |
476 | - run_distro, parts_dir, env = args |
477 | - self.assertEqual(distro, run_distro) |
478 | + self.assertEqual(1, run_parts_fixture.new_value.call_count) |
479 | + args, _ = run_parts_fixture.new_value.calls[0] |
480 | + run_distro_name, parts_dir = args |
481 | + self.assertEqual(distro.name, run_distro_name) |
482 | self.assertEqual("publish-distro.d", parts_dir) |
483 | |
484 | def test_runPublishDistroParts_passes_parameters(self): |
485 | @@ -565,14 +478,19 @@ |
486 | script = self.makeScript(distro) |
487 | script.setUp() |
488 | script.setUpDirs() |
489 | - script.runParts = FakeMethod() |
490 | + run_parts_fixture = self.useFixture(MonkeyPatch( |
491 | + "lp.archivepublisher.scripts.publish_ftpmaster.run_parts", |
492 | + FakeMethod())) |
493 | script.runPublishDistroParts(distro, distro.main_archive) |
494 | - args, kwargs = script.runParts.calls[0] |
495 | - run_distro, parts_dir, env = args |
496 | - required_parameters = set([ |
497 | - "ARCHIVEROOT", "DISTSROOT", "OVERRIDEROOT"]) |
498 | - missing_parameters = required_parameters.difference(set(env.keys())) |
499 | - self.assertEqual(set(), missing_parameters) |
500 | + _, kwargs = run_parts_fixture.new_value.calls[0] |
501 | + distro_config = get_pub_config(distro) |
502 | + self.assertThat(kwargs["env"], ContainsDict({ |
503 | + "ARCHIVEROOT": Equals(get_archive_root(distro_config)), |
504 | + "DISTSROOT": Equals( |
505 | + os.path.join(get_distscopy_root(distro_config), "dists")), |
506 | + "OVERRIDEROOT": Equals( |
507 | + get_archive_root(distro_config) + "-overrides"), |
508 | + })) |
509 | |
510 | def test_clearEmptyDirs_cleans_up_empty_directories(self): |
511 | distro = self.makeDistroWithPublishDirectory() |
512 | @@ -621,67 +539,16 @@ |
513 | script.options.distribution = self.factory.getUniqueString() |
514 | self.assertRaises(LaunchpadScriptFailure, script.processOptions) |
515 | |
516 | - def test_runParts_runs_parts(self): |
517 | - self.enableRunParts() |
518 | - script = self.makeScript(self.prepareUbuntu()) |
519 | - script.setUp() |
520 | - distro = script.distributions[0] |
521 | - script.executeShell = FakeMethod() |
522 | - script.runParts(distro, "finalize.d", {}) |
523 | - self.assertEqual(1, script.executeShell.call_count) |
524 | - args, kwargs = script.executeShell.calls[-1] |
525 | - command_line, = args |
526 | - self.assertIn("run-parts", command_line) |
527 | - self.assertIn( |
528 | - os.path.join(self.parts_directory, "ubuntu/finalize.d"), |
529 | - command_line) |
530 | - |
531 | - def test_runParts_passes_parameters(self): |
532 | - self.enableRunParts() |
533 | - script = self.makeScript(self.prepareUbuntu()) |
534 | - script.setUp() |
535 | - distro = script.distributions[0] |
536 | - script.executeShell = FakeMethod() |
537 | - key = self.factory.getUniqueString() |
538 | - value = self.factory.getUniqueString() |
539 | - script.runParts(distro, "finalize.d", {key: value}) |
540 | - args, kwargs = script.executeShell.calls[-1] |
541 | - command_line, = args |
542 | - self.assertIn("%s=%s" % (key, value), command_line) |
543 | - |
544 | - def test_executeShell_executes_shell_command(self): |
545 | - distro = self.makeDistroWithPublishDirectory() |
546 | - script = self.makeScript(distro) |
547 | - marker = os.path.join( |
548 | - get_pub_config(distro).root_dir, "marker") |
549 | - script.executeShell("touch %s" % marker) |
550 | - self.assertTrue(file_exists(marker)) |
551 | - |
552 | - def test_executeShell_reports_failure_if_requested(self): |
553 | - distro = self.makeDistroWithPublishDirectory() |
554 | - script = self.makeScript(distro) |
555 | - |
556 | - class ArbitraryFailure(Exception): |
557 | - """Some exception that's not likely to come from elsewhere.""" |
558 | - |
559 | - self.assertRaises( |
560 | - ArbitraryFailure, |
561 | - script.executeShell, "/bin/false", failure=ArbitraryFailure()) |
562 | - |
563 | - def test_executeShell_does_not_report_failure_if_not_requested(self): |
564 | - distro = self.makeDistroWithPublishDirectory() |
565 | - script = self.makeScript(distro) |
566 | - # The test is that this does not fail: |
567 | - script.executeShell("/bin/false") |
568 | - |
569 | def test_runFinalizeParts_passes_parameters(self): |
570 | script = self.makeScript(self.prepareUbuntu()) |
571 | script.setUp() |
572 | distro = script.distributions[0] |
573 | - script.runParts = FakeMethod() |
574 | + run_parts_fixture = self.useFixture(MonkeyPatch( |
575 | + "lp.archivepublisher.scripts.publish_ftpmaster.run_parts", |
576 | + FakeMethod())) |
577 | script.runFinalizeParts(distro) |
578 | - args, kwargs = script.runParts.calls[0] |
579 | - run_distro, parts_dir, env = args |
580 | + _, kwargs = run_parts_fixture.new_value.calls[0] |
581 | + env = kwargs["env"] |
582 | required_parameters = set(["ARCHIVEROOTS", "SECURITY_UPLOAD_ONLY"]) |
583 | missing_parameters = required_parameters.difference(set(env.keys())) |
584 | self.assertEqual(set(), missing_parameters) |
585 | @@ -798,15 +665,11 @@ |
586 | self.assertContentEqual( |
587 | [distro.main_archive, partner_archive], published_archives) |
588 | |
589 | - def test_runFinalizeParts_quotes_archiveroots(self): |
590 | - # Passing ARCHIVEROOTS to the finalize.d scripts is a bit |
591 | - # difficult because the variable holds multiple values in a |
592 | - # single, double-quoted string. Escaping and quoting a sequence |
593 | - # of escaped and quoted items won't work. |
594 | - # This test establishes how a script can sanely deal with the |
595 | - # list. It'll probably go wrong if the configured archive root |
596 | - # contains spaces and such, but should work with Unix-sensible |
597 | - # paths. |
598 | + def test_runFinalizeParts_passes_archiveroots_correctly(self): |
599 | + # The ARCHIVEROOTS environment variable may contain spaces, and |
600 | + # these are passed through correctly. It'll go wrong if the |
601 | + # configured archive root contains whitespace, but works with |
602 | + # Unix-sensible paths. |
603 | distro = self.makeDistroWithPublishDirectory() |
604 | self.factory.makeArchive( |
605 | distribution=distro, purpose=ArchivePurpose.PARTNER) |
606 | |
607 | === added file 'lib/lp/archivepublisher/tests/test_run_parts.py' |
608 | --- lib/lp/archivepublisher/tests/test_run_parts.py 1970-01-01 00:00:00 +0000 |
609 | +++ lib/lp/archivepublisher/tests/test_run_parts.py 2018-01-18 15:33:21 +0000 |
610 | @@ -0,0 +1,113 @@ |
611 | +# Copyright 2011-2018 Canonical Ltd. This software is licensed under the |
612 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
613 | + |
614 | +"""Test publisher run-parts handling.""" |
615 | + |
616 | +from __future__ import absolute_import, print_function, unicode_literals |
617 | + |
618 | +__metaclass__ = type |
619 | + |
620 | +import os.path |
621 | + |
622 | +from fixtures import MonkeyPatch |
623 | +from testtools.matchers import ( |
624 | + ContainsDict, |
625 | + Equals, |
626 | + FileExists, |
627 | + ) |
628 | + |
629 | +from lp.archivepublisher.run_parts import ( |
630 | + execute_subprocess, |
631 | + find_run_parts_dir, |
632 | + run_parts, |
633 | + ) |
634 | +from lp.services.config import config |
635 | +from lp.services.log.logger import DevNullLogger |
636 | +from lp.testing import TestCase |
637 | +from lp.testing.fakemethod import FakeMethod |
638 | + |
639 | + |
640 | +class RunPartsMixin: |
641 | + """Helper for run-parts tests.""" |
642 | + |
643 | + def enableRunParts(self, parts_directory=None): |
644 | + """Set up for run-parts execution. |
645 | + |
646 | + :param parts_directory: Base location for the run-parts directories. |
647 | + If omitted, a temporary directory will be used. |
648 | + """ |
649 | + if parts_directory is None: |
650 | + parts_directory = self.makeTemporaryDirectory() |
651 | + os.makedirs(os.path.join( |
652 | + parts_directory, "ubuntu", "publish-distro.d")) |
653 | + os.makedirs(os.path.join(parts_directory, "ubuntu", "finalize.d")) |
654 | + self.parts_directory = parts_directory |
655 | + self.pushConfig("archivepublisher", run_parts_location=parts_directory) |
656 | + |
657 | + |
658 | +class TestFindRunPartsDir(TestCase, RunPartsMixin): |
659 | + |
660 | + def test_finds_runparts_directory(self): |
661 | + self.enableRunParts() |
662 | + self.assertEqual( |
663 | + os.path.join( |
664 | + config.root, self.parts_directory, "ubuntu", "finalize.d"), |
665 | + find_run_parts_dir("ubuntu", "finalize.d")) |
666 | + |
667 | + def test_ignores_blank_config(self): |
668 | + self.enableRunParts("") |
669 | + self.assertIs(None, find_run_parts_dir("ubuntu", "finalize.d")) |
670 | + |
671 | + def test_ignores_none_config(self): |
672 | + self.enableRunParts("none") |
673 | + self.assertIs(None, find_run_parts_dir("ubuntu", "finalize.d")) |
674 | + |
675 | + def test_ignores_nonexistent_directory(self): |
676 | + self.enableRunParts() |
677 | + self.assertIs(None, find_run_parts_dir("nonexistent", "finalize.d")) |
678 | + |
679 | + |
680 | +class TestExecuteSubprocess(TestCase): |
681 | + |
682 | + def test_executes_shell_command(self): |
683 | + marker = os.path.join(self.makeTemporaryDirectory(), "marker") |
684 | + execute_subprocess(["touch", marker]) |
685 | + self.assertThat(marker, FileExists()) |
686 | + |
687 | + def test_reports_failure_if_requested(self): |
688 | + class ArbitraryFailure(Exception): |
689 | + """Some exception that's not likely to come from elsewhere.""" |
690 | + |
691 | + self.assertRaises( |
692 | + ArbitraryFailure, |
693 | + execute_subprocess, ["/bin/false"], failure=ArbitraryFailure()) |
694 | + |
695 | + def test_does_not_report_failure_if_not_requested(self): |
696 | + # The test is that this does not fail: |
697 | + execute_subprocess(["/bin/false"]) |
698 | + |
699 | + |
700 | +class TestRunParts(TestCase, RunPartsMixin): |
701 | + |
702 | + def test_runs_parts(self): |
703 | + self.enableRunParts() |
704 | + execute_subprocess_fixture = self.useFixture(MonkeyPatch( |
705 | + "lp.archivepublisher.run_parts.execute_subprocess", FakeMethod())) |
706 | + run_parts("ubuntu", "finalize.d", log=DevNullLogger(), env={}) |
707 | + self.assertEqual(1, execute_subprocess_fixture.new_value.call_count) |
708 | + args, kwargs = execute_subprocess_fixture.new_value.calls[-1] |
709 | + self.assertEqual( |
710 | + (["run-parts", "--", |
711 | + os.path.join(self.parts_directory, "ubuntu/finalize.d")],), |
712 | + args) |
713 | + |
714 | + def test_passes_parameters(self): |
715 | + self.enableRunParts() |
716 | + execute_subprocess_fixture = self.useFixture(MonkeyPatch( |
717 | + "lp.archivepublisher.run_parts.execute_subprocess", FakeMethod())) |
718 | + key = self.factory.getUniqueString() |
719 | + value = self.factory.getUniqueString() |
720 | + run_parts( |
721 | + "ubuntu", "finalize.d", log=DevNullLogger(), env={key: value}) |
722 | + args, kwargs = execute_subprocess_fixture.new_value.calls[-1] |
723 | + self.assertThat(kwargs["env"], ContainsDict({key: Equals(value)})) |