Merge lp:~james-w/pkgme-service/review-click into lp:pkgme-service
- review-click
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | James Westby |
Approved revision: | no longer in the source branch. |
Merged at revision: | 143 |
Proposed branch: | lp:~james-w/pkgme-service/review-click |
Merge into: | lp:pkgme-service |
Diff against target: |
455 lines (+133/-128) 4 files modified
src/djpkgme/client.py (+6/-11) src/djpkgme/tasks.py (+107/-97) src/djpkgme/tests/test_handlers.py (+2/-2) src/djpkgme/tests/test_tasks.py (+18/-18) |
To merge this branch: | bzr merge lp:~james-w/pkgme-service/review-click |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ricardo Kirkner (community) | Approve | ||
Review via email: mp+190241@code.launchpad.net |
Commit message
Split out the logic of packaging an app from the mechanics of handling callbacks.
We want to re-use the callback logic for reviewing click packages, so split up
the existing code to make that separation clearer.
Description of the change
Hi,
This is the first of my stack of branches to add reviewing of click
packages to pkgme-service.
It splits the logic of running some code and sending callback/errbacks to
sca from the logic of packaging an application. We'll then be able to glue
in the click review code as an alternative codepath while re-using the
callback code.
Thanks,
James
James Westby (james-w) wrote : | # |
- 143. By James Westby
-
[r=ricardokirkner] Split out the logic of packaging an app from the mechanics of handling callbacks.
We want to re-use the callback logic for reviewing click packages, so split up
the existing code to make that separation clearer.
Preview Diff
1 | === modified file 'src/djpkgme/client.py' |
2 | --- src/djpkgme/client.py 2012-09-14 13:59:48 +0000 |
3 | +++ src/djpkgme/client.py 2013-10-09 20:40:35 +0000 |
4 | @@ -41,13 +41,13 @@ |
5 | """Called when any error happens during package build.""" |
6 | body = get_response_dict(None, metadata) |
7 | body['error'] = error_data |
8 | - submit_to_myapps(metadata['errback_url'], json.dumps(body), |
9 | - logger=logger) |
10 | - |
11 | - |
12 | -def submit_to_myapps(url, json_body, logger=None): |
13 | - """Put ``json_body`` to MyApps at ``url``.""" |
14 | + submit_to_myapps(metadata['errback_url'], body, logger=logger) |
15 | + |
16 | + |
17 | +def submit_to_myapps(url, body, logger=None): |
18 | + """Put ``body`` json-encoded to MyApps at ``url``.""" |
19 | GOOD_RESPONSES = (200,) |
20 | + json_body = json.dumps(body) |
21 | |
22 | headers = {} |
23 | if url.startswith(settings.MYAPPS_BASE_URL): |
24 | @@ -66,11 +66,6 @@ |
25 | raise CallbackError(url, 'PUT', response.status, content) |
26 | |
27 | |
28 | -def submit_pkgme_info(packaged_app_url, metadata, logger=None): |
29 | - body = json.dumps(get_response_dict(packaged_app_url, metadata)) |
30 | - return submit_to_myapps(metadata['callback_url'], body, logger=logger) |
31 | - |
32 | - |
33 | def get_response_dict(packaged_app_url, metadata): |
34 | """Return a json-serializable guess set for the myapps callback.""" |
35 | |
36 | |
37 | === modified file 'src/djpkgme/tasks.py' |
38 | --- src/djpkgme/tasks.py 2012-11-13 14:51:44 +0000 |
39 | +++ src/djpkgme/tasks.py 2013-10-09 20:40:35 +0000 |
40 | @@ -26,8 +26,9 @@ |
41 | |
42 | from .client import ( |
43 | CallbackError, |
44 | + get_response_dict, |
45 | submit_error, |
46 | - submit_pkgme_info, |
47 | + submit_to_myapps, |
48 | ) |
49 | from .osutils import ( |
50 | download_file, |
51 | @@ -164,15 +165,116 @@ |
52 | PkgmeError, |
53 | ) |
54 | |
55 | - # The name of the directory that we store the downloaded file in. |
56 | - DOWNLOAD_DIRECTORY_NAME = 'download' |
57 | - |
58 | REQUIRED_FIELDS = [ |
59 | 'callback_url', |
60 | 'package_url', |
61 | ] |
62 | |
63 | - def build_package(self, metadata, overrides): |
64 | + def _submit_error(self, metadata, error_data): |
65 | + if metadata.get('errback_url', None): |
66 | + self.get_logger().error( |
67 | + "Submitting error info to %s" % (metadata['errback_url'],)) |
68 | + try: |
69 | + submit_error(metadata, error_data, logger=self.get_logger()) |
70 | + except CallbackError, e: |
71 | + # XXX: Should generate an OOPS report and log the OOPS number. |
72 | + # Obviously we can't send to the client. |
73 | + self.get_logger().error( |
74 | + "Error while trying to submit error to client: %s\n%s" |
75 | + % (str(e), error_data)) |
76 | + else: |
77 | + raise |
78 | + |
79 | + def _validate_metadata(self, metadata): |
80 | + """Make sure 'metadata' has all the fields we need.""" |
81 | + missing_fields = [] |
82 | + for field in self.REQUIRED_FIELDS: |
83 | + if field not in metadata: |
84 | + missing_fields.append(field) |
85 | + if missing_fields: |
86 | + raise MissingFieldsError(metadata, missing_fields) |
87 | + |
88 | + def _log_failure_exception(self, metadata, ftype): |
89 | + self.get_logger().info("%s (%s) failed with %s exception: %s" % ( |
90 | + ftype, |
91 | + metadata.get('myapps_id', '<unknown myapps id>'), |
92 | + metadata.get('package_url', '<unknown package url>'), |
93 | + traceback.format_exc(), |
94 | + )) |
95 | + |
96 | + def run(self, metadata, overrides): |
97 | + """Build a source package. |
98 | + |
99 | + Requires 'metadata' to have: |
100 | + * callback_url |
101 | + * package_url |
102 | + |
103 | + Uses these if they exist: |
104 | + * errback_url |
105 | + * myapps_id |
106 | + |
107 | + Assumes that the backend can determine a non-empty value for |
108 | + 'package_name'. |
109 | + """ |
110 | + self._validate_metadata(metadata) |
111 | + self.get_logger().info( |
112 | + "Building package for %s: %s" % ( |
113 | + metadata.get('myapps_id', '<unknown myapps id>'), |
114 | + metadata.get('package_url', '<unknown package url>'), |
115 | + )) |
116 | + job = PackageAppJob() |
117 | + return self.run_with_error_handling(job, metadata, overrides) |
118 | + |
119 | + def run_with_error_handling(self, job, metadata, overrides): |
120 | + try: |
121 | + generated_info = job.run(metadata, overrides, self.get_logger()) |
122 | + except self.EXPECTED_EXCEPTIONS, e: |
123 | + # pkgme couldn't make the package. If the user were running |
124 | + # 'pkgme' on the command line they would get a simple, |
125 | + # user-friendly error. Just send that, and not the whole |
126 | + # traceback. |
127 | + self._log_failure_exception(metadata, "user") |
128 | + self._submit_error(metadata, str(e)) |
129 | + except FATAL_EXCEPTIONS: |
130 | + # Someone hit Ctrl-C or something like that. Just keep raising. |
131 | + raise |
132 | + except: |
133 | + # The exception is something unexpected, and thus represents a bug |
134 | + # in the system. |
135 | + # |
136 | + # XXX: Should generate an OOPS and report that to the client. For |
137 | + # now, send the error across without the traceback. This is an |
138 | + # interim measure to reduce our reliance on timeouts for doing |
139 | + # system testing. |
140 | + self._log_failure_exception(metadata, "internal") |
141 | + exc_type, exc_obj, tb = sys.exc_info() |
142 | + self._submit_error( |
143 | + metadata, exc_info_as_dict((exc_type, exc_obj, None))) |
144 | + else: |
145 | + self.get_logger().info( |
146 | + "Submitting success info to %r: %s" |
147 | + % (metadata['callback_url'], generated_info)) |
148 | + try: |
149 | + callback_body = job.transform_results( |
150 | + generated_info, metadata) |
151 | + submit_to_myapps(metadata['callback_url'], callback_body, |
152 | + logger=self.get_logger()) |
153 | + except CallbackError: |
154 | + self._submit_error(metadata, exc_info_as_dict(sys.exc_info())) |
155 | + |
156 | +tasks.register(BuildPackageTask) |
157 | + |
158 | + |
159 | +class PackageAppJob(object): |
160 | + |
161 | + # The name of the directory that we store the downloaded file in. |
162 | + DOWNLOAD_DIRECTORY_NAME = 'download' |
163 | + |
164 | + def transform_results(self, package_path, metadata): |
165 | + package_url = get_url_for_file(package_path) |
166 | + return get_response_dict(package_url, metadata) |
167 | + |
168 | + def run(self, metadata, overrides, logger): |
169 | """Build packaging for 'metadata'. |
170 | |
171 | :return: A path on disk to a tarball containing the built source |
172 | @@ -201,7 +303,6 @@ |
173 | # devportal-metadata.json) are tarred up and stored in a preconfigured |
174 | # location: PKGME_OUTPUT_DIRECTORY. |
175 | working_dirs = [] |
176 | - logger = self.get_logger() |
177 | with TempDir() as temp_dir: |
178 | output_dir = temp_dir.path |
179 | download_dir = os.path.join( |
180 | @@ -256,97 +357,6 @@ |
181 | return make_tarball( |
182 | output_dir, settings.PKGME_OUTPUT_DIRECTORY, package_name) |
183 | |
184 | - def _submit_error(self, metadata, error_data): |
185 | - if metadata.get('errback_url', None): |
186 | - self.get_logger().error( |
187 | - "Submitting error info to %s" % (metadata['errback_url'],)) |
188 | - try: |
189 | - submit_error(metadata, error_data, logger=self.get_logger()) |
190 | - except CallbackError, e: |
191 | - # XXX: Should generate an OOPS report and log the OOPS number. |
192 | - # Obviously we can't send to the client. |
193 | - self.get_logger().error( |
194 | - "Error while trying to submit error to client: %s\n%s" |
195 | - % (str(e), error_data)) |
196 | - else: |
197 | - raise |
198 | - |
199 | - def _validate_metadata(self, metadata): |
200 | - """Make sure 'metadata' has all the fields we need.""" |
201 | - missing_fields = [] |
202 | - for field in self.REQUIRED_FIELDS: |
203 | - if field not in metadata: |
204 | - missing_fields.append(field) |
205 | - if missing_fields: |
206 | - raise MissingFieldsError(metadata, missing_fields) |
207 | - |
208 | - def _log_failure_exception(self, metadata, ftype): |
209 | - self.get_logger().info("%s (%s) failed with %s exception: %s" % ( |
210 | - ftype, |
211 | - metadata.get('myapps_id', '<unknown myapps id>'), |
212 | - metadata.get('package_url', '<unknown package url>'), |
213 | - traceback.format_exc(), |
214 | - )) |
215 | - |
216 | - def run(self, metadata, overrides): |
217 | - """Build a source package. |
218 | - |
219 | - Requires 'metadata' to have: |
220 | - * callback_url |
221 | - * package_url |
222 | - |
223 | - Uses these if they exist: |
224 | - * errback_url |
225 | - * myapps_id |
226 | - |
227 | - Assumes that the backend can determine a non-empty value for |
228 | - 'package_name'. |
229 | - """ |
230 | - self._validate_metadata(metadata) |
231 | - self.get_logger().info( |
232 | - "Building package for %s: %s" % ( |
233 | - metadata.get('myapps_id', '<unknown myapps id>'), |
234 | - metadata.get('package_url', '<unknown package url>'), |
235 | - )) |
236 | - try: |
237 | - packaged_app_path = self.build_package(metadata, overrides) |
238 | - except self.EXPECTED_EXCEPTIONS, e: |
239 | - # pkgme couldn't make the package. If the user were running |
240 | - # 'pkgme' on the command line they would get a simple, |
241 | - # user-friendly error. Just send that, and not the whole |
242 | - # traceback. |
243 | - self._log_failure_exception(metadata, "user") |
244 | - self._submit_error(metadata, str(e)) |
245 | - except FATAL_EXCEPTIONS: |
246 | - # Someone hit Ctrl-C or something like that. Just keep raising. |
247 | - raise |
248 | - except: |
249 | - # The exception is something unexpected, and thus represents a bug |
250 | - # in the system. |
251 | - # |
252 | - # XXX: Should generate an OOPS and report that to the client. For |
253 | - # now, send the error across without the traceback. This is an |
254 | - # interim measure to reduce our reliance on timeouts for doing |
255 | - # system testing. |
256 | - self._log_failure_exception(metadata, "internal") |
257 | - exc_type, exc_obj, tb = sys.exc_info() |
258 | - self._submit_error( |
259 | - metadata, exc_info_as_dict((exc_type, exc_obj, None))) |
260 | - else: |
261 | - packaged_app_url = get_url_for_file(packaged_app_path) |
262 | - self.get_logger().info( |
263 | - "Submitting success info to %r: %s" |
264 | - % (metadata['callback_url'], packaged_app_url)) |
265 | - try: |
266 | - submit_pkgme_info(packaged_app_url, metadata, |
267 | - logger=self.get_logger()) |
268 | - except CallbackError: |
269 | - self._submit_error(metadata, exc_info_as_dict(sys.exc_info())) |
270 | - else: |
271 | - return packaged_app_url |
272 | - |
273 | -tasks.register(BuildPackageTask) |
274 | - |
275 | |
276 | class CauseOopsTask(Task): |
277 | def run(self): |
278 | |
279 | === modified file 'src/djpkgme/tests/test_handlers.py' |
280 | --- src/djpkgme/tests/test_handlers.py 2012-03-12 14:10:27 +0000 |
281 | +++ src/djpkgme/tests/test_handlers.py 2013-10-09 20:40:35 +0000 |
282 | @@ -46,7 +46,7 @@ |
283 | |
284 | # Patch httplib2 so the handler doesn't try to PUT to callback_url |
285 | @patch('httplib2.Http.request', return_value=(FakeResponse(200, 'OK'), '')) |
286 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
287 | + @patch('djpkgme.tasks.PackageAppJob.run') |
288 | def test_no_overrides_success(self, mock_build_package, mock_request): |
289 | request = Mock() |
290 | mock_build_package.return_value = 'foo' |
291 | @@ -57,7 +57,7 @@ |
292 | |
293 | # Patch httplib2 so the handler doesn't try to PUT to callback_url |
294 | @patch('httplib2.Http.request', return_value=(FakeResponse(200, 'OK'), '')) |
295 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
296 | + @patch('djpkgme.tasks.PackageAppJob.run') |
297 | def test_overrides_success(self, mock_build_package, mock_request): |
298 | request = Mock() |
299 | mock_build_package.return_value = 'foo' |
300 | |
301 | === modified file 'src/djpkgme/tests/test_tasks.py' |
302 | --- src/djpkgme/tests/test_tasks.py 2012-11-15 15:34:28 +0000 |
303 | +++ src/djpkgme/tests/test_tasks.py 2013-10-09 20:40:35 +0000 |
304 | @@ -2,6 +2,7 @@ |
305 | # http://ask.github.com/celery/cookbook/unit-testing.html |
306 | |
307 | import json |
308 | +import logging |
309 | import os |
310 | import sys |
311 | import tarfile |
312 | @@ -32,6 +33,7 @@ |
313 | BuildPackageTask, |
314 | CallbackError, |
315 | get_url_for_file, |
316 | + PackageAppJob, |
317 | prepare_icons, |
318 | prepare_metadata, |
319 | translate_dict, |
320 | @@ -225,7 +227,7 @@ |
321 | class BuildPackageTaskTestCase(TestCaseWithFactory): |
322 | |
323 | @patch('httplib2.Http.request', return_value=(FakeResponse(200, 'OK'), '')) |
324 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
325 | + @patch('djpkgme.tasks.PackageAppJob.run') |
326 | def test_build_package_is_called(self, mock_build_package, mock_request): |
327 | mock_build_package.return_value = "foo" |
328 | |
329 | @@ -234,12 +236,11 @@ |
330 | |
331 | result = BuildPackageTask.delay(metadata, overrides) |
332 | |
333 | - self.assertEqual(get_url_for_file('foo'), result.get()) |
334 | self.assertTrue(result.successful()) |
335 | self.assertEqual(1, mock_build_package.call_count) |
336 | |
337 | @patch('httplib2.Http.request', return_value=(FakeResponse(200, 'OK'), '')) |
338 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
339 | + @patch('djpkgme.tasks.PackageAppJob.run') |
340 | def test_result_is_submitted(self, mock_build_package, mock_request): |
341 | mock_build_package.return_value = "foo" |
342 | |
343 | @@ -260,8 +261,8 @@ |
344 | self.assertEqual(expected, json.loads(kwargs['body'])) |
345 | |
346 | @patch('djpkgme.tasks.submit_error') |
347 | - @patch('djpkgme.tasks.submit_pkgme_info') |
348 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
349 | + @patch('djpkgme.tasks.submit_to_myapps') |
350 | + @patch('djpkgme.tasks.PackageAppJob.run') |
351 | def test_cannot_send_callback(self, mock_build_package, mock_submit, |
352 | mock_error): |
353 | # If we get a negative response to our callback, then we try to send |
354 | @@ -285,7 +286,7 @@ |
355 | |
356 | @patch('djpkgme.tasks.BuildPackageTask.get_logger') |
357 | @patch('djpkgme.tasks.submit_error') |
358 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
359 | + @patch('djpkgme.tasks.PackageAppJob.run') |
360 | def test_cannot_send_errback(self, mock_build_package, mock_error, |
361 | mock_get_logger): |
362 | # If we get a negative response to our errback, then we just log the |
363 | @@ -310,25 +311,24 @@ |
364 | self.patch(task.request, 'loglevel', UNREASONABLY_HIGH_LEVEL) |
365 | |
366 | @patch('djpkgme.client.submit_to_myapps') |
367 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
368 | + @patch('djpkgme.tasks.PackageAppJob.run') |
369 | def test_failed_build_package(self, mock_build_package, mock_submit): |
370 | metadata = self.factory.make_metadata() |
371 | mock_build_package.side_effect = PkgmeError('some failure') |
372 | task = BuildPackageTask() |
373 | self._suppress_logging(task) |
374 | task.run(metadata, {}) |
375 | - [((url, json_body), kwargs)] = mock_submit.call_args_list |
376 | + [((url, body), kwargs)] = mock_submit.call_args_list |
377 | self.assertEqual(metadata['errback_url'], url) |
378 | del kwargs['logger'] |
379 | self.assertEqual({}, kwargs) |
380 | - body = json.loads(json_body) |
381 | error = body.pop('error') |
382 | self.assertEqual( |
383 | [metadata['errback_url'], |
384 | get_response_dict(None, metadata), 'some failure', {}], |
385 | [url, body, error, kwargs]) |
386 | |
387 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
388 | + @patch('djpkgme.tasks.PackageAppJob.run') |
389 | def test_failed_build_package_no_errback_url(self, mock_build_package): |
390 | # If no errback URL is provided then just raise. |
391 | metadata = self.factory.make_metadata() |
392 | @@ -338,7 +338,7 @@ |
393 | self.assertRaises(PkgmeError, task.run, metadata, {}) |
394 | |
395 | @patch('djpkgme.client.submit_to_myapps') |
396 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
397 | + @patch('djpkgme.tasks.PackageAppJob.run') |
398 | def test_failed_build_package_unexpected(self, mock_build_package, |
399 | mock_submit): |
400 | metadata = self.factory.make_metadata() |
401 | @@ -347,11 +347,10 @@ |
402 | task = BuildPackageTask() |
403 | self._suppress_logging(task) |
404 | task.run(metadata, {}) |
405 | - [((url, json_body), kwargs)] = mock_submit.call_args_list |
406 | + [((url, body), kwargs)] = mock_submit.call_args_list |
407 | self.assertEqual(metadata['errback_url'], url) |
408 | del kwargs['logger'] |
409 | self.assertEqual({}, kwargs) |
410 | - body = json.loads(json_body) |
411 | error = body.pop('error') |
412 | self.assertThat( |
413 | [url, body, error, kwargs], |
414 | @@ -363,7 +362,7 @@ |
415 | 'traceback': 'RuntimeError: some failure\n'}), |
416 | Equals({})])) |
417 | |
418 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
419 | + @patch('djpkgme.tasks.PackageAppJob.run') |
420 | def test_failed_build_package_unexpected_no_errback_url( |
421 | self, mock_build_package): |
422 | # If no errback URL is provided then just raise. |
423 | @@ -373,7 +372,7 @@ |
424 | task = BuildPackageTask() |
425 | self.assertRaises(RuntimeError, task.run, metadata, {}) |
426 | |
427 | - @patch('djpkgme.tasks.BuildPackageTask.build_package') |
428 | + @patch('djpkgme.tasks.PackageAppJob.run') |
429 | def test_failed_build_package_fatal(self, mock_build_package): |
430 | # If keyboard interrupt or something like that is raised, then |
431 | # re-raise, even if errback_url is set. |
432 | @@ -383,11 +382,11 @@ |
433 | self.assertRaises(KeyboardInterrupt, task.run, metadata, {}) |
434 | |
435 | |
436 | -class TestBuildPackageTaskIntegration(TestCaseWithFactory): |
437 | +class TestPackageAppJobIntegration(TestCaseWithFactory): |
438 | """Tests that actually build the package.""" |
439 | |
440 | def run_task(self, metadata, file_path): |
441 | - task = BuildPackageTask() |
442 | + job = PackageAppJob() |
443 | def download_file(url, output_dir): |
444 | # Create a tarball and stick it in 'output_dir'. |
445 | proper_location = os.path.join( |
446 | @@ -398,7 +397,8 @@ |
447 | self.useFixture(LibdepFixture(DEPENDENCIES_FOR_TEST_DATA)) |
448 | temp_dir = self.useFixture(TempDir()).path |
449 | self.patch(settings, 'PKGME_OUTPUT_DIRECTORY', temp_dir) |
450 | - return task.build_package(metadata, {}) |
451 | + logger = logging.getLogger() |
452 | + return job.run(metadata, {}, logger) |
453 | |
454 | def test_writes_packaging_for_tarball(self): |
455 | # BuildPackageTask.build_package runs pkgme to build the package and |
This is largely a moving of code, with a couple of changes such as moving the
json.dumps in to the submit_to_myapps function.
Thanks,
James