Merge lp:~zyga/checkbox/fix-1378295 into lp:checkbox

Proposed by Zygmunt Krynicki
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
Reviewer Review Type Date Requested Status
Maciej Kisielewski Approve
Review via email: mp+256007@code.launchpad.net

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 SessionResumeHelper.__init__() args mandatory
c25ea18 plainbox:resume: pass flags and location to _build_JobResult()
47a9850 plainbox:resume: make _build_JobResult(flags, location) mandatory
3a7c7bb plainbox:resume: better docstring for SessionResumeHelper.__init__()
b285ee9 plainbox:resume: better docstring for SessionResumeHelper1.__init__()
e5608b2 plainbox:resume: make SessionResumeHelper1.__init__(...) arguments mandatory
7e73a5a plainbox:resume: enable extra testing for 4th format
29a1c38 plainbox:resume: add SessionResumeHelper1._load_io_log_filename()
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

To post a comment you must log in.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

LGTM
Big +1 for docstring upgrades.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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,

Subscribers

People subscribed via source and target branches