Merge lp:~kissiel/checkbox/sa-garbage-collection into lp:checkbox

Proposed by Maciej Kisielewski
Status: Merged
Approved by: Sylvain Pineau
Approved revision: 4139
Merged at revision: 4228
Proposed branch: lp:~kissiel/checkbox/sa-garbage-collection
Merge into: lp:checkbox
Diff against target: 344 lines (+190/-14)
7 files modified
checkbox-touch/checkbox-touch.qml (+23/-4)
checkbox-touch/components/CheckboxTouchApplication.qml (+10/-0)
checkbox-touch/components/ResumeSessionPage.qml (+15/-0)
checkbox-touch/py/checkbox_touch.py (+25/-0)
checkbox-touch/tests/autopilot/checkbox_touch/__init__.py (+10/-0)
checkbox-touch/tests/autopilot/checkbox_touch/test_checkbox_touch.py (+42/-10)
plainbox/plainbox/impl/session/assistant.py (+65/-0)
To merge this branch: bzr merge lp:~kissiel/checkbox/sa-garbage-collection
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Review via email: mp+281796@code.launchpad.net

Description of the change

This MR brings garbage collection to Session Assisstant and Checkbox-Converged.

6726481 plainbox:session:assistant: add get_old_sessions function
ee5b43c plainbox:session:assistant: add delete_sessions function
ef34911 plainbox:session:assistant: bump usage expectations
d95b13b checkbox-touch: add py<->qml bits for session garbage collection
555f6f6 checkbox-touch:component: add 'delete sessions' button to resume page
9c203cc checkbox-touch: delete old sessions on startup
3b3541f checkbox-touch: add gcAndStartSession function
6fdaddb checkbox-touch: run session GC whenever starting a new session
5de5b0c checkbox-touch:autopilot: move select_two_tests_and_quit to top class
ecb84b3 checkbox-touch:autopilot: add garbage collection tests

To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

