Merge lp:~zyga/checkbox/fix-1378295 into lp:checkbox
- fix-1378295
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Maciej Kisielewski | ||||
Approved revision: | 3705 | ||||
Merged at revision: | 3704 | ||||
Proposed branch: | lp:~zyga/checkbox/fix-1378295 | ||||
Merge into: | lp:checkbox | ||||
Diff against target: |
1674 lines (+493/-165) 7 files modified
plainbox/plainbox/impl/result.py (+1/-1) plainbox/plainbox/impl/session/manager.py (+2/-1) plainbox/plainbox/impl/session/resume.py (+119/-11) plainbox/plainbox/impl/session/suspend.py (+92/-39) plainbox/plainbox/impl/session/test_manager.py (+2/-1) plainbox/plainbox/impl/session/test_resume.py (+162/-77) plainbox/plainbox/impl/session/test_suspend.py (+115/-35) |
||||
To merge this branch: | bzr merge lp:~zyga/checkbox/fix-1378295 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Maciej Kisielewski | Approve | ||
Review via email: mp+256007@code.launchpad.net |
Commit message
Description of the change
This branch contains a fix for the referenced bug. I took the approach of modifying the result class
to require session_dir argument to reconstruct the IO log records from relative pathnames. Then I realized it's easier to keep the in-memory representation based on absolute pathnames and only save relative pathanmes. This gave birth to the fifth suspend/resume format.
All of the patches below build towards the solution.
71d0a69 plainbox:result: fix typo
b3f66e6 plainbox:suspend: refresh copyright
e612885 plainbox:suspend: fix PEP257 issues
fa597be plainbox:suspend: add session_dir argument to suspend()
97e3ccc plainbox:suspend: push session_dir to all helper methods
c7cfc96 plainbox:session: decouple helper class from tests
6420c49 plainbox:suspend: use a more convenient io_log_pathname
1a0879b plainbox:suspend: add 5th suspend format
f54312f plainbox:suspend: switch to 5th format by default
6d8e349 plainbox:manager: pass 2nd argument to suspend()
f17216a plainbox:resume: fix typo
62dc81b plainbox:resume: fix PEP-8 issue
1f4da26 plainbox:resume: make SessionResumeHe
c25ea18 plainbox:resume: pass flags and location to _build_JobResult()
47a9850 plainbox:resume: make _build_
3a7c7bb plainbox:resume: better docstring for SessionResumeHe
b285ee9 plainbox:resume: better docstring for SessionResumeHe
e5608b2 plainbox:resume: make SessionResumeHe
7e73a5a plainbox:resume: enable extra testing for 4th format
29a1c38 plainbox:resume: add SessionResumeHe
c855762 plainbox:resume: use absolute path for DiskJobResult resume test
d849000 plainbox:resume: add 5th resume helper
31ef1ce plainbox:resume: split tests for DiskJobResult 1-4 and 5
a1990f4 plainbox:resume: enable 5th resume helper
Preview Diff
1 | === modified file 'plainbox/plainbox/impl/result.py' |
2 | --- plainbox/plainbox/impl/result.py 2015-03-30 19:25:56 +0000 |
3 | +++ plainbox/plainbox/impl/result.py 2015-04-13 18:23:59 +0000 |
4 | @@ -73,7 +73,7 @@ |
5 | # Tuple representing meta-data associated with each possible value of "outcome" |
6 | # |
7 | # This tuple replaces various ad-hoc mapping that keyed off the outcome field |
8 | -# to compute something. Currently the following fields are suppoted: |
9 | +# to compute something. Currently the following fields are supported: |
10 | # |
11 | # value - the actual constant like IJobResult.OUTCOME_NONE (for completeness) |
12 | # |
13 | |
14 | === modified file 'plainbox/plainbox/impl/session/manager.py' |
15 | --- plainbox/plainbox/impl/session/manager.py 2015-02-25 23:54:36 +0000 |
16 | +++ plainbox/plainbox/impl/session/manager.py 2015-04-13 18:23:59 +0000 |
17 | @@ -355,7 +355,8 @@ |
18 | :meth:`SessionManager.load_session()`. |
19 | """ |
20 | logger.debug("SessionManager.checkpoint()") |
21 | - data = SessionSuspendHelper().suspend(self.state) |
22 | + data = SessionSuspendHelper().suspend( |
23 | + self.state, self.storage.location) |
24 | logger.debug( |
25 | ngettext( |
26 | "Saving %d byte of checkpoint data to %r", |
27 | |
28 | === modified file 'plainbox/plainbox/impl/session/resume.py' |
29 | --- plainbox/plainbox/impl/session/resume.py 2015-01-26 15:38:31 +0000 |
30 | +++ plainbox/plainbox/impl/session/resume.py 2015-04-13 18:23:59 +0000 |
31 | @@ -172,6 +172,8 @@ |
32 | return SessionPeekHelper3().peek_json(json_repr) |
33 | elif version == 4: |
34 | return SessionPeekHelper4().peek_json(json_repr) |
35 | + elif version == 5: |
36 | + return SessionPeekHelper5().peek_json(json_repr) |
37 | else: |
38 | raise IncompatibleSessionError( |
39 | _("Unsupported version {}").format(version)) |
40 | @@ -186,9 +188,48 @@ |
41 | appropriate, format specific, resume class. |
42 | """ |
43 | |
44 | - def __init__(self, job_list, flags=None, location=None): |
45 | + def __init__( |
46 | + self, job_list: 'List[JobDefinition]', |
47 | + flags: 'Optional[Iterable[str]]', location: 'Optional[str]' |
48 | + ): |
49 | """ |
50 | - Initialize the helper with a list of known jobs. |
51 | + Initialize the helper with a list of known jobs and support data. |
52 | + |
53 | + :param job_list: |
54 | + List of known jobs |
55 | + :param flags: |
56 | + Any iterable object with string versions of resume support flags. |
57 | + This can be None, if the application doesn't wish to enable any of |
58 | + the feature flags. |
59 | + :param location: |
60 | + Location of the session directory. This is the same as |
61 | + ``session_dir`` in the corresponding suspend API. It is also the |
62 | + same as ``storage.location`` (where ``storage`` is a |
63 | + :class:`plainbox.impl.session.storage.SessionStorage` object. |
64 | + |
65 | + Applicable flags are ``FLAG_FILE_REFERENCE_CHECKS_S``, |
66 | + ``FLAG_REWRITE_LOG_PATHNAMES_S`` and ``FLAG_IGNORE_JOB_CHECKSUMS_S``. |
67 | + Their meaning is described below. |
68 | + |
69 | + ``FLAG_FILE_REFERENCE_CHECKS_S``: |
70 | + Flag controlling reference checks from within the session file to |
71 | + external files. If enabled such checks are performed and can cause |
72 | + additional exceptions to be raised. Currently this only affects the |
73 | + representation of the DiskJobResult instances. |
74 | + |
75 | + ``FLAG_REWRITE_LOG_PATHNAMES_S``: |
76 | + Flag controlling rewriting of log file pathnames. It depends on the |
77 | + location to be non-None and then rewrites pathnames of all them |
78 | + missing log files to be relative to the session storage location. |
79 | + It effectively depends on FLAG_FILE_REFERENCE_CHECKS_F being set at |
80 | + the same time, otherwise it is ignored. |
81 | + |
82 | + ``FLAG_IGNORE_JOB_CHECKSUMS_S``: |
83 | + Flag controlling integrity checks between jobs present at resume |
84 | + time and jobs present at suspend time. Since providers cannot be |
85 | + serialized (nor should they) this integrity check prevents anyone |
86 | + from resuming a session if job definitions have changed. Using this |
87 | + flag effectively disables that check. |
88 | """ |
89 | self.job_list = job_list |
90 | logger.debug("Session Resume Helper started with jobs: %r", job_list) |
91 | @@ -254,6 +295,9 @@ |
92 | elif version == 4: |
93 | helper = SessionResumeHelper4( |
94 | self.job_list, self.flags, self.location) |
95 | + elif version == 5: |
96 | + helper = SessionResumeHelper5( |
97 | + self.job_list, self.flags, self.location) |
98 | else: |
99 | raise IncompatibleSessionError( |
100 | _("Unsupported version {}").format(version)) |
101 | @@ -423,6 +467,19 @@ |
102 | """ |
103 | |
104 | |
105 | +class SessionPeekHelper5(SessionPeekHelper4): |
106 | + """ |
107 | + Helper class for implementing session peek feature |
108 | + |
109 | + This class works with data constructed by |
110 | + :class:`~plainbox.impl.session.suspend.SessionSuspendHelper5` which has |
111 | + been pre-processed by :class:`SessionPeekHelper` (to strip the initial |
112 | + envelope). |
113 | + |
114 | + The only goal of this class is to reconstruct session state meta-data. |
115 | + """ |
116 | + |
117 | + |
118 | class SessionResumeHelper1(MetaDataHelper1MixIn): |
119 | """ |
120 | Helper class for implementing session resume feature |
121 | @@ -441,10 +498,10 @@ |
122 | failure modes are possible. Those are documented in :meth:`resume()` |
123 | """ |
124 | |
125 | - # Flag controling reference checks from within the session file to external |
126 | - # files. If enabled such checks are performed and can cause additional |
127 | - # exceptions to be raised. Currently this only affects the representation |
128 | - # of the DiskJobResult instances. |
129 | + # Flag controlling reference checks from within the session file to |
130 | + # external files. If enabled such checks are performed and can cause |
131 | + # additional exceptions to be raised. Currently this only affects the |
132 | + # representation of the DiskJobResult instances. |
133 | FLAG_FILE_REFERENCE_CHECKS_S = 'file-reference-checks' |
134 | FLAG_FILE_REFERENCE_CHECKS_F = 0x01 |
135 | # Flag controlling rewriting of log file pathnames. It depends on the |
136 | @@ -462,9 +519,27 @@ |
137 | FLAG_IGNORE_JOB_CHECKSUMS_S = 'ignore-job-checksums' |
138 | FLAG_IGNORE_JOB_CHECKSUMS_F = 0x04 |
139 | |
140 | - def __init__(self, job_list, flags=None, location=None): |
141 | + def __init__( |
142 | + self, job_list: 'List[JobDefinition]', |
143 | + flags: 'Optional[Iterable[str]]', location: 'Optional[str]' |
144 | + ): |
145 | """ |
146 | - Initialize the helper with a list of known jobs. |
147 | + Initialize the helper with a list of known jobs and support data. |
148 | + |
149 | + :param job_list: |
150 | + List of known jobs |
151 | + :param flags: |
152 | + Any iterable object with string versions of resume support flags. |
153 | + This can be None, if the application doesn't wish to enable any of |
154 | + the feature flags. |
155 | + :param location: |
156 | + Location of the session directory. This is the same as |
157 | + ``session_dir`` in the corresponding suspend API. It is also the |
158 | + same as ``storage.location`` (where ``storage`` is a |
159 | + :class:`plainbox.impl.session.storage.SessionStorage` object. |
160 | + |
161 | + See :meth:`SessionResumeHelper.__init__()` for description and meaning |
162 | + of each flag. |
163 | """ |
164 | self.job_list = job_list |
165 | self.flags = 0 |
166 | @@ -703,7 +778,7 @@ |
167 | raise |
168 | |
169 | @classmethod |
170 | - def _build_JobResult(cls, result_repr, flags=0, location=None): |
171 | + def _build_JobResult(cls, result_repr, flags, location): |
172 | """ |
173 | Convert the representation of MemoryJobResult or DiskJobResult |
174 | back into an actual instance. |
175 | @@ -724,8 +799,8 @@ |
176 | value_none=True) |
177 | # Construct either DiskJobResult or MemoryJobResult |
178 | if 'io_log_filename' in result_repr: |
179 | - io_log_filename = _validate( |
180 | - result_repr, key='io_log_filename', value_type=str) |
181 | + io_log_filename = cls._load_io_log_filename( |
182 | + result_repr, flags, location) |
183 | if (flags & cls.FLAG_FILE_REFERENCE_CHECKS_F |
184 | and not os.path.isfile(io_log_filename) |
185 | and flags & cls.FLAG_REWRITE_LOG_PATHNAMES_F): |
186 | @@ -759,6 +834,10 @@ |
187 | }) |
188 | |
189 | @classmethod |
190 | + def _load_io_log_filename(cls, result_repr, flags, location): |
191 | + return _validate(result_repr, key='io_log_filename', value_type=str) |
192 | + |
193 | + @classmethod |
194 | def _rewrite_pathname(cls, pathname, location): |
195 | return re.sub( |
196 | '.*\/\.cache\/plainbox\/sessions/[^//]+', location, pathname) |
197 | @@ -850,6 +929,35 @@ |
198 | """ |
199 | |
200 | |
201 | +class SessionResumeHelper5(SessionResumeHelper4): |
202 | + """ |
203 | + Helper class for implementing session resume feature |
204 | + |
205 | + This class works with data constructed by |
206 | + :class:`~plainbox.impl.session.suspend.SessionSuspendHelper5` which has |
207 | + been pre-processed by :class:`SessionResumeHelper` (to strip the initial |
208 | + envelope). |
209 | + |
210 | + Due to the constraints of what can be represented in a suspended session, |
211 | + this class cannot work in isolation. It must operate with a list of know |
212 | + jobs. |
213 | + |
214 | + Since (most of the) jobs are being provided externally (as they represent |
215 | + the non-serialized parts of checkbox or other job providers) several |
216 | + failure modes are possible. Those are documented in :meth:`resume()` |
217 | + """ |
218 | + |
219 | + @classmethod |
220 | + def _load_io_log_filename(cls, result_repr, flags, location): |
221 | + io_log_filename = super()._load_io_log_filename( |
222 | + result_repr, flags, location) |
223 | + if os.path.isabs(io_log_filename): |
224 | + return io_log_filename |
225 | + if location is None: |
226 | + raise ValueError("Location must be a directory name") |
227 | + return os.path.join(location, io_log_filename) |
228 | + |
229 | + |
230 | def _validate(obj, **flags): |
231 | """ |
232 | Multi-purpose extraction and validation function. |
233 | |
234 | === modified file 'plainbox/plainbox/impl/session/suspend.py' |
235 | --- plainbox/plainbox/impl/session/suspend.py 2014-11-13 09:38:20 +0000 |
236 | +++ plainbox/plainbox/impl/session/suspend.py 2015-04-13 18:23:59 +0000 |
237 | @@ -1,13 +1,12 @@ |
238 | # This file is part of Checkbox. |
239 | # |
240 | -# Copyright 2012, 2013 Canonical Ltd. |
241 | +# Copyright 2012-2015 Canonical Ltd. |
242 | # Written by: |
243 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
244 | # |
245 | # Checkbox is free software: you can redistribute it and/or modify |
246 | # it under the terms of the GNU General Public License version 3, |
247 | # as published by the Free Software Foundation. |
248 | - |
249 | # |
250 | # Checkbox is distributed in the hope that it will be useful, |
251 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
252 | @@ -18,6 +17,8 @@ |
253 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
254 | |
255 | """ |
256 | +Implementation of session suspend feature. |
257 | + |
258 | :mod:`plainbox.impl.session.suspend` -- session suspend support |
259 | =============================================================== |
260 | |
261 | @@ -79,12 +80,15 @@ |
262 | :attr:`plainbox.impl.session.state.SessionMetaData.app_id` |
263 | 4) Same as '3' but hollow results are not saved and jobs that only |
264 | have hollow results are not mentioned in the job -> checksum map. |
265 | +5) Same as '4' but DiskJobResult is stored with a relative pathname to the log |
266 | + file if session_dir is provided. |
267 | """ |
268 | |
269 | +import base64 |
270 | import gzip |
271 | import json |
272 | import logging |
273 | -import base64 |
274 | +import os |
275 | |
276 | from plainbox.impl.result import DiskJobResult |
277 | from plainbox.impl.result import MemoryJobResult |
278 | @@ -93,6 +97,7 @@ |
279 | |
280 | |
281 | class SessionSuspendHelper1: |
282 | + |
283 | """ |
284 | Helper class for computing binary representation of a session. |
285 | |
286 | @@ -105,14 +110,24 @@ |
287 | |
288 | VERSION = 1 |
289 | |
290 | - def suspend(self, session): |
291 | + def suspend(self, session, session_dir=None): |
292 | """ |
293 | + Compute suspend representation. |
294 | + |
295 | Compute the data that is saved by :class:`SessionStorage` as a |
296 | part of :meth:`SessionStorage.save_checkpoint()`. |
297 | |
298 | + :param session: |
299 | + The SessionState object to represent. |
300 | + :param session_dir: |
301 | + (optional) The base directory of the session. If this argument is |
302 | + used then it can alter the representation of some objects related |
303 | + to filesystem artefacts. It is recommended to always pass the |
304 | + session directory. |
305 | + |
306 | :returns bytes: the serialized data |
307 | """ |
308 | - json_repr = self._json_repr(session) |
309 | + json_repr = self._json_repr(session, session_dir) |
310 | data = json.dumps( |
311 | json_repr, |
312 | ensure_ascii=False, |
313 | @@ -123,7 +138,7 @@ |
314 | # NOTE: gzip.compress is not deterministic on python3.2 |
315 | return gzip.compress(data) |
316 | |
317 | - def _json_repr(self, session): |
318 | + def _json_repr(self, session, session_dir): |
319 | """ |
320 | Compute the representation of all of the data that needs to be saved. |
321 | |
322 | @@ -144,12 +159,12 @@ |
323 | """ |
324 | return { |
325 | "version": self.VERSION, |
326 | - "session": self._repr_SessionState(session), |
327 | + "session": self._repr_SessionState(session, session_dir), |
328 | } |
329 | |
330 | - def _repr_SessionState(self, obj): |
331 | + def _repr_SessionState(self, obj, session_dir): |
332 | """ |
333 | - Compute the representation of :class:`SessionState` |
334 | + Compute the representation of SessionState. |
335 | |
336 | :returns: |
337 | JSON-friendly representation |
338 | @@ -183,18 +198,18 @@ |
339 | "results": { |
340 | # Currently we store only one result but we may store |
341 | # more than that in a later version. |
342 | - state.job.id: [self._repr_JobResult(state.result)] |
343 | + state.job.id: [self._repr_JobResult(state.result, session_dir)] |
344 | for state in obj.job_state_map.values() |
345 | }, |
346 | "desired_job_list": [ |
347 | job.id for job in obj.desired_job_list |
348 | ], |
349 | - "metadata": self._repr_SessionMetaData(obj.metadata), |
350 | + "metadata": self._repr_SessionMetaData(obj.metadata, session_dir), |
351 | } |
352 | |
353 | - def _repr_SessionMetaData(self, obj): |
354 | + def _repr_SessionMetaData(self, obj, session_dir): |
355 | """ |
356 | - Compute the representation of :class:`SessionMetaData`. |
357 | + Compute the representation of SessionMetaData. |
358 | |
359 | :returns: |
360 | JSON-friendly representation. |
361 | @@ -222,21 +237,19 @@ |
362 | "running_job_name": obj.running_job_name |
363 | } |
364 | |
365 | - def _repr_JobResult(self, obj): |
366 | - """ |
367 | - Compute the representation of one of IJobResult subclasses |
368 | - """ |
369 | + def _repr_JobResult(self, obj, session_dir): |
370 | + """ Compute the representation of one of IJobResult subclasses. """ |
371 | if isinstance(obj, DiskJobResult): |
372 | - return self._repr_DiskJobResult(obj) |
373 | + return self._repr_DiskJobResult(obj, session_dir) |
374 | elif isinstance(obj, MemoryJobResult): |
375 | - return self._repr_MemoryJobResult(obj) |
376 | + return self._repr_MemoryJobResult(obj, session_dir) |
377 | else: |
378 | raise TypeError( |
379 | "_repr_JobResult() supports DiskJobResult or MemoryJobResult") |
380 | |
381 | - def _repr_JobResultBase(self, obj): |
382 | + def _repr_JobResultBase(self, obj, session_dir): |
383 | """ |
384 | - Compute the representation of :class:`plainbox.impl.job._JobResultBase` |
385 | + Compute the representation of _JobResultBase. |
386 | |
387 | :returns: |
388 | JSON-friendly representation |
389 | @@ -268,10 +281,9 @@ |
390 | "return_code": obj.return_code, |
391 | } |
392 | |
393 | - def _repr_MemoryJobResult(self, obj): |
394 | + def _repr_MemoryJobResult(self, obj, session_dir): |
395 | """ |
396 | - Compute the representation of |
397 | - :class:`plainbox.impl.job.MemoryJobResult` |
398 | + Compute the representation of MemoryJobResult. |
399 | |
400 | :returns: |
401 | JSON-friendly representation |
402 | @@ -285,16 +297,16 @@ |
403 | Representation of the list of IO Log records |
404 | """ |
405 | assert isinstance(obj, MemoryJobResult) |
406 | - result = self._repr_JobResultBase(obj) |
407 | + result = self._repr_JobResultBase(obj, session_dir) |
408 | result.update({ |
409 | "io_log": [self._repr_IOLogRecord(record) |
410 | for record in obj.io_log], |
411 | }) |
412 | return result |
413 | |
414 | - def _repr_DiskJobResult(self, obj): |
415 | + def _repr_DiskJobResult(self, obj, session_dir): |
416 | """ |
417 | - Compute the representation of :class:`plainbox.impl.job.DiskJobResult` |
418 | + Compute the representation of DiskJobResult. |
419 | |
420 | :returns: |
421 | JSON-friendly representation |
422 | @@ -308,7 +320,7 @@ |
423 | The name of the file that keeps the serialized IO log |
424 | """ |
425 | assert isinstance(obj, DiskJobResult) |
426 | - result = self._repr_JobResultBase(obj) |
427 | + result = self._repr_JobResultBase(obj, session_dir) |
428 | result.update({ |
429 | "io_log_filename": obj.io_log_filename, |
430 | }) |
431 | @@ -316,8 +328,7 @@ |
432 | |
433 | def _repr_IOLogRecord(self, obj): |
434 | """ |
435 | - Compute the representation of |
436 | - :class:`plainbox.impl.result.IOLogRecord` |
437 | + Compute the representation of IOLogRecord. |
438 | |
439 | :returns: |
440 | JSON-friendly representation |
441 | @@ -337,6 +348,7 @@ |
442 | |
443 | |
444 | class SessionSuspendHelper2(SessionSuspendHelper1): |
445 | + |
446 | """ |
447 | Helper class for computing binary representation of a session. |
448 | |
449 | @@ -349,7 +361,7 @@ |
450 | |
451 | VERSION = 2 |
452 | |
453 | - def _repr_SessionMetaData(self, obj): |
454 | + def _repr_SessionMetaData(self, obj, session_dir): |
455 | """ |
456 | Compute the representation of :class:`SessionMetaData`. |
457 | |
458 | @@ -377,7 +389,8 @@ |
459 | Arbitrary application specific binary blob encoded with base64. |
460 | This field may be null. |
461 | """ |
462 | - data = super(SessionSuspendHelper2, self)._repr_SessionMetaData(obj) |
463 | + data = super(SessionSuspendHelper2, self)._repr_SessionMetaData( |
464 | + obj, session_dir) |
465 | if obj.app_blob is None: |
466 | data['app_blob'] = None |
467 | else: |
468 | @@ -388,6 +401,7 @@ |
469 | |
470 | |
471 | class SessionSuspendHelper3(SessionSuspendHelper2): |
472 | + |
473 | """ |
474 | Helper class for computing binary representation of a session. |
475 | |
476 | @@ -400,7 +414,7 @@ |
477 | |
478 | VERSION = 3 |
479 | |
480 | - def _repr_SessionMetaData(self, obj): |
481 | + def _repr_SessionMetaData(self, obj, session_dir): |
482 | """ |
483 | Compute the representation of :class:`SessionMetaData`. |
484 | |
485 | @@ -432,12 +446,14 @@ |
486 | A string identifying the application that stored app_blob. |
487 | Thirs field may be null. |
488 | """ |
489 | - data = super(SessionSuspendHelper3, self)._repr_SessionMetaData(obj) |
490 | + data = super(SessionSuspendHelper3, self)._repr_SessionMetaData( |
491 | + obj, session_dir) |
492 | data['app_id'] = obj.app_id |
493 | return data |
494 | |
495 | |
496 | class SessionSuspendHelper4(SessionSuspendHelper3): |
497 | + |
498 | """ |
499 | Helper class for computing binary representation of a session. |
500 | |
501 | @@ -450,9 +466,9 @@ |
502 | |
503 | VERSION = 4 |
504 | |
505 | - def _repr_SessionState(self, obj): |
506 | + def _repr_SessionState(self, obj, session_dir): |
507 | """ |
508 | - Compute the representation of :class:`SessionState` |
509 | + Compute the representation of :class:`SessionState`. |
510 | |
511 | :returns: |
512 | JSON-friendly representation |
513 | @@ -496,16 +512,53 @@ |
514 | "results": { |
515 | # Currently we store only one result but we may store |
516 | # more than that in a later version. |
517 | - state.job.id: [self._repr_JobResult(state.result)] |
518 | + state.job.id: [self._repr_JobResult(state.result, session_dir)] |
519 | for state in obj.job_state_map.values() |
520 | if not state.result.is_hollow |
521 | }, |
522 | "desired_job_list": [ |
523 | job.id for job in obj.desired_job_list |
524 | ], |
525 | - "metadata": self._repr_SessionMetaData(obj.metadata), |
526 | + "metadata": self._repr_SessionMetaData(obj.metadata, session_dir), |
527 | } |
528 | |
529 | |
530 | +class SessionSuspendHelper5(SessionSuspendHelper4): |
531 | + |
532 | + """ |
533 | + Helper class for computing binary representation of a session. |
534 | + |
535 | + The helper only creates a bytes object to save. Actual saving should |
536 | + be performed using some other means, preferably using |
537 | + :class:`~plainbox.impl.session.storage.SessionStorage`. |
538 | + |
539 | + This class creates version '5' snapshots. |
540 | + """ |
541 | + |
542 | + VERSION = 5 |
543 | + |
544 | + def _repr_DiskJobResult(self, obj, session_dir): |
545 | + """ |
546 | + Compute the representation of DiskJobResult. |
547 | + |
548 | + :returns: |
549 | + JSON-friendly representation |
550 | + :rtype: |
551 | + dict |
552 | + |
553 | + The dictionary has the following keys *in addition to* what is |
554 | + produced by :meth:`_repr_JobResultBase()`: |
555 | + |
556 | + ``io_log_filename`` |
557 | + The path of the file that keeps the serialized IO log relative |
558 | + to the session directory. |
559 | + """ |
560 | + result = super()._repr_DiskJobResult(obj, session_dir) |
561 | + if session_dir is not None: |
562 | + result["io_log_filename"] = os.path.relpath( |
563 | + obj.io_log_filename, session_dir) |
564 | + return result |
565 | + |
566 | + |
567 | # Alias for the most recent version |
568 | -SessionSuspendHelper = SessionSuspendHelper4 |
569 | +SessionSuspendHelper = SessionSuspendHelper5 |
570 | |
571 | === modified file 'plainbox/plainbox/impl/session/test_manager.py' |
572 | --- plainbox/plainbox/impl/session/test_manager.py 2015-02-25 23:54:36 +0000 |
573 | +++ plainbox/plainbox/impl/session/test_manager.py 2015-04-13 18:23:59 +0000 |
574 | @@ -93,7 +93,8 @@ |
575 | # Ensure that a fresh instance of the suspend helper was used to |
576 | # call the suspend() method and that the session state parameter |
577 | # was passed to it. |
578 | - helper_cls().suspend.assert_called_with(self.context.state) |
579 | + helper_cls().suspend.assert_called_with( |
580 | + self.context.state, self.storage.location) |
581 | # Ensure that save_checkpoint() was called on the storage object with |
582 | # the return value of what the suspend helper produced. |
583 | self.storage.save_checkpoint.assert_called_with( |
584 | |
585 | === modified file 'plainbox/plainbox/impl/session/test_resume.py' |
586 | --- plainbox/plainbox/impl/session/test_resume.py 2015-01-26 15:38:31 +0000 |
587 | +++ plainbox/plainbox/impl/session/test_resume.py 2015-04-13 18:23:59 +0000 |
588 | @@ -48,6 +48,7 @@ |
589 | from plainbox.impl.session.resume import SessionResumeHelper2 |
590 | from plainbox.impl.session.resume import SessionResumeHelper3 |
591 | from plainbox.impl.session.resume import SessionResumeHelper4 |
592 | +from plainbox.impl.session.resume import SessionResumeHelper5 |
593 | from plainbox.impl.session.state import SessionState |
594 | from plainbox.impl.testing_utils import make_job |
595 | from plainbox.testing_utils.testcases import TestCaseWithParameters |
596 | @@ -122,7 +123,7 @@ |
597 | b'{"session":{"desired_job_list":[],"jobs":{},"metadata":' |
598 | b'{"app_blob":null,"flags":[],"running_job_name":null,"title":null' |
599 | b'},"results":{}},"version":1}') |
600 | - SessionResumeHelper([]).resume(data) |
601 | + SessionResumeHelper([], None, None).resume(data) |
602 | mocked_helper1.resume_json.assertCalledOnce() |
603 | |
604 | @mock.patch('plainbox.impl.session.resume.SessionResumeHelper2') |
605 | @@ -131,7 +132,7 @@ |
606 | b'{"session":{"desired_job_list":[],"jobs":{},"metadata":' |
607 | b'{"app_blob":null,"flags":[],"running_job_name":null,"title":null' |
608 | b'},"results":{}},"version":2}') |
609 | - SessionResumeHelper([]).resume(data) |
610 | + SessionResumeHelper([], None, None).resume(data) |
611 | mocked_helper2.resume_json.assertCalledOnce() |
612 | |
613 | @mock.patch('plainbox.impl.session.resume.SessionResumeHelper3') |
614 | @@ -141,7 +142,7 @@ |
615 | b'{"app_blob":null,"app_id":null,"flags":[],' |
616 | b'"running_job_name":null,"title":null' |
617 | b'},"results":{}},"version":3}') |
618 | - SessionResumeHelper([]).resume(data) |
619 | + SessionResumeHelper([], None, None).resume(data) |
620 | mocked_helper3.resume_json.assertCalledOnce() |
621 | |
622 | @mock.patch('plainbox.impl.session.resume.SessionResumeHelper4') |
623 | @@ -151,15 +152,25 @@ |
624 | b'{"app_blob":null,"app_id":null,"flags":[],' |
625 | b'"running_job_name":null,"title":null' |
626 | b'},"results":{}},"version":4}') |
627 | - SessionResumeHelper([]).resume(data) |
628 | + SessionResumeHelper([], None, None).resume(data) |
629 | mocked_helper4.resume_json.assertCalledOnce() |
630 | |
631 | - def test_resume_dispatch_v5(self): |
632 | - data = gzip.compress( |
633 | - b'{"version":5}') |
634 | + @mock.patch('plainbox.impl.session.resume.SessionResumeHelper5') |
635 | + def test_resume_dispatch_v5(self, mocked_helper5): |
636 | + data = gzip.compress( |
637 | + b'{"session":{"desired_job_list":[],"jobs":{},"metadata":' |
638 | + b'{"app_blob":null,"app_id":null,"flags":[],' |
639 | + b'"running_job_name":null,"title":null' |
640 | + b'},"results":{}},"version":5}') |
641 | + SessionResumeHelper([], None, None).resume(data) |
642 | + mocked_helper5.resume_json.assertCalledOnce() |
643 | + |
644 | + def test_resume_dispatch_v6(self): |
645 | + data = gzip.compress( |
646 | + b'{"version":6}') |
647 | with self.assertRaises(IncompatibleSessionError) as boom: |
648 | - SessionResumeHelper([]).resume(data) |
649 | - self.assertEqual(str(boom.exception), "Unsupported version 5") |
650 | + SessionResumeHelper([], None, None).resume(data) |
651 | + self.assertEqual(str(boom.exception), "Unsupported version 6") |
652 | |
653 | |
654 | class SessionResumeTests(TestCase): |
655 | @@ -177,7 +188,7 @@ |
656 | """ |
657 | data = b"foo" |
658 | with self.assertRaises(CorruptedSessionError) as boom: |
659 | - SessionResumeHelper([]).resume(data) |
660 | + SessionResumeHelper([], None, None).resume(data) |
661 | self.assertIsInstance(boom.exception.__context__, IOError) |
662 | |
663 | def test_resume_garbage_unicode(self): |
664 | @@ -191,7 +202,7 @@ |
665 | b"\xff".decode('UTF-8') |
666 | data = gzip.compress(b"\xff") |
667 | with self.assertRaises(CorruptedSessionError) as boom: |
668 | - SessionResumeHelper([]).resume(data) |
669 | + SessionResumeHelper([], None, None).resume(data) |
670 | self.assertIsInstance(boom.exception.__context__, UnicodeDecodeError) |
671 | |
672 | def test_resume_garbage_json(self): |
673 | @@ -202,7 +213,7 @@ |
674 | """ |
675 | data = gzip.compress(b"{") |
676 | with self.assertRaises(CorruptedSessionError) as boom: |
677 | - SessionResumeHelper([]).resume(data) |
678 | + SessionResumeHelper([], None, None).resume(data) |
679 | self.assertIsInstance(boom.exception.__context__, ValueError) |
680 | |
681 | |
682 | @@ -322,7 +333,7 @@ |
683 | """ |
684 | def early_cb(session): |
685 | self.seen_session = session |
686 | - session = SessionResumeHelper(self.job_list).resume( |
687 | + session = SessionResumeHelper(self.job_list, None, None).resume( |
688 | self.suspend_data, early_cb) |
689 | self.assertIs(session, self.seen_session) |
690 | |
691 | @@ -337,11 +348,12 @@ |
692 | |
693 | parameter_names = ('resume_cls',) |
694 | parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
695 | - (SessionResumeHelper3,)) |
696 | + (SessionResumeHelper3,), (SessionResumeHelper4,), |
697 | + (SessionResumeHelper5,)) |
698 | |
699 | def setUp(self): |
700 | self.session_repr = {} |
701 | - self.helper = self.parameters.resume_cls([]) |
702 | + self.helper = self.parameters.resume_cls([], None, None) |
703 | |
704 | def test_calls_build_SessionState(self): |
705 | """ |
706 | @@ -411,7 +423,8 @@ |
707 | |
708 | parameter_names = ('resume_cls',) |
709 | parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
710 | - (SessionResumeHelper3,)) |
711 | + (SessionResumeHelper3,), (SessionResumeHelper4,), |
712 | + (SessionResumeHelper5,)) |
713 | |
714 | def test_build_IOLogRecord_missing_delay(self): |
715 | """ |
716 | @@ -511,7 +524,7 @@ |
717 | with self.assertRaises(CorruptedSessionError) as boom: |
718 | obj_repr = copy.copy(self.good_repr) |
719 | del obj_repr['outcome'] |
720 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
721 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
722 | self.assertEqual( |
723 | str(boom.exception), "Missing value for key 'outcome'") |
724 | |
725 | @@ -522,7 +535,7 @@ |
726 | with self.assertRaises(CorruptedSessionError) as boom: |
727 | obj_repr = copy.copy(self.good_repr) |
728 | obj_repr['outcome'] = 42 |
729 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
730 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
731 | self.assertEqual( |
732 | str(boom.exception), |
733 | "Value of key 'outcome' is of incorrect type int") |
734 | @@ -535,7 +548,7 @@ |
735 | with self.assertRaises(CorruptedSessionError) as boom: |
736 | obj_repr = copy.copy(self.good_repr) |
737 | obj_repr['outcome'] = 'maybe' |
738 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
739 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
740 | self.assertEqual( |
741 | str(boom.exception), ( |
742 | "Value for key 'outcome' not in allowed set ['crash', 'fail'," |
743 | @@ -549,7 +562,7 @@ |
744 | """ |
745 | obj_repr = copy.copy(self.good_repr) |
746 | obj_repr['outcome'] = None |
747 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
748 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
749 | self.assertEqual(obj.outcome, None) |
750 | |
751 | def test_build_JobResult_restores_outcome(self): |
752 | @@ -558,7 +571,7 @@ |
753 | """ |
754 | obj_repr = copy.copy(self.good_repr) |
755 | obj_repr['outcome'] = 'fail' |
756 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
757 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
758 | self.assertEqual(obj.outcome, 'fail') |
759 | |
760 | def test_build_JobResult_checks_for_missing_comments(self): |
761 | @@ -568,7 +581,7 @@ |
762 | with self.assertRaises(CorruptedSessionError) as boom: |
763 | obj_repr = copy.copy(self.good_repr) |
764 | del obj_repr['comments'] |
765 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
766 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
767 | self.assertEqual( |
768 | str(boom.exception), "Missing value for key 'comments'") |
769 | |
770 | @@ -579,7 +592,7 @@ |
771 | with self.assertRaises(CorruptedSessionError) as boom: |
772 | obj_repr = copy.copy(self.good_repr) |
773 | obj_repr['comments'] = False |
774 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
775 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
776 | self.assertEqual( |
777 | str(boom.exception), |
778 | "Value of key 'comments' is of incorrect type bool") |
779 | @@ -591,7 +604,7 @@ |
780 | """ |
781 | obj_repr = copy.copy(self.good_repr) |
782 | obj_repr['comments'] = None |
783 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
784 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
785 | self.assertEqual(obj.comments, None) |
786 | |
787 | def test_build_JobResult_restores_comments(self): |
788 | @@ -600,7 +613,7 @@ |
789 | """ |
790 | obj_repr = copy.copy(self.good_repr) |
791 | obj_repr['comments'] = 'this is a comment' |
792 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
793 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
794 | self.assertEqual(obj.comments, 'this is a comment') |
795 | |
796 | def test_build_JobResult_checks_for_missing_return_code(self): |
797 | @@ -610,7 +623,7 @@ |
798 | with self.assertRaises(CorruptedSessionError) as boom: |
799 | obj_repr = copy.copy(self.good_repr) |
800 | del obj_repr['return_code'] |
801 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
802 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
803 | self.assertEqual( |
804 | str(boom.exception), "Missing value for key 'return_code'") |
805 | |
806 | @@ -621,7 +634,7 @@ |
807 | with self.assertRaises(CorruptedSessionError) as boom: |
808 | obj_repr = copy.copy(self.good_repr) |
809 | obj_repr['return_code'] = "text" |
810 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
811 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
812 | self.assertEqual( |
813 | str(boom.exception), |
814 | "Value of key 'return_code' is of incorrect type str") |
815 | @@ -633,7 +646,7 @@ |
816 | """ |
817 | obj_repr = copy.copy(self.good_repr) |
818 | obj_repr['return_code'] = None |
819 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
820 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
821 | self.assertEqual(obj.return_code, None) |
822 | |
823 | def test_build_JobResult_restores_return_code(self): |
824 | @@ -642,7 +655,7 @@ |
825 | """ |
826 | obj_repr = copy.copy(self.good_repr) |
827 | obj_repr['return_code'] = 42 |
828 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
829 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
830 | self.assertEqual(obj.return_code, 42) |
831 | |
832 | def test_build_JobResult_checks_for_missing_execution_duration(self): |
833 | @@ -653,7 +666,7 @@ |
834 | with self.assertRaises(CorruptedSessionError) as boom: |
835 | obj_repr = copy.copy(self.good_repr) |
836 | del obj_repr['execution_duration'] |
837 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
838 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
839 | self.assertEqual( |
840 | str(boom.exception), "Missing value for key 'execution_duration'") |
841 | |
842 | @@ -665,7 +678,7 @@ |
843 | with self.assertRaises(CorruptedSessionError) as boom: |
844 | obj_repr = copy.copy(self.good_repr) |
845 | obj_repr['execution_duration'] = "text" |
846 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
847 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
848 | self.assertEqual( |
849 | str(boom.exception), |
850 | "Value of key 'execution_duration' is of incorrect type str") |
851 | @@ -677,7 +690,7 @@ |
852 | """ |
853 | obj_repr = copy.copy(self.good_repr) |
854 | obj_repr['execution_duration'] = None |
855 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
856 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
857 | self.assertEqual(obj.execution_duration, None) |
858 | |
859 | def test_build_JobResult_restores_execution_duration(self): |
860 | @@ -687,7 +700,7 @@ |
861 | """ |
862 | obj_repr = copy.copy(self.good_repr) |
863 | obj_repr['execution_duration'] = 5.1 |
864 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
865 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
866 | self.assertAlmostEqual(obj.execution_duration, 5.1) |
867 | |
868 | |
869 | @@ -701,7 +714,8 @@ |
870 | |
871 | parameter_names = ('resume_cls',) |
872 | parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
873 | - (SessionResumeHelper3,)) |
874 | + (SessionResumeHelper3,), (SessionResumeHelper4,), |
875 | + (SessionResumeHelper5,)) |
876 | good_repr = { |
877 | 'outcome': "pass", |
878 | 'comments': None, |
879 | @@ -711,7 +725,7 @@ |
880 | } |
881 | |
882 | def test_build_JobResult_restores_MemoryJobResult_representations(self): |
883 | - obj = self.parameters.resume_cls._build_JobResult(self.good_repr) |
884 | + obj = self.parameters.resume_cls._build_JobResult(self.good_repr, 0, None) |
885 | self.assertIsInstance(obj, MemoryJobResult) |
886 | |
887 | def test_build_JobResult_checks_for_missing_io_log(self): |
888 | @@ -721,7 +735,7 @@ |
889 | with self.assertRaises(CorruptedSessionError) as boom: |
890 | obj_repr = copy.copy(self.good_repr) |
891 | del obj_repr['io_log'] |
892 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
893 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
894 | self.assertEqual( |
895 | str(boom.exception), "Missing value for key 'io_log'") |
896 | |
897 | @@ -733,7 +747,7 @@ |
898 | with self.assertRaises(CorruptedSessionError) as boom: |
899 | obj_repr = copy.copy(self.good_repr) |
900 | obj_repr['io_log'] = "text" |
901 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
902 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
903 | self.assertEqual( |
904 | str(boom.exception), |
905 | "Value of key 'io_log' is of incorrect type str") |
906 | @@ -746,7 +760,7 @@ |
907 | with self.assertRaises(CorruptedSessionError) as boom: |
908 | obj_repr = copy.copy(self.good_repr) |
909 | obj_repr['io_log'] = None |
910 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
911 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
912 | self.assertEqual( |
913 | str(boom.exception), |
914 | "Value of key 'io_log' cannot be None") |
915 | @@ -758,7 +772,7 @@ |
916 | """ |
917 | obj_repr = copy.copy(self.good_repr) |
918 | obj_repr['io_log'] = [[0.0, 'stdout', '']] |
919 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
920 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
921 | # NOTE: MemoryJobResult.io_log is a property that converts |
922 | # whatever was stored to IOLogRecord and returns a _tuple_ |
923 | # so the original list is not visible |
924 | @@ -767,27 +781,26 @@ |
925 | ])) |
926 | |
927 | |
928 | -class DiskJobResultResumeTests(JobResultResumeMixIn, TestCaseWithParameters): |
929 | - """ |
930 | - Tests for :class:`~plainbox.impl.session.resume.SessionResumeHelper1`, |
931 | - :class:`~plainbox.impl.session.resume.SessionResumeHelper2' and |
932 | - :class:`~plainbox.impl.session.resume.SessionResumeHelper3' and how they |
933 | - handle recreating DiskJobResult form their representations |
934 | - """ |
935 | +class DiskJobResultResumeTestsCommon(JobResultResumeMixIn, TestCaseWithParameters): |
936 | + |
937 | + """ Tests for common behavior of DiskJobResult resume for all formats. """ |
938 | |
939 | parameter_names = ('resume_cls',) |
940 | parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
941 | - (SessionResumeHelper3,)) |
942 | + (SessionResumeHelper3,), (SessionResumeHelper4,), |
943 | + (SessionResumeHelper5,)) |
944 | good_repr = { |
945 | 'outcome': "pass", |
946 | 'comments': None, |
947 | 'return_code': None, |
948 | 'execution_duration': None, |
949 | - 'io_log_filename': "file.txt" |
950 | + # NOTE: path is absolute (realistic data required by most of tests) |
951 | + 'io_log_filename': "/file.txt" |
952 | } |
953 | |
954 | def test_build_JobResult_restores_DiskJobResult_representations(self): |
955 | - obj = self.parameters.resume_cls._build_JobResult(self.good_repr) |
956 | + obj = self.parameters.resume_cls._build_JobResult( |
957 | + self.good_repr, 0, None) |
958 | self.assertIsInstance(obj, DiskJobResult) |
959 | |
960 | def test_build_JobResult_does_not_check_for_missing_io_log_filename(self): |
961 | @@ -799,7 +812,7 @@ |
962 | with self.assertRaises(CorruptedSessionError) as boom: |
963 | obj_repr = copy.copy(self.good_repr) |
964 | del obj_repr['io_log_filename'] |
965 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
966 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
967 | # NOTE: the error message explicitly talks about 'io_log', not |
968 | # about 'io_log_filename' because we're hitting the other path |
969 | # of the restore function |
970 | @@ -814,7 +827,7 @@ |
971 | with self.assertRaises(CorruptedSessionError) as boom: |
972 | obj_repr = copy.copy(self.good_repr) |
973 | obj_repr['io_log_filename'] = False |
974 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
975 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
976 | self.assertEqual( |
977 | str(boom.exception), |
978 | "Value of key 'io_log_filename' is of incorrect type bool") |
979 | @@ -827,20 +840,87 @@ |
980 | with self.assertRaises(CorruptedSessionError) as boom: |
981 | obj_repr = copy.copy(self.good_repr) |
982 | obj_repr['io_log_filename'] = None |
983 | - self.parameters.resume_cls._build_JobResult(obj_repr) |
984 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
985 | self.assertEqual( |
986 | str(boom.exception), |
987 | "Value of key 'io_log_filename' cannot be None") |
988 | |
989 | - def test_build_JobResult_restores_io_log_filename(self): |
990 | - """ |
991 | - verify that _build_JobResult() restores the value of |
992 | - ``io_log_filename`` DiskJobResult representations |
993 | - """ |
994 | - obj_repr = copy.copy(self.good_repr) |
995 | - obj_repr['io_log_filename'] = "some-file.txt" |
996 | - obj = self.parameters.resume_cls._build_JobResult(obj_repr) |
997 | - self.assertEqual(obj.io_log_filename, "some-file.txt") |
998 | + |
999 | +class DiskJobResultResumeTests1to4(TestCaseWithParameters): |
1000 | + |
1001 | + """ Tests for behavior of DiskJobResult resume for formats 1 to 4. """ |
1002 | + |
1003 | + parameter_names = ('resume_cls',) |
1004 | + parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
1005 | + (SessionResumeHelper3,), (SessionResumeHelper4,)) |
1006 | + good_repr = { |
1007 | + 'outcome': "pass", |
1008 | + 'comments': None, |
1009 | + 'return_code': None, |
1010 | + 'execution_duration': None, |
1011 | + 'io_log_filename': "/file.txt" |
1012 | + } |
1013 | + |
1014 | + def test_build_JobResult_restores_io_log_filename(self): |
1015 | + """ _build_JobResult() accepts relative paths without location. """ |
1016 | + obj_repr = copy.copy(self.good_repr) |
1017 | + obj_repr['io_log_filename'] = "some-file.txt" |
1018 | + obj = self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
1019 | + self.assertEqual(obj.io_log_filename, "some-file.txt") |
1020 | + |
1021 | + def test_build_JobResult_restores_relative_io_log_filename(self): |
1022 | + """ _build_JobResult() ignores location for relative paths. """ |
1023 | + obj_repr = copy.copy(self.good_repr) |
1024 | + obj_repr['io_log_filename'] = "some-file.txt" |
1025 | + obj = self.parameters.resume_cls._build_JobResult( |
1026 | + obj_repr, 0, '/path/to') |
1027 | + self.assertEqual(obj.io_log_filename, "some-file.txt") |
1028 | + |
1029 | + def test_build_JobResult_restores_absolute_io_log_filename(self): |
1030 | + """ _build_JobResult() preserves absolute paths. """ |
1031 | + obj_repr = copy.copy(self.good_repr) |
1032 | + obj_repr['io_log_filename'] = "/some-file.txt" |
1033 | + obj = self.parameters.resume_cls._build_JobResult( |
1034 | + obj_repr, 0, '/path/to') |
1035 | + self.assertEqual(obj.io_log_filename, "/some-file.txt") |
1036 | + |
1037 | + |
1038 | +class DiskJobResultResumeTests5(TestCaseWithParameters): |
1039 | + |
1040 | + """ Tests for behavior of DiskJobResult resume for format 5. """ |
1041 | + |
1042 | + parameter_names = ('resume_cls',) |
1043 | + parameter_values = ((SessionResumeHelper5,),) |
1044 | + good_repr = { |
1045 | + 'outcome': "pass", |
1046 | + 'comments': None, |
1047 | + 'return_code': None, |
1048 | + 'execution_duration': None, |
1049 | + 'io_log_filename': "/file.txt" |
1050 | + } |
1051 | + |
1052 | + def test_build_JobResult_restores_io_log_filename(self): |
1053 | + """ _build_JobResult() rejects relative paths without location. """ |
1054 | + obj_repr = copy.copy(self.good_repr) |
1055 | + obj_repr['io_log_filename'] = "some-file.txt" |
1056 | + with self.assertRaisesRegex(ValueError, "Location "): |
1057 | + self.parameters.resume_cls._build_JobResult(obj_repr, 0, None) |
1058 | + |
1059 | + def test_build_JobResult_restores_relative_io_log_filename(self): |
1060 | + """ _build_JobResult() uses location for relative paths. """ |
1061 | + obj_repr = copy.copy(self.good_repr) |
1062 | + obj_repr['io_log_filename'] = "some-file.txt" |
1063 | + obj = self.parameters.resume_cls._build_JobResult( |
1064 | + obj_repr, 0, '/path/to') |
1065 | + self.assertEqual(obj.io_log_filename, "/path/to/some-file.txt") |
1066 | + |
1067 | + def test_build_JobResult_restores_absolute_io_log_filename(self): |
1068 | + """ _build_JobResult() preserves absolute paths. """ |
1069 | + obj_repr = copy.copy(self.good_repr) |
1070 | + obj_repr['io_log_filename'] = "/some-file.txt" |
1071 | + obj = self.parameters.resume_cls._build_JobResult( |
1072 | + obj_repr, 0, '/path/to') |
1073 | + self.assertEqual(obj.io_log_filename, "/some-file.txt") |
1074 | |
1075 | |
1076 | class DesiredJobListResumeTests(TestCaseWithParameters): |
1077 | @@ -853,7 +933,8 @@ |
1078 | |
1079 | parameter_names = ('resume_cls',) |
1080 | parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
1081 | - (SessionResumeHelper3,)) |
1082 | + (SessionResumeHelper3,), (SessionResumeHelper4,), |
1083 | + (SessionResumeHelper5,)) |
1084 | |
1085 | def setUp(self): |
1086 | # All of the tests need a SessionState object and some jobs to work |
1087 | @@ -1229,7 +1310,8 @@ |
1088 | |
1089 | parameter_names = ('resume_cls',) |
1090 | parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
1091 | - (SessionResumeHelper3,)) |
1092 | + (SessionResumeHelper3,), (SessionResumeHelper4,), |
1093 | + (SessionResumeHelper5,)) |
1094 | |
1095 | def setUp(self): |
1096 | self.job_id = 'job' |
1097 | @@ -1246,7 +1328,7 @@ |
1098 | 'io_log': [], |
1099 | }] |
1100 | } |
1101 | - self.helper = self.parameters.resume_cls([self.job]) |
1102 | + self.helper = self.parameters.resume_cls([self.job], None, None) |
1103 | # This object is artificial and would be constructed internally |
1104 | # by the helper but having it here makes testing easier as we |
1105 | # can reliably test a single method in isolation. |
1106 | @@ -1392,7 +1474,8 @@ |
1107 | |
1108 | parameter_names = ('resume_cls',) |
1109 | parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
1110 | - (SessionResumeHelper3,)) |
1111 | + (SessionResumeHelper3,), (SessionResumeHelper4,), |
1112 | + (SessionResumeHelper5,)) |
1113 | |
1114 | def test_process_job_restores_resources(self): |
1115 | """ |
1116 | @@ -1422,7 +1505,7 @@ |
1117 | ], |
1118 | }] |
1119 | } |
1120 | - helper = self.parameters.resume_cls([job]) |
1121 | + helper = self.parameters.resume_cls([job], None, None) |
1122 | session = SessionState([job]) |
1123 | # Ensure that the resource was not there initially |
1124 | self.assertNotIn(job_id, session.resource_map) |
1125 | @@ -1466,7 +1549,7 @@ |
1126 | ], |
1127 | }] |
1128 | } |
1129 | - helper = self.parameters.resume_cls([job]) |
1130 | + helper = self.parameters.resume_cls([job], None, None) |
1131 | session = SessionState([job]) |
1132 | # Ensure that the 'generated' job was not there initially |
1133 | self.assertNotIn('generated', session.job_state_map) |
1134 | @@ -1493,7 +1576,8 @@ |
1135 | |
1136 | parameter_names = ('resume_cls',) |
1137 | parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
1138 | - (SessionResumeHelper3,)) |
1139 | + (SessionResumeHelper3,), (SessionResumeHelper4,), |
1140 | + (SessionResumeHelper5,)) |
1141 | |
1142 | def test_empty_session(self): |
1143 | """ |
1144 | @@ -1506,7 +1590,7 @@ |
1145 | 'jobs': {}, |
1146 | 'results': {} |
1147 | } |
1148 | - helper = self.parameters.resume_cls([]) |
1149 | + helper = self.parameters.resume_cls([], None, None) |
1150 | session = SessionState([]) |
1151 | helper._restore_SessionState_jobs_and_results(session, session_repr) |
1152 | self.assertEqual(session.job_list, []) |
1153 | @@ -1534,7 +1618,7 @@ |
1154 | }] |
1155 | } |
1156 | } |
1157 | - helper = self.parameters.resume_cls([]) |
1158 | + helper = self.parameters.resume_cls([], None, None) |
1159 | session = SessionState([job]) |
1160 | helper._restore_SessionState_jobs_and_results(session, session_repr) |
1161 | # Session still has one job in it |
1162 | @@ -1587,7 +1671,7 @@ |
1163 | } |
1164 | } |
1165 | # We only pass the parent to the helper! Child will be re-created |
1166 | - helper = self.parameters.resume_cls([parent]) |
1167 | + helper = self.parameters.resume_cls([parent], None, None) |
1168 | session = SessionState([parent]) |
1169 | helper._restore_SessionState_jobs_and_results(session, session_repr) |
1170 | # We should now have two jobs, parent and child |
1171 | @@ -1671,7 +1755,7 @@ |
1172 | } |
1173 | # We only pass the parent to the helper! |
1174 | # The 'child' and 'grandchild' jobs will be re-created |
1175 | - helper = self.parameters.resume_cls([parent]) |
1176 | + helper = self.parameters.resume_cls([parent], None, None) |
1177 | session = SessionState([parent]) |
1178 | helper._restore_SessionState_jobs_and_results(session, session_repr) |
1179 | # We should now have two jobs, parent and child |
1180 | @@ -1692,7 +1776,7 @@ |
1181 | 'job-id': [] |
1182 | } |
1183 | } |
1184 | - helper = self.parameters.resume_cls([]) |
1185 | + helper = self.parameters.resume_cls([], None, None) |
1186 | session = SessionState([]) |
1187 | with self.assertRaises(CorruptedSessionError) as boom: |
1188 | helper._restore_SessionState_jobs_and_results( |
1189 | @@ -1712,7 +1796,8 @@ |
1190 | |
1191 | parameter_names = ('resume_cls',) |
1192 | parameter_values = ((SessionResumeHelper1,), (SessionResumeHelper2,), |
1193 | - (SessionResumeHelper3,), (SessionResumeHelper4,)) |
1194 | + (SessionResumeHelper3,), (SessionResumeHelper4,), |
1195 | + (SessionResumeHelper5,)) |
1196 | |
1197 | def test_simple_session(self): |
1198 | """ |
1199 | @@ -1731,7 +1816,7 @@ |
1200 | job_a.id: [], |
1201 | } |
1202 | } |
1203 | - helper = self.parameters.resume_cls([job_a, job_b]) |
1204 | + helper = self.parameters.resume_cls([job_a, job_b], None, None) |
1205 | session = SessionState([job_a, job_b]) |
1206 | helper._restore_SessionState_job_list(session, session_repr) |
1207 | # Job "a" is still in the list but job "b" got removed |
1208 | @@ -1760,7 +1845,7 @@ |
1209 | 'results': {}, # nothing ran yet |
1210 | }, |
1211 | } |
1212 | - helper = SessionResumeHelper4([job_a, job_a_dep, job_unrelated]) |
1213 | + helper = SessionResumeHelper4([job_a, job_a_dep, job_unrelated], None, None) |
1214 | # Mock away meta-data restore code as we're not testing that |
1215 | with mock.patch.object(helper, '_restore_SessionState_metadata'): |
1216 | session = helper.resume_json(session_repr) |
1217 | @@ -1787,7 +1872,7 @@ |
1218 | 'results': {}, # nothing ran yet |
1219 | } |
1220 | } |
1221 | - helper = SessionResumeHelper4([job_a]) |
1222 | + helper = SessionResumeHelper4([job_a], None, None) |
1223 | # Mock away meta-data restore code as we're not testing that |
1224 | with mock.patch.object(helper, '_restore_SessionState_metadata'): |
1225 | session = helper.resume_json(session_repr) |
1226 | |
1227 | === modified file 'plainbox/plainbox/impl/session/test_suspend.py' |
1228 | --- plainbox/plainbox/impl/session/test_suspend.py 2015-01-07 14:52:42 +0000 |
1229 | +++ plainbox/plainbox/impl/session/test_suspend.py 2015-04-13 18:23:59 +0000 |
1230 | @@ -39,6 +39,7 @@ |
1231 | from plainbox.impl.session.suspend import SessionSuspendHelper2 |
1232 | from plainbox.impl.session.suspend import SessionSuspendHelper3 |
1233 | from plainbox.impl.session.suspend import SessionSuspendHelper4 |
1234 | +from plainbox.impl.session.suspend import SessionSuspendHelper5 |
1235 | from plainbox.impl.testing_utils import make_job |
1236 | from plainbox.vendor import mock |
1237 | |
1238 | @@ -60,7 +61,7 @@ |
1239 | """ |
1240 | |
1241 | def setUp(self): |
1242 | - self.helper = SessionSuspendHelper1() |
1243 | + self.helper = self.HELPER_CLS() |
1244 | self.empty_result = self.TESTED_CLS({}) |
1245 | self.typical_result = self.TESTED_CLS({ |
1246 | "outcome": self.TESTED_CLS.OUTCOME_PASS, |
1247 | @@ -69,39 +70,40 @@ |
1248 | "return_code": 1, |
1249 | # NOTE: those are actually specific to TESTED_CLS but it is |
1250 | # a simple hack that gets the job done |
1251 | - "io_log_filename": "/nonexistent.log", |
1252 | + "io_log_filename": "/path/to/log.txt", |
1253 | "io_log": [ |
1254 | (0, 'stdout', b'first part\n'), |
1255 | (0.1, 'stdout', b'second part\n'), |
1256 | ] |
1257 | }) |
1258 | + self.session_dir = None |
1259 | |
1260 | def test_repr_xxxJobResult_outcome(self): |
1261 | """ |
1262 | verify that DiskJobResult.outcome is serialized correctly |
1263 | """ |
1264 | - data = self.repr_method(self.typical_result) |
1265 | + data = self.repr_method(self.typical_result, self.session_dir) |
1266 | self.assertEqual(data['outcome'], DiskJobResult.OUTCOME_PASS) |
1267 | |
1268 | def test_repr_xxxJobResult_execution_duration(self): |
1269 | """ |
1270 | verify that DiskJobResult.execution_duration is serialized correctly |
1271 | """ |
1272 | - data = self.repr_method(self.typical_result) |
1273 | + data = self.repr_method(self.typical_result, self.session_dir) |
1274 | self.assertAlmostEqual(data['execution_duration'], 42.5) |
1275 | |
1276 | def test_repr_xxxJobResult_comments(self): |
1277 | """ |
1278 | verify that DiskJobResult.comments is serialized correctly |
1279 | """ |
1280 | - data = self.repr_method(self.typical_result) |
1281 | + data = self.repr_method(self.typical_result, self.session_dir) |
1282 | self.assertEqual(data['comments'], "the screen was corrupted") |
1283 | |
1284 | def test_repr_xxxJobResult_return_code(self): |
1285 | """ |
1286 | verify that DiskJobResult.return_code is serialized correctly |
1287 | """ |
1288 | - data = self.repr_method(self.typical_result) |
1289 | + data = self.repr_method(self.typical_result, self.session_dir) |
1290 | self.assertEqual(data['return_code'], 1) |
1291 | |
1292 | |
1293 | @@ -111,6 +113,7 @@ |
1294 | """ |
1295 | |
1296 | TESTED_CLS = MemoryJobResult |
1297 | + HELPER_CLS = SessionSuspendHelper1 |
1298 | |
1299 | def setUp(self): |
1300 | super(SuspendMemoryJobResultTests, self).setUp() |
1301 | @@ -120,7 +123,7 @@ |
1302 | """ |
1303 | verify that the representation of an empty MemoryJobResult is okay |
1304 | """ |
1305 | - data = self.repr_method(self.empty_result) |
1306 | + data = self.repr_method(self.empty_result, self.session_dir) |
1307 | self.assertEqual(data, { |
1308 | "outcome": None, |
1309 | "execution_duration": None, |
1310 | @@ -133,7 +136,8 @@ |
1311 | """ |
1312 | verify that MemoryJobResult.io_log is serialized correctly |
1313 | """ |
1314 | - data = self.helper._repr_MemoryJobResult(self.typical_result) |
1315 | + data = self.helper._repr_MemoryJobResult( |
1316 | + self.typical_result, self.session_dir) |
1317 | self.assertEqual(data['io_log'], [ |
1318 | [0, 'stdout', 'Zmlyc3QgcGFydAo='], |
1319 | [0.1, 'stdout', 'c2Vjb25kIHBhcnQK'], |
1320 | @@ -146,6 +150,7 @@ |
1321 | """ |
1322 | |
1323 | TESTED_CLS = DiskJobResult |
1324 | + HELPER_CLS = SessionSuspendHelper1 |
1325 | |
1326 | def setUp(self): |
1327 | super(SuspendDiskJobResultTests, self).setUp() |
1328 | @@ -155,7 +160,7 @@ |
1329 | """ |
1330 | verify that the representation of an empty DiskJobResult is okay |
1331 | """ |
1332 | - data = self.repr_method(self.empty_result) |
1333 | + data = self.repr_method(self.empty_result, self.session_dir) |
1334 | self.assertEqual(data, { |
1335 | "outcome": None, |
1336 | "execution_duration": None, |
1337 | @@ -168,8 +173,30 @@ |
1338 | """ |
1339 | verify that DiskJobResult.io_log_filename is serialized correctly |
1340 | """ |
1341 | - data = self.helper._repr_DiskJobResult(self.typical_result) |
1342 | - self.assertEqual(data['io_log_filename'], "/nonexistent.log") |
1343 | + data = self.helper._repr_DiskJobResult( |
1344 | + self.typical_result, self.session_dir) |
1345 | + self.assertEqual(data['io_log_filename'], "/path/to/log.txt") |
1346 | + |
1347 | + |
1348 | +class Suspend5DiskJobResultTests(SuspendDiskJobResultTests): |
1349 | + """ |
1350 | + Tests that check how DiskJobResult is represented by SessionSuspendHelper5 |
1351 | + """ |
1352 | + |
1353 | + TESTED_CLS = DiskJobResult |
1354 | + HELPER_CLS = SessionSuspendHelper5 |
1355 | + |
1356 | + def test_repr_DiskJobResult_io_log_filename__no_session_dir(self): |
1357 | + """ io_log_filename is absolute in session_dir is not used. """ |
1358 | + data = self.helper._repr_DiskJobResult( |
1359 | + self.typical_result, None) |
1360 | + self.assertEqual(data['io_log_filename'], "/path/to/log.txt") |
1361 | + |
1362 | + def test_repr_DiskJobResult_io_log_filename__session_dir(self): |
1363 | + """ io_log_filename is relative if session_dir is used. """ |
1364 | + data = self.helper._repr_DiskJobResult( |
1365 | + self.typical_result, "/path/to") |
1366 | + self.assertEqual(data['io_log_filename'], "log.txt") |
1367 | |
1368 | |
1369 | class SessionSuspendHelper1Tests(TestCase): |
1370 | @@ -179,6 +206,7 @@ |
1371 | |
1372 | def setUp(self): |
1373 | self.helper = SessionSuspendHelper1() |
1374 | + self.session_dir = None |
1375 | |
1376 | def test_repr_IOLogRecord(self): |
1377 | """ |
1378 | @@ -195,7 +223,7 @@ |
1379 | calls _repr_MemoryJobResult |
1380 | """ |
1381 | result = MemoryJobResult({}) |
1382 | - self.helper._repr_JobResult(result) |
1383 | + self.helper._repr_JobResult(result, self.session_dir) |
1384 | mocked_helper._repr_MemoryJobResult.assertCalledOnceWith(result) |
1385 | |
1386 | @mock.patch('plainbox.impl.session.suspend.SessionSuspendHelper') |
1387 | @@ -205,7 +233,7 @@ |
1388 | calls _repr_DiskJobResult |
1389 | """ |
1390 | result = DiskJobResult({}) |
1391 | - self.helper._repr_JobResult(result) |
1392 | + self.helper._repr_JobResult(result, self.session_dir) |
1393 | mocked_helper._repr_DiskJobResult.assertCalledOnceWith(result) |
1394 | |
1395 | def test_repr_JobResult_with_junk(self): |
1396 | @@ -221,7 +249,8 @@ |
1397 | verify that representation of empty SessionMetaData is okay |
1398 | """ |
1399 | # all defaults with empty values |
1400 | - data = self.helper._repr_SessionMetaData(SessionMetaData()) |
1401 | + data = self.helper._repr_SessionMetaData( |
1402 | + SessionMetaData(), self.session_dir) |
1403 | self.assertEqual(data, { |
1404 | 'title': None, |
1405 | 'flags': [], |
1406 | @@ -237,7 +266,7 @@ |
1407 | title='USB Testing session', |
1408 | flags=['incomplete'], |
1409 | running_job_name='usb/detect' |
1410 | - )) |
1411 | + ), self.session_dir) |
1412 | self.assertEqual(data, { |
1413 | 'title': 'USB Testing session', |
1414 | 'flags': ['incomplete'], |
1415 | @@ -248,7 +277,8 @@ |
1416 | """ |
1417 | verify that representation of empty SessionState is okay |
1418 | """ |
1419 | - data = self.helper._repr_SessionState(SessionState([])) |
1420 | + data = self.helper._repr_SessionState( |
1421 | + SessionState([]), self.session_dir) |
1422 | self.assertEqual(data, { |
1423 | 'jobs': {}, |
1424 | 'results': {}, |
1425 | @@ -264,28 +294,28 @@ |
1426 | """ |
1427 | verify that the json representation has the 'version' field |
1428 | """ |
1429 | - data = self.helper._json_repr(SessionState([])) |
1430 | + data = self.helper._json_repr(SessionState([]), self.session_dir) |
1431 | self.assertIn("version", data) |
1432 | |
1433 | def test_json_repr_current_version(self): |
1434 | """ |
1435 | verify what the version field is |
1436 | """ |
1437 | - data = self.helper._json_repr(SessionState([])) |
1438 | + data = self.helper._json_repr(SessionState([]), self.session_dir) |
1439 | self.assertEqual(data['version'], 1) |
1440 | |
1441 | def test_json_repr_stores_session_state(self): |
1442 | """ |
1443 | verify that the json representation has the 'session' field |
1444 | """ |
1445 | - data = self.helper._json_repr(SessionState([])) |
1446 | + data = self.helper._json_repr(SessionState([]), self.session_dir) |
1447 | self.assertIn("session", data) |
1448 | |
1449 | def test_suspend(self): |
1450 | """ |
1451 | verify that the suspend() method returns gzipped JSON representation |
1452 | """ |
1453 | - data = self.helper.suspend(SessionState([])) |
1454 | + data = self.helper.suspend(SessionState([]), self.session_dir) |
1455 | # XXX: we cannot really test what the compressed data looks like |
1456 | # because apparently python3.2 gzip output is non-deterministic. |
1457 | # It seems to be an instance of the gzip bug that was fixed a few |
1458 | @@ -321,6 +351,7 @@ |
1459 | """ |
1460 | |
1461 | def setUp(self): |
1462 | + self.session_dir = None |
1463 | # Crete a "__category__" job |
1464 | self.category_job = JobDefinition({ |
1465 | "plugin": "local", |
1466 | @@ -387,7 +418,8 @@ |
1467 | # and use the data we've defined so far to create JSON-friendly |
1468 | # description of the session state. |
1469 | self.helper = SessionSuspendHelper1() |
1470 | - self.data = self.helper._repr_SessionState(self.session_state) |
1471 | + self.data = self.helper._repr_SessionState( |
1472 | + self.session_state, self.session_dir) |
1473 | |
1474 | def test_state_tracked_for_all_jobs(self): |
1475 | """ |
1476 | @@ -524,12 +556,13 @@ |
1477 | |
1478 | def setUp(self): |
1479 | self.helper = SessionSuspendHelper2() |
1480 | + self.session_dir = None |
1481 | |
1482 | def test_json_repr_current_version(self): |
1483 | """ |
1484 | verify what the version field is |
1485 | """ |
1486 | - data = self.helper._json_repr(SessionState([])) |
1487 | + data = self.helper._json_repr(SessionState([]), self.session_dir) |
1488 | self.assertEqual(data['version'], 2) |
1489 | |
1490 | def test_repr_SessionMetaData_empty_metadata(self): |
1491 | @@ -537,7 +570,8 @@ |
1492 | verify that representation of empty SessionMetaData is okay |
1493 | """ |
1494 | # all defaults with empty values |
1495 | - data = self.helper._repr_SessionMetaData(SessionMetaData()) |
1496 | + data = self.helper._repr_SessionMetaData( |
1497 | + SessionMetaData(), self.session_dir) |
1498 | self.assertEqual(data, { |
1499 | 'title': None, |
1500 | 'flags': [], |
1501 | @@ -555,7 +589,7 @@ |
1502 | flags=['incomplete'], |
1503 | running_job_name='usb/detect', |
1504 | app_blob=b'blob', |
1505 | - )) |
1506 | + ), self.session_dir) |
1507 | self.assertEqual(data, { |
1508 | 'title': 'USB Testing session', |
1509 | 'flags': ['incomplete'], |
1510 | @@ -567,7 +601,8 @@ |
1511 | """ |
1512 | verify that representation of empty SessionState is okay |
1513 | """ |
1514 | - data = self.helper._repr_SessionState(SessionState([])) |
1515 | + data = self.helper._repr_SessionState( |
1516 | + SessionState([]), self.session_dir) |
1517 | self.assertEqual(data, { |
1518 | 'jobs': {}, |
1519 | 'results': {}, |
1520 | @@ -584,7 +619,8 @@ |
1521 | """ |
1522 | verify that the suspend() method returns gzipped JSON representation |
1523 | """ |
1524 | - data = self.helper.suspend(SessionState([])) |
1525 | + data = self.helper.suspend( |
1526 | + SessionState([]), self.session_dir) |
1527 | # XXX: we cannot really test what the compressed data looks like |
1528 | # because apparently python3.2 gzip output is non-deterministic. |
1529 | # It seems to be an instance of the gzip bug that was fixed a few |
1530 | @@ -609,12 +645,13 @@ |
1531 | |
1532 | def setUp(self): |
1533 | self.helper = SessionSuspendHelper3() |
1534 | + self.session_dir = None |
1535 | |
1536 | def test_json_repr_current_version(self): |
1537 | """ |
1538 | verify what the version field is |
1539 | """ |
1540 | - data = self.helper._json_repr(SessionState([])) |
1541 | + data = self.helper._json_repr(SessionState([]), self.session_dir) |
1542 | self.assertEqual(data['version'], 3) |
1543 | |
1544 | def test_repr_SessionMetaData_empty_metadata(self): |
1545 | @@ -622,7 +659,8 @@ |
1546 | verify that representation of empty SessionMetaData is okay |
1547 | """ |
1548 | # all defaults with empty values |
1549 | - data = self.helper._repr_SessionMetaData(SessionMetaData()) |
1550 | + data = self.helper._repr_SessionMetaData( |
1551 | + SessionMetaData(), self.session_dir) |
1552 | self.assertEqual(data, { |
1553 | 'title': None, |
1554 | 'flags': [], |
1555 | @@ -642,7 +680,7 @@ |
1556 | running_job_name='usb/detect', |
1557 | app_blob=b'blob', |
1558 | app_id='com.canonical.certification.plainbox', |
1559 | - )) |
1560 | + ), self.session_dir) |
1561 | self.assertEqual(data, { |
1562 | 'title': 'USB Testing session', |
1563 | 'flags': ['incomplete'], |
1564 | @@ -655,7 +693,8 @@ |
1565 | """ |
1566 | verify that representation of empty SessionState is okay |
1567 | """ |
1568 | - data = self.helper._repr_SessionState(SessionState([])) |
1569 | + data = self.helper._repr_SessionState( |
1570 | + SessionState([]), self.session_dir) |
1571 | self.assertEqual(data, { |
1572 | 'jobs': {}, |
1573 | 'results': {}, |
1574 | @@ -673,7 +712,7 @@ |
1575 | """ |
1576 | verify that the suspend() method returns gzipped JSON representation |
1577 | """ |
1578 | - data = self.helper.suspend(SessionState([])) |
1579 | + data = self.helper.suspend(SessionState([]), self.session_dir) |
1580 | # XXX: we cannot really test what the compressed data looks like |
1581 | # because apparently python3.2 gzip output is non-deterministic. |
1582 | # It seems to be an instance of the gzip bug that was fixed a few |
1583 | @@ -699,12 +738,13 @@ |
1584 | |
1585 | def setUp(self): |
1586 | self.helper = SessionSuspendHelper4() |
1587 | + self.session_dir = None |
1588 | |
1589 | def test_json_repr_current_version(self): |
1590 | """ |
1591 | verify what the version field is |
1592 | """ |
1593 | - data = self.helper._json_repr(SessionState([])) |
1594 | + data = self.helper._json_repr(SessionState([]), self.session_dir) |
1595 | self.assertEqual(data['version'], 4) |
1596 | |
1597 | def test_repr_SessionState_typical_session(self): |
1598 | @@ -734,7 +774,7 @@ |
1599 | session_state = SessionState([used_job, unused_job]) |
1600 | session_state.update_desired_job_list([used_job]) |
1601 | session_state.update_job_result(used_job, used_result) |
1602 | - data = self.helper._repr_SessionState(session_state) |
1603 | + data = self.helper._repr_SessionState(session_state, self.session_dir) |
1604 | self.assertEqual(data, { |
1605 | 'jobs': { |
1606 | 'used': ('8c393c19fdfde1b6afc5b79d0a1617ecf7531cd832a16450dc' |
1607 | @@ -763,7 +803,7 @@ |
1608 | """ |
1609 | verify that the suspend() method returns gzipped JSON representation |
1610 | """ |
1611 | - data = self.helper.suspend(SessionState([])) |
1612 | + data = self.helper.suspend(SessionState([]), self.session_dir) |
1613 | # XXX: we cannot really test what the compressed data looks like |
1614 | # because apparently python3.2 gzip output is non-deterministic. |
1615 | # It seems to be an instance of the gzip bug that was fixed a few |
1616 | @@ -782,6 +822,45 @@ |
1617 | b'"version":4}')) |
1618 | |
1619 | |
1620 | +class SessionSuspendHelper5Tests(SessionSuspendHelper4Tests): |
1621 | + """ |
1622 | + Tests for various methods of SessionSuspendHelper5 |
1623 | + """ |
1624 | + |
1625 | + def setUp(self): |
1626 | + self.helper = SessionSuspendHelper5() |
1627 | + self.session_dir = None |
1628 | + |
1629 | + def test_json_repr_current_version(self): |
1630 | + """ |
1631 | + verify what the version field is |
1632 | + """ |
1633 | + data = self.helper._json_repr(SessionState([]), self.session_dir) |
1634 | + self.assertEqual(data['version'], 5) |
1635 | + |
1636 | + def test_suspend(self): |
1637 | + """ |
1638 | + verify that the suspend() method returns gzipped JSON representation |
1639 | + """ |
1640 | + data = self.helper.suspend(SessionState([]), self.session_dir) |
1641 | + # XXX: we cannot really test what the compressed data looks like |
1642 | + # because apparently python3.2 gzip output is non-deterministic. |
1643 | + # It seems to be an instance of the gzip bug that was fixed a few |
1644 | + # years ago. |
1645 | + # |
1646 | + # I've filed a bug on python3.2 in Ubuntu and Python upstream project |
1647 | + # https://bugs.launchpad.net/ubuntu/+source/python3.2/+bug/871083 |
1648 | + # |
1649 | + # In the meantime we can only test that we got bytes out |
1650 | + self.assertIsInstance(data, bytes) |
1651 | + # And that we can gzip uncompress them and get what we expected |
1652 | + self.assertEqual(gzip.decompress(data), ( |
1653 | + b'{"session":{"desired_job_list":[],"jobs":{},"metadata":' |
1654 | + b'{"app_blob":null,"app_id":null,"flags":[],' |
1655 | + b'"running_job_name":null,"title":null},"results":{}},' |
1656 | + b'"version":5}')) |
1657 | + |
1658 | + |
1659 | class RegressionTests(TestCase): |
1660 | |
1661 | def test_1388055(self): |
1662 | @@ -800,10 +879,11 @@ |
1663 | self.assertEqual(state.run_list, [job_a_dep, job_a]) |
1664 | self.assertEqual(state.desired_job_list, [job_a]) |
1665 | helper = SessionSuspendHelper4() |
1666 | + session_dir = None |
1667 | # Mock away the meta-data as we're not testing that |
1668 | with mock.patch.object(helper, '_repr_SessionMetaData') as m: |
1669 | m.return_value = 'mocked' |
1670 | - actual = helper._repr_SessionState(state) |
1671 | + actual = helper._repr_SessionState(state, session_dir) |
1672 | expected = { |
1673 | 'jobs': { |
1674 | job_a_dep.id: job_a_dep.checksum, |
LGTM
Big +1 for docstring upgrades.