Merge lp:~kissiel/checkbox/suspend-v7 into lp:checkbox
- suspend-v7
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~kissiel/checkbox/suspend-v7 |
Merge into: | lp:checkbox |
Diff against target: |
497 lines (+331/-11) 4 files modified
plainbox/plainbox/impl/session/resume.py (+33/-8) plainbox/plainbox/impl/session/state.py (+27/-2) plainbox/plainbox/impl/session/suspend.py (+125/-1) plainbox/plainbox/impl/session/test_suspend.py (+146/-0) |
To merge this branch: | bzr merge lp:~kissiel/checkbox/suspend-v7 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau | Needs Fixing | ||
Zygmunt Krynicki (community) | Needs Fixing | ||
Review via email: mp+279128@code.launchpad.net |
Commit message
Description of the change
This MR brings v7 of session information.
v7 contains two additions:
- timestamp of when the session was created
- list of the test plan IDs that were selected during session creation
Note that this MR doesn't update any of the applications to use v7.
94a4203 plainbox:session: add 7th suspend format
b5f87a3 plainbox:
9c0db7c plainbox:
Zygmunt Krynicki (zyga) wrote : | # |
16:56 <@zyga> kissiel: I'd like to also see patches that make use fo the new field where it makes sense to do so (SA APIs)
16:57 < kissiel> zyga: I used datetime at first go, but this required custom endcoders and decoders
16:57 < kissiel> this is why I changed that to posix
16:58 <@zyga> kissiel: that's okay, we can handle that on POD assignment
16:59 <@zyga> kissiel: timestamp is infinitely more useful
16:59 <@zyga> kissiel: when it carries a human readable time concept
16:59 <@zyga> kissiel: not when it carries a number
16:59 < kissiel> zyga: so were should the parsing happen?
16:59 < kissiel> zyga: in your opinion?
16:59 <@zyga> kissiel: you can fix that in one place, in the resume code that deserializes session meta-data
16:59 <@zyga> kissiel: it does assignment anyway
17:00 < kissiel> zyga: there's more than 1 place where json.loads happne
17:00 < kissiel> *happen
17:00 <@zyga> kissiel: can you be more specific?
17:00 <@zyga> this field is _assigned_ in one place
17:00 <@zyga> kissiel: json decode can and should just decode a string
17:01 <@zyga> kissiel: there is (and should be) only one place that assigns session_
17:01 <@zyga> *stamp
17:01 < kissiel> mkay
17:01 < kissiel> I think I know what you mean
17:01 <@zyga> kissiel: fantastic, thanks
17:01 < kissiel> this will make use of the default json decoder
17:01 <@zyga> kissiel: well, just decode as before, but re-interpret that one field
Sylvain Pineau (sylvain-pineau) wrote : | # |
There's one issue with resume unit tests, and we would need a test for resuming version 7 and a test to try the next (not yet available) 8 (test_resume_
=======
ERROR: test_resume_
-------
Traceback (most recent call last):
File "/home/
value = obj[key]
KeyError: 'session'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/
SessionResu
File "/home/
return self._resume_
File "/home/
return helper.
File "/home/
session_repr = _validate(
File "/home/
raise CorruptedSessio
plainbox.
Unmerged revisions
- 4119. By Maciej Kisielewski
-
plainbox:
session: resume: bump date in header Signed-off-by: Maciej Kisielewski <email address hidden>
- 4118. By Maciej Kisielewski
-
plainbox:
session: resume: fix PEP-8 issues Signed-off-by: Maciej Kisielewski <email address hidden>
- 4117. By Maciej Kisielewski
-
plainbox:session: add 7th suspend format
This one brings session timestamp and info about the selected timestamps to the
session storage.Signed-off-by: Maciej Kisielewski <email address hidden>
Preview Diff
1 | === modified file 'plainbox/plainbox/impl/session/resume.py' |
2 | --- plainbox/plainbox/impl/session/resume.py 2015-09-09 08:28:34 +0000 |
3 | +++ plainbox/plainbox/impl/session/resume.py 2015-12-01 14:50:52 +0000 |
4 | @@ -1,6 +1,6 @@ |
5 | # This file is part of Checkbox. |
6 | # |
7 | -# Copyright 2012, 2013, 2014 Canonical Ltd. |
8 | +# Copyright 2012-2015 Canonical Ltd. |
9 | # Written by: |
10 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
11 | # |
12 | @@ -200,6 +200,8 @@ |
13 | return SessionPeekHelper5().peek_json(json_repr) |
14 | elif version == 6: |
15 | return SessionPeekHelper6().peek_json(json_repr) |
16 | + elif version == 7: |
17 | + return SessionPeekHelper7().peek_json(json_repr) |
18 | else: |
19 | raise IncompatibleSessionError( |
20 | _("Unsupported version {}").format(version)) |
21 | @@ -328,6 +330,9 @@ |
22 | elif version == 6: |
23 | helper = SessionResumeHelper6( |
24 | self.job_list, self.flags, self.location) |
25 | + elif version == 7: |
26 | + helper = SessionResumeHelper7( |
27 | + self.job_list, self.flags, self.location) |
28 | else: |
29 | raise IncompatibleSessionError( |
30 | _("Unsupported version {}").format(version)) |
31 | @@ -550,6 +555,19 @@ |
32 | """ |
33 | |
34 | |
35 | +class SessionPeekHelper7(SessionPeekHelper6): |
36 | + """ |
37 | + Helper class for implementing session peek feature |
38 | + |
39 | + This class works with data constructed by |
40 | + :class:`~plainbox.impl.session.suspend.SessionSuspendHelper7` which has |
41 | + been pre-processed by :class:`SessionPeekHelper` (to strip the initial |
42 | + envelope). |
43 | + |
44 | + The only goal of this class is to reconstruct session state meta-data. |
45 | + """ |
46 | + |
47 | + |
48 | class SessionResumeHelper1(MetaDataHelper1MixIn): |
49 | |
50 | """ |
51 | @@ -860,13 +878,13 @@ |
52 | # - All of the jobs that we need to run (aka, the desired jobs |
53 | # list). This is pretty obvious and it is exactly what must |
54 | # be preserved or trim_job_list() will complain |
55 | - set([job.id for job in session.run_list]) |
56 | + set([job.id for job in session.run_list]) | |
57 | # - All of the jobs that have representation (aka checksum). |
58 | # We want those jobs because they have results (or they would not |
59 | # end up in the list as of format v4). If they have results we |
60 | # just have to keep them. Perhaps the session had a different |
61 | # selection earlier, who knows. |
62 | - | set(jobs_repr) |
63 | + set(jobs_repr) |
64 | ) |
65 | try: |
66 | # NOTE: this should never raise ValueError (which signals that we |
67 | @@ -904,16 +922,16 @@ |
68 | if 'io_log_filename' in result_repr: |
69 | io_log_filename = cls._load_io_log_filename( |
70 | result_repr, flags, location) |
71 | - if (flags & cls.FLAG_FILE_REFERENCE_CHECKS_F |
72 | - and not os.path.isfile(io_log_filename) |
73 | - and flags & cls.FLAG_REWRITE_LOG_PATHNAMES_F): |
74 | + if (flags & cls.FLAG_FILE_REFERENCE_CHECKS_F and |
75 | + not os.path.isfile(io_log_filename) and |
76 | + flags & cls.FLAG_REWRITE_LOG_PATHNAMES_F): |
77 | io_log_filename2 = cls._rewrite_pathname(io_log_filename, |
78 | location) |
79 | logger.warning(_("Rewrote file name from %r to %r"), |
80 | io_log_filename, io_log_filename2) |
81 | io_log_filename = io_log_filename2 |
82 | - if (flags & cls.FLAG_FILE_REFERENCE_CHECKS_F |
83 | - and not os.path.isfile(io_log_filename)): |
84 | + if (flags & cls.FLAG_FILE_REFERENCE_CHECKS_F and |
85 | + not os.path.isfile(io_log_filename)): |
86 | raise BrokenReferenceToExternalFile( |
87 | _("cannot access file: {!r}").format(io_log_filename)) |
88 | return DiskJobResult({ |
89 | @@ -1062,6 +1080,7 @@ |
90 | raise ValueError("Location must be a directory name") |
91 | return os.path.join(location, io_log_filename) |
92 | |
93 | + |
94 | class SessionResumeHelper6(SessionResumeHelper5): |
95 | """ |
96 | Helper class for implementing session resume feature |
97 | @@ -1119,6 +1138,12 @@ |
98 | return session |
99 | |
100 | |
101 | +class SessionResumeHelper7(SessionResumeHelper6): |
102 | + """ |
103 | + Helper class for implementing session resume feature |
104 | + """ |
105 | + |
106 | + |
107 | def _validate(obj, **flags): |
108 | """Multi-purpose extraction and validation function.""" |
109 | # Fetch data from the container OR use json_repr directly |
110 | |
111 | === modified file 'plainbox/plainbox/impl/session/state.py' |
112 | --- plainbox/plainbox/impl/session/state.py 2015-09-03 11:00:21 +0000 |
113 | +++ plainbox/plainbox/impl/session/state.py 2015-12-01 14:50:52 +0000 |
114 | @@ -22,6 +22,7 @@ |
115 | ============================================================ |
116 | """ |
117 | import collections |
118 | +import datetime |
119 | import logging |
120 | import re |
121 | |
122 | @@ -70,7 +71,7 @@ |
123 | FLAG_BOOTSTRAPPING = "bootstrapping" |
124 | |
125 | def __init__(self, title=None, flags=None, running_job_name=None, |
126 | - app_blob=None, app_id=None): |
127 | + app_blob=None, app_id=None, timestamp=None): |
128 | """Initialize a new session state meta-data object.""" |
129 | if flags is None: |
130 | flags = [] |
131 | @@ -79,6 +80,7 @@ |
132 | self._running_job_name = running_job_name |
133 | self._app_blob = app_blob |
134 | self._app_id = app_id |
135 | + self._timestamp = timestamp |
136 | |
137 | def __repr__(self): |
138 | """Get the representation of the session state meta-data.""" |
139 | @@ -185,6 +187,16 @@ |
140 | raise TypeError(_("app_id must be either None or str")) |
141 | self._app_id = value |
142 | |
143 | + @property |
144 | + def timestamp(self): |
145 | + """ |
146 | + POSIX time when the session was created. |
147 | + |
148 | + Note that this a simple number (POSIX timestamp) to store this value, |
149 | + as it's easier to (de)serialize, and most of the time it will be used |
150 | + for sorting. |
151 | + """ |
152 | + return self._timestamp |
153 | |
154 | class SessionDeviceContext: |
155 | |
156 | @@ -365,6 +377,7 @@ |
157 | added to the system. |
158 | """ |
159 | self._test_plan_list = test_plan_list |
160 | + self._state.selected_test_plans = [unit.id for unit in test_plan_list] |
161 | self._invalidate_override_map() |
162 | self._bulk_override_update() |
163 | if test_plan_list: |
164 | @@ -790,7 +803,9 @@ |
165 | self._mandatory_job_list = [] |
166 | self._run_list = [] |
167 | self._resource_map = {} |
168 | - self._metadata = SessionMetaData() |
169 | + self._selected_test_plans = [] |
170 | + self._metadata = SessionMetaData( |
171 | + timestamp=datetime.datetime.timestamp(datetime.datetime.utcnow())) |
172 | super(SessionState, self).__init__() |
173 | |
174 | def trim_job_list(self, qualifier): |
175 | @@ -1199,6 +1214,16 @@ |
176 | """Map from resource id to a list of resource records.""" |
177 | return self._resource_map |
178 | |
179 | + @property |
180 | + def selected_test_plans(self): |
181 | + """List of test plan ids that have been selected for execution.""" |
182 | + return self._selected_test_plans |
183 | + |
184 | + @selected_test_plans.setter |
185 | + def selected_test_plans(self, test_plans): |
186 | + """set the list of selected test plans.""" |
187 | + self._selected_test_plans = test_plans |
188 | + |
189 | def get_outcome_stats(self): |
190 | """ |
191 | Process the JobState map to get stats about the job outcomes. |
192 | |
193 | === modified file 'plainbox/plainbox/impl/session/suspend.py' |
194 | --- plainbox/plainbox/impl/session/suspend.py 2015-08-20 11:17:46 +0000 |
195 | +++ plainbox/plainbox/impl/session/suspend.py 2015-12-01 14:50:52 +0000 |
196 | @@ -83,6 +83,8 @@ |
197 | 5) Same as '4' but DiskJobResult is stored with a relative pathname to the log |
198 | file if session_dir is provided. |
199 | 6) Same as '5' plus store the list of mandatory jobs. |
200 | +7) Same as '6' plus store session's timestamp and the list of test plan ids |
201 | + that were selected for execution. |
202 | """ |
203 | |
204 | import base64 |
205 | @@ -647,5 +649,127 @@ |
206 | "metadata": self._repr_SessionMetaData(obj.metadata, session_dir), |
207 | } |
208 | |
209 | +class SessionSuspendHelper7(SessionSuspendHelper6): |
210 | + """ |
211 | + Helper class for computing binary representation of a session. |
212 | + |
213 | + The helper only creates a bytes object to save. Actual saving should |
214 | + be performed using some other means, preferably using |
215 | + :class:`~plainbox.impl.session.storage.SessionStorage`. |
216 | + |
217 | + This class creates version '7' snapshots. |
218 | + """ |
219 | + |
220 | + VERSION = 7 |
221 | + |
222 | + def _repr_SessionState(self, obj, session_dir): |
223 | + """ |
224 | + Compute the representation of :class:`SessionState`. |
225 | + |
226 | + :returns: |
227 | + JSON-friendly representation |
228 | + :rtype: |
229 | + dict |
230 | + |
231 | + The result is a dictionary with the following items: |
232 | + |
233 | + ``jobs``: |
234 | + Dictionary mapping job id to job checksum. |
235 | + The checksum is computed with |
236 | + :attr:`~plainbox.impl.job.JobDefinition.checksum`. |
237 | + Two kinds of jobs are mentioned here: |
238 | + - jobs that ever ran and have a result |
239 | + - jobs that may run (are on the run list now) |
240 | + The idea is to capture the "state" of the jobs that are |
241 | + "important" to this session, that should be checked for |
242 | + modifications when the session resumes later. |
243 | + |
244 | + ``results`` |
245 | + Dictionary mapping job id to a list of results. |
246 | + Each result is represented by data computed by |
247 | + :meth:`_repr_JobResult()`. Only jobs that actually have |
248 | + a result are mentioned here. The automatically generated |
249 | + "None" result that is always present for every job is skipped. |
250 | + |
251 | + ``desired_job_list``: |
252 | + List of (ids) of jobs that are desired (to be executed) |
253 | + |
254 | + ``mandatory_job_list``: |
255 | + List of (ids) of jobs that must be executed |
256 | + |
257 | + ``selected_test_plans``: |
258 | + List of (ids) of test plans that were selected for execution |
259 | + |
260 | + ``metadata``: |
261 | + The representation of meta-data associated with the session |
262 | + state object. |
263 | + """ |
264 | + id_run_list = frozenset([job.id for job in obj.run_list]) |
265 | + return { |
266 | + "jobs": { |
267 | + state.job.id: state.job.checksum |
268 | + for state in obj.job_state_map.values() |
269 | + if not state.result.is_hollow or state.job.id in id_run_list |
270 | + }, |
271 | + "results": { |
272 | + state.job.id: [self._repr_JobResult(result, session_dir) |
273 | + for result in state.result_history] |
274 | + for state in obj.job_state_map.values() |
275 | + if len(state.result_history) > 0 |
276 | + }, |
277 | + "desired_job_list": [ |
278 | + job.id for job in obj.desired_job_list |
279 | + ], |
280 | + "mandatory_job_list": [ |
281 | + job.id for job in obj.mandatory_job_list |
282 | + ], |
283 | + "selected_test_plans": obj.selected_test_plans, |
284 | + "metadata": self._repr_SessionMetaData(obj.metadata, session_dir), |
285 | + } |
286 | + |
287 | + def _repr_SessionMetaData(self, obj, session_dir): |
288 | + """ |
289 | + Compute the representation of :class:`SessionMetaData`. |
290 | + |
291 | + :returns: |
292 | + JSON-friendly representation. |
293 | + :rtype: |
294 | + dict |
295 | + |
296 | + The result is a dictionary with the following items: |
297 | + |
298 | + ``title``: |
299 | + Title of the session. Arbitrary text provided by the |
300 | + application. |
301 | + |
302 | + ``flags``: |
303 | + List of strings that enumerate the flags the session is in. |
304 | + There are some well-known flags but this list can have any |
305 | + items it it. |
306 | + |
307 | + ``running_job_name``: |
308 | + Id of the job that was about to be executed before |
309 | + snapshotting took place. Can be None. |
310 | + |
311 | + ``app_blob``: |
312 | + Arbitrary application specific binary blob encoded with base64. |
313 | + This field may be null. |
314 | + |
315 | + ``app_id``: |
316 | + A string identifying the application that stored app_blob. |
317 | + Thirs field may be null. |
318 | + |
319 | + ``timestamp``: |
320 | + A POSIX timestamp of the time when the session was created. |
321 | + """ |
322 | + # reuse previous version |
323 | + data = super(SessionSuspendHelper7, self)._repr_SessionMetaData( |
324 | + obj, session_dir) |
325 | + # and add the timestamp |
326 | + data['timestamp'] = obj.timestamp |
327 | + return data |
328 | + |
329 | + |
330 | + |
331 | # Alias for the most recent version |
332 | -SessionSuspendHelper = SessionSuspendHelper6 |
333 | +SessionSuspendHelper = SessionSuspendHelper7 |
334 | |
335 | === modified file 'plainbox/plainbox/impl/session/test_suspend.py' |
336 | --- plainbox/plainbox/impl/session/test_suspend.py 2015-08-28 10:02:09 +0000 |
337 | +++ plainbox/plainbox/impl/session/test_suspend.py 2015-12-01 14:50:52 +0000 |
338 | @@ -41,6 +41,7 @@ |
339 | from plainbox.impl.session.suspend import SessionSuspendHelper4 |
340 | from plainbox.impl.session.suspend import SessionSuspendHelper5 |
341 | from plainbox.impl.session.suspend import SessionSuspendHelper6 |
342 | +from plainbox.impl.session.suspend import SessionSuspendHelper7 |
343 | from plainbox.impl.testing_utils import make_job |
344 | from plainbox.vendor import mock |
345 | |
346 | @@ -989,6 +990,151 @@ |
347 | }) |
348 | |
349 | |
350 | +class SessionSuspendHelper7Tests(SessionSuspendHelper6Tests): |
351 | + """Tests for various methods of SessionSuspendHelper7.""" |
352 | + def setUp(self): |
353 | + self.helper = SessionSuspendHelper7() |
354 | + self.session_dir = None |
355 | + |
356 | + def test_json_repr_current_version(self): |
357 | + """ |
358 | + verify what the version field is |
359 | + """ |
360 | + data = self.helper._json_repr(SessionState([]), self.session_dir) |
361 | + self.assertEqual(data['version'], 7) |
362 | + |
363 | + @mock.patch('datetime.datetime') |
364 | + def test_suspend(self, mock_dt): |
365 | + mock_dt.timestamp.return_value = 5 |
366 | + data = self.helper.suspend(SessionState([]), self.session_dir) |
367 | + self.assertIsInstance(data, bytes) |
368 | + self.assertEqual(gzip.decompress(data), ( |
369 | + b'{"session":{"desired_job_list":[],"jobs":{},' |
370 | + b'"mandatory_job_list":[],"metadata":' |
371 | + b'{"app_blob":null,"app_id":null,"flags":[],' |
372 | + b'"running_job_name":null,"timestamp":5,"title":null},' |
373 | + b'"results":{},"selected_test_plans":[]},"version":7}')) |
374 | + |
375 | + @mock.patch('datetime.datetime') |
376 | + def test_repr_SessionState_empty_session(self, mock_dt): |
377 | + """ |
378 | + verify that representation of empty SessionState is okay |
379 | + """ |
380 | + mock_dt.timestamp.return_value = 20 |
381 | + data = self.helper._repr_SessionState( |
382 | + SessionState([]), self.session_dir) |
383 | + self.assertEqual(data, { |
384 | + 'jobs': {}, |
385 | + 'results': {}, |
386 | + 'desired_job_list': [], |
387 | + 'mandatory_job_list': [], |
388 | + 'selected_test_plans': [], |
389 | + 'metadata': { |
390 | + 'title': None, |
391 | + 'flags': [], |
392 | + 'running_job_name': None, |
393 | + 'app_blob': None, |
394 | + 'app_id': None, |
395 | + 'timestamp': 20 |
396 | + }, |
397 | + }) |
398 | + |
399 | + def test_repr_SessionMetaData_empty_metadata(self): |
400 | + """ |
401 | + verify that representation of empty SessionMetaData is okay |
402 | + """ |
403 | + # all defaults with empty values |
404 | + data = self.helper._repr_SessionMetaData( |
405 | + SessionMetaData(), self.session_dir) |
406 | + self.assertEqual(data, { |
407 | + 'title': None, |
408 | + 'flags': [], |
409 | + 'running_job_name': None, |
410 | + 'app_blob': None, |
411 | + 'app_id': None, |
412 | + 'timestamp': None, |
413 | + }) |
414 | + |
415 | + def test_repr_SessionMetaData_typical_metadata(self): |
416 | + """ |
417 | + verify that representation of typical SessionMetaData is okay |
418 | + """ |
419 | + # no surprises here, just the same data copied over |
420 | + data = self.helper._repr_SessionMetaData(SessionMetaData( |
421 | + title='USB Testing session', |
422 | + flags=['incomplete'], |
423 | + running_job_name='usb/detect', |
424 | + app_blob=b'blob', |
425 | + app_id='com.canonical.certification.plainbox', |
426 | + timestamp=100.0, |
427 | + ), self.session_dir) |
428 | + self.assertEqual(data, { |
429 | + 'title': 'USB Testing session', |
430 | + 'flags': ['incomplete'], |
431 | + 'running_job_name': 'usb/detect', |
432 | + 'app_blob': 'YmxvYg==', |
433 | + 'app_id': 'com.canonical.certification.plainbox', |
434 | + 'timestamp': 100.0 |
435 | + }) |
436 | + |
437 | + @mock.patch('datetime.datetime') |
438 | + def test_repr_SessionState_typical_session(self, mock_dt): |
439 | + """ |
440 | + verify the representation of a SessionState with some unused jobs |
441 | + |
442 | + Unused jobs should just have no representation. Their checksum |
443 | + should not be mentioned. Their results (empty results) should be |
444 | + ignored. |
445 | + """ |
446 | + mock_dt.timestamp.return_value = 40 |
447 | + used_job = JobDefinition({ |
448 | + "plugin": "shell", |
449 | + "id": "used", |
450 | + "command": "echo 'hello world'", |
451 | + }) |
452 | + unused_job = JobDefinition({ |
453 | + "plugin": "shell", |
454 | + "id": "unused", |
455 | + "command": "echo 'hello world'", |
456 | + }) |
457 | + used_result = MemoryJobResult({ |
458 | + "io_log": [ |
459 | + (0.0, "stdout", b'hello world\n'), |
460 | + ], |
461 | + 'outcome': IJobResult.OUTCOME_PASS |
462 | + }) |
463 | + session_state = SessionState([used_job, unused_job]) |
464 | + session_state.update_desired_job_list([used_job]) |
465 | + session_state.update_job_result(used_job, used_result) |
466 | + data = self.helper._repr_SessionState(session_state, self.session_dir) |
467 | + self.assertEqual(data, { |
468 | + 'jobs': { |
469 | + 'used': ('8c393c19fdfde1b6afc5b79d0a1617ecf7531cd832a16450dc' |
470 | + '2f3f50d329d373') |
471 | + }, |
472 | + 'results': { |
473 | + 'used': [{ |
474 | + 'comments': None, |
475 | + 'execution_duration': None, |
476 | + 'io_log': [[0.0, 'stdout', 'aGVsbG8gd29ybGQK']], |
477 | + 'outcome': 'pass', |
478 | + 'return_code': None |
479 | + }] |
480 | + }, |
481 | + 'desired_job_list': ['used'], |
482 | + 'mandatory_job_list': [], |
483 | + 'selected_test_plans': [], |
484 | + 'metadata': { |
485 | + 'title': None, |
486 | + 'flags': [], |
487 | + 'running_job_name': None, |
488 | + 'app_blob': None, |
489 | + 'app_id': None, |
490 | + 'timestamp': 40, |
491 | + }, |
492 | + }) |
493 | + |
494 | + |
495 | class RegressionTests(TestCase): |
496 | |
497 | def test_1388055(self): |
16:53 <@zyga> kissiel: -1 on posix timestamp, let's use something like datetime object
16:53 <@zyga> kissiel: I'm somewhat split on sessionstate, I'll explain why in a sec
16:54 <@zyga> kissiel: let's store more than one "timestamp": created / finished / updated -- created is fixed, finished is also fixed (when submitting) and
last updated is changed each time we do stuff
16:55 <@zyga> kissiel: a single timestamp is less useful IMHO
16:55 * zyga wonders where is the patch that made SessionState a POD
16:55 <@zyga> kissiel: the UI should look at "updated" time stamps as that's what the user understands
16:55 <@zyga> kissiel: but the rest should be useful too
16:56 <@zyga> kissiel: I'd like to also see patches that make use fo the new field where it makes sense to do so (SA APIs)
I'm also somewhat split on adding more stuff to SessionState. We have the unfortunate split between SessionManager (global state), SesesionDeviceC ontext (device state) that contains a SessionState (which is the only thing that gets serialized). I'd like to see a follow-up where SessionManager is serialized instead. Storing global test plan list and per-device SessionState (which can later on just merge with SessionDeviceCo ntext, the only reason that is separate was lack of SessionAssistant and difficulty of changing without breaking apps).