A big +1, awesome feature.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'checkbox-touch/checkbox-touch.qml'
2--- checkbox-touch/checkbox-touch.qml 2015-11-25 10:19:33 +0000
3+++ checkbox-touch/checkbox-touch.qml 2016-01-06 20:45:00 +0000
4@@ -150,6 +150,7 @@
5 CheckboxTouchApplication {
6 id: app
7 py: py
8+ property var incompleteSessions: []
9 onAppReady: {
10 console.log("Plainbox version " + plainboxVersion);
11 console.log("Checkbox Touch version " + applicationVersion);
12@@ -157,6 +158,10 @@
13 "checkbox_touch" : applicationVersion,
14 "plainbox" : plainboxVersion
15 };
16+ getIncompleteSessions(function(sessions) {
17+ incompleteSessions = sessions;
18+ resumeSessionPage.incompleteSessionCount = sessions.length;
19+ });
20 resumeOrStartSession();
21 }
22 onSessionReady: {
23@@ -280,7 +285,14 @@
24 onRestartSession: {
25 pageStack.clear();
26 pageStack.push(welcomePage);
27- app.startSession();
28+ gcAndStartSession();
29+ }
30+ onDeleteIncomplete: {
31+ app.deleteOldSessions(app.incompleteSessions, function() {
32+ pageStack.clear();
33+ pageStack.push(welcomePage);
34+ app.startSession();
35+ });
36 }
37 }
38
39@@ -445,10 +457,10 @@
40 } else {
41 if (result.errors_encountered) {
42 ErrorLogic.showError(mainView, i18n.tr("Could not resume session."),
43- app.startSession(),
44+ gcAndStartSession(),
45 i18n.tr("Start new session"));
46 } else {
47- app.startSession();
48+ gcAndStartSession();
49 }
50 }
51 });
52@@ -533,7 +545,7 @@
53 var endTesting = function() {
54 pageStack.clear();
55 app.clearSession(function() {
56- app.startSession();
57+ gcAndStartSession();
58 pageStack.push(welcomePage);
59 });
60 };
61@@ -713,5 +725,12 @@
62 }
63 process_input();
64 }
65+ function gcAndStartSession() {
66+ // delete sessions that won't be resumed (NOT incomplete sessions)
67+ // and start a new session
68+ app.deleteOldSessions([], function() {
69+ app.startSession();
70+ });
71+ }
72
73 }
74
75=== modified file 'checkbox-touch/components/CheckboxTouchApplication.qml'
76--- checkbox-touch/components/CheckboxTouchApplication.qml 2015-12-03 03:41:21 +0000
77+++ checkbox-touch/components/CheckboxTouchApplication.qml 2016-01-06 20:45:00 +0000
78@@ -176,6 +176,16 @@
79 if (continuation_error) continuation_error(error);
80 });
81 }
82+ function getIncompleteSessions(continuation) {
83+ request("get_incomplete_sessions", [], continuation, function(error) {
84+ console.error("Unable to get incomplete sessions")
85+ });
86+ }
87+ function deleteOldSessions(sessionIds, continuation) {
88+ request("delete_old_sessions", [sessionIds], continuation, function(error) {
89+ console.error("Unable to remove old sessions")
90+ });
91+ }
92
93 function rememberPassword(password, continuation) {
94 // using low-level py.call() to 'silently' pass password string through pyotherside
95
96=== modified file 'checkbox-touch/components/ResumeSessionPage.qml'
97--- checkbox-touch/components/ResumeSessionPage.qml 2015-10-07 17:04:55 +0000
98+++ checkbox-touch/components/ResumeSessionPage.qml 2016-01-06 20:45:00 +0000
99@@ -31,9 +31,11 @@
100
101 Page {
102 property alias resumeText: resumeLabel.text
103+ property var incompleteSessionCount: 0
104 signal rerunLast();
105 signal continueSession();
106 signal restartSession();
107+ signal deleteIncomplete();
108
109 objectName: "resumeSessionPage"
110 title: i18n.tr("Resume session")
111@@ -66,6 +68,19 @@
112 }
113
114 LatchButton {
115+ id: deleteIncompleteButton
116+ objectName: "deleteIncompleteButton"
117+ visible: incompleteSessionCount > 0
118+ text: i18n.tr("Delete incomplete sessions (%0)".arg(incompleteSessionCount))
119+ unlatchedColor: UbuntuColors.red
120+ Layout.fillWidth: true
121+ onLatchedClicked: {
122+ deleteIncomplete();
123+ columnLayout.latchGroup();
124+ }
125+ }
126+
127+ LatchButton {
128 id: rerunButton
129 objectName: "rerunButton"
130 unlatchedColor: UbuntuColors.warmGrey
131
132=== modified file 'checkbox-touch/py/checkbox_touch.py'
133--- checkbox-touch/py/checkbox_touch.py 2015-12-10 12:51:05 +0000
134+++ checkbox-touch/py/checkbox_touch.py 2016-01-06 20:45:00 +0000
135@@ -487,6 +487,31 @@
136 if conn:
137 conn.close()
138
139+ @view
140+ def get_incomplete_sessions(self):
141+ """Get ids of sessions with an 'incomplete' flag."""
142+ self._incomplete_sessions = [
143+ s[0] for s in self.assistant.get_old_sessions(
144+ flags={'incomplete'}, allow_not_flagged=False)]
145+ return self._incomplete_sessions
146+
147+ @view
148+ def delete_old_sessions(self, additional_sessions):
149+ """
150+ Delete session storages.
151+
152+ :param additional_sessions:
153+ List of ids of sessions that should be removed.
154+
155+ This function removes all complete sessions (i.e. the ones that session
156+ assistant returns when get_old_sessions is run with the default params)
157+ with the addition of the ones specified in the ``additional_sessions``
158+ param.
159+ """
160+ garbage = [s[0] for s in self.assistant.get_old_sessions()]
161+ garbage += additional_sessions
162+ self.assistant.delete_sessions(garbage)
163+
164 def _get_user_directory_documents(self):
165 xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \
166 os.path.expanduser('~/.config')
167
168=== modified file 'checkbox-touch/tests/autopilot/checkbox_touch/__init__.py'
169--- checkbox-touch/tests/autopilot/checkbox_touch/__init__.py 2015-09-18 14:28:20 +0000
170+++ checkbox-touch/tests/autopilot/checkbox_touch/__init__.py 2016-01-06 20:45:00 +0000
171@@ -89,6 +89,16 @@
172 objectName='continueButton')
173 self.pointing_device.click_object(continue_btn)
174
175+ def select_two_tests_and_quit(self):
176+ self.start_and_select_tests(
177+ '2015.com.canonical.certification::normal', [
178+ '2015.com.canonical.certification::autopilot/user-verify-1',
179+ '2015.com.canonical.certification::autopilot/user-verify-2'])
180+ # make sure that test is shown (therefore session has been started)
181+ self.app.wait_select_single(
182+ objectName='userInteractVerifyIntroPage', visible=True)
183+ self.app.process.terminate()
184+
185 def process_sequence_of_clicks_on_pages(self, steps):
186 """
187 Do a sequence of clicks on simple page->component hierarchies.
188
189=== modified file 'checkbox-touch/tests/autopilot/checkbox_touch/test_checkbox_touch.py'
190--- checkbox-touch/tests/autopilot/checkbox_touch/test_checkbox_touch.py 2015-12-10 10:08:49 +0000
191+++ checkbox-touch/tests/autopilot/checkbox_touch/test_checkbox_touch.py 2016-01-06 20:45:00 +0000
192@@ -129,16 +129,6 @@
193
194
195 class SessionResumeTests(checkbox_touch.ClickAppTestCase):
196- def select_two_tests_and_quit(self):
197- self.start_and_select_tests(
198- '2015.com.canonical.certification::normal', [
199- '2015.com.canonical.certification::autopilot/user-verify-1',
200- '2015.com.canonical.certification::autopilot/user-verify-2'])
201- # make sure that test is shown (therefore session has been started)
202- self.app.wait_select_single(
203- objectName='userInteractVerifyIntroPage', visible=True)
204- self.app.process.terminate()
205-
206 def test_rerun_after_resume(self):
207 self.select_two_tests_and_quit()
208 self.launch_application()
209@@ -298,3 +288,45 @@
210 ]
211 self.process_sequence_of_clicks_on_pages(next_steps)
212 self.check_results({'passed': '2', 'failed': '0', 'skipped': '0'})
213+
214+class GarbageCollectionTests(checkbox_touch.ClickAppTestCase):
215+ def test_garbage_collection_popup_shown(self):
216+ """Ensure popup is shown when there are candidates for removal."""
217+ # start a session, then quit the app, to create dangling incomplete
218+ # session
219+ self.select_two_tests_and_quit()
220+ self.launch_application()
221+ # check if the button from session garbage collect popup is visible
222+ self.app.wait_select_single(
223+ objectName="deleteIncompleteButton", visible=True)
224+
225+ def test_restart_doesnt_remove(self):
226+ self.select_two_tests_and_quit()
227+ self.launch_application()
228+ resume_page = self.app.wait_select_single(
229+ objectName='resumeSessionPage', visible=True)
230+ restart_btn = resume_page.wait_select_single(
231+ objectName='restartButton')
232+ self.pointing_device.click_object(restart_btn)
233+ # quit the app
234+ self.app.process.terminate()
235+ self.launch_application()
236+ # make sure the app still wants to garbage collect some sessions
237+ # check if the button from session garbage collect popup is visible
238+ self.app.wait_select_single(
239+ objectName="deleteIncompleteButton", visible=True)
240+
241+ def test_remove_all_removes(self):
242+ self.select_two_tests_and_quit()
243+ self.launch_application()
244+ delete_btn = self.app.wait_select_single(
245+ objectName="deleteIncompleteButton", visible=True)
246+ self.pointing_device.click_object(delete_btn)
247+ # make sure the app behaves normally afterwards
248+ self.skipResumeIfShown()
249+ # quit the app
250+ self.app.process.terminate()
251+ self.launch_application()
252+ # make sure that app doens't show garbage collection dialog
253+ self.app.wait_select_single(
254+ objectName='startTestButton', enabled=True)
255
256=== modified file 'plainbox/plainbox/impl/session/assistant.py'
257--- plainbox/plainbox/impl/session/assistant.py 2015-12-11 09:08:09 +0000
258+++ plainbox/plainbox/impl/session/assistant.py 2016-01-06 20:45:00 +0000
259@@ -176,6 +176,10 @@
260 "create a transport for the C3 system"),
261 self.get_canonical_hexr_transport: (
262 "create a transport for the HEXR system"),
263+ self.get_old_sessions: (
264+ "get previously created sessions"),
265+ self.delete_sessions: (
266+ "delete previously created sessions"),
267 }
268 # Restart support
269 self._restart_cmd_callback = None
270@@ -463,6 +467,63 @@
271 _logger.debug("Provider selected: %r", provider)
272
273 @raises(UnexpectedMethodCall)
274+ def get_old_sessions(self, flags: 'Set[str]'={
275+ SessionMetaData.FLAG_SUBMITTED, SessionMetaData.FLAG_BOOTSTRAPPING},
276+ allow_not_flagged: bool=True) -> 'List[Tuple[str, Set[str]]]':
277+ """
278+ Get the list of previously run sessions.
279+
280+ :param flags:
281+ Set of flags from which at least one flag must be present in the
282+ metadata of the processed session storage in order for that storage
283+ to be returned.
284+ :param allow_not_flagged:
285+ Also return sessions that have no flags attached.
286+ :returns:
287+ A list of tuples containing session id and flags that were attached
288+ to that session.
289+ :raises UnexpectedMethodCall:
290+ If the call is made at an unexpected time. Do not catch this error.
291+ It is a bug in your program. The error message will indicate what
292+ is the likely cause.
293+ """
294+ UsageExpectation.of(self).enforce()
295+ for storage in self._repo.get_storage_list():
296+ data = storage.load_checkpoint()
297+ if len(data) == 0:
298+ continue
299+ try:
300+ metadata = SessionPeekHelper().peek(data)
301+ if (metadata.app_id == self._app_id):
302+ if ((allow_not_flagged and not metadata.flags) or
303+ (metadata.flags & flags)):
304+ yield storage.id, metadata.flags
305+ except SessionResumeError as exc:
306+ _logger.info("Exception raised when trying to peek session"
307+ "data: %s", str(exc))
308+
309+ @raises(UnexpectedMethodCall)
310+ def delete_sessions(self, session_ids: 'List[str]') -> None:
311+ """
312+ Delete session storages.
313+
314+ :param session_ids:
315+ A list of session ids which storages should be removed.
316+ :raises UnexpectedMethodCall:
317+ If the call is made at an unexpected time. Do not catch this error.
318+ It is a bug in your program. The error message will indicate what
319+ is the likely cause.
320+
321+ .. note::
322+ If the session is not found in the currently selected session
323+ repository, it is silently ignored.
324+ """
325+ UsageExpectation.of(self).enforce()
326+ for storage in self._repo.get_storage_list():
327+ if storage.id in session_ids:
328+ storage.remove()
329+
330+ @raises(UnexpectedMethodCall)
331 def start_new_session(self, title: str):
332 """
333 Create a new testing session.
334@@ -1272,6 +1333,10 @@
335 "create a transport for the C3 system"),
336 self.get_canonical_hexr_transport: (
337 "create a transport for the HEXR system"),
338+ self.get_old_sessions: (
339+ "get previously created sessions"),
340+ self.delete_sessions: (
341+ "delete previously created sessions"),
342 }
343
344 @raises(KeyError, TransportError, UnexpectedMethodCall)

Subscribers

People subscribed via source and target branches