Merge lp:~kissiel/checkbox/sa-in-cbt into lp:checkbox

Proposed by Maciej Kisielewski
Status: Merged
Approved by: Zygmunt Krynicki
Approved revision: 3997
Merged at revision: 3970
Proposed branch: lp:~kissiel/checkbox/sa-in-cbt
Merge into: lp:checkbox
Diff against target: 958 lines (+248/-396)
5 files modified
checkbox-touch/checkbox-touch.qml (+39/-32)
checkbox-touch/components/CheckboxTouchApplication.qml (+4/-4)
checkbox-touch/py/checkbox_touch.py (+135/-327)
checkbox-touch/tests/autopilot/checkbox_touch/test_checkbox_touch.py (+33/-30)
plainbox/plainbox/impl/session/assistant.py (+37/-3)
To merge this branch: bzr merge lp:~kissiel/checkbox/sa-in-cbt
Reviewer Review Type Date Requested Status
Zygmunt Krynicki (community) Approve
Review via email: mp+269634@code.launchpad.net

Description of the change

This MR brings a small revolution to Checkbox {Touch,Converged}.

It replaces all the internal logic related to running plainbox core with calls to SessionAssistant.
It also smuggles some fixes to SessionAssistant along the way.
Then, it makes Autopilot happy with the New Order,
to finally clean up pep8 issues, embetter existing and add missing docstrings.

a6b1280 checkbox-touch: run run_test_activity for ALL plugins
e30f8f1 checkbox-touch: [move2SA] use SA in __init__
7de6bda checkbox-touch: [move2SA] use SA in start_session
861c36a checkbox-touch: [move2SA] use SA in is_session_resumable
bedf971 checkbox-touch: [move2SA] use SA in get_testplans
7e1e3a2 checkbox-touch: [move2SA] use SA in remember_testplan
037f79a checkbox-touch: [move2SA] use SA in get_categories
a853d86 checkbox-touch: [move2SA] use SA in remember_categories
9e08a76 checkbox-touch: [move2SA] use SA in get_available tests
2fe8eeb checkbox-touch: [move2SA] use SA in get_rerun_candidates
2fcc534 checkbox-touch: [move2SA] use SA in remember_tests
3a6c5cb checkbox-touch: [move2SA] use SA in get_next_test
d8fc496 checkbox-touch: [move2SA] use SA in register_test_result
2fd2af9 checkbox-touch: [move2SA] use SA in run_test_activity
952d973 checkbox-touch: [move2SA] use SA in get_results
225b491 checkbox-touch: [move2SA] use SA in export_results
f66a3b6 checkbox-touch: add timestamp to app_blob
dfce594 checkbox-touch: [move2SA] remove _checkpoint method
d3f7f14 checkbox-touch: add a method for loading embedded providers
85e9416 checkbox-touch:autopilot: rearrange order of full-scenario to match SA sorting
e507f79 plainbox:session:assistant: correct UsageExpectations after session is started
a0e3297 plainbox:session:assistant: allow export_to_file call in 'normal' state
37153b4 checkbox-touch: move embedded providers stuff to CBTApp.__init__
41c813e plainbox:session:assistant: require select_test_plan after resume
73adaa6 plainbox:session:assistant: add finalize_session method
db9146c checkbox-touch:[move2SA] use SA in clear_session
d24664e checkbox-touch: remove unnecessary methods
e45942c checkbox-touch: remove import of no longer used modules
dc61ce2 checkbox-touch: fix PEP8 issues
70148a8 checkbox-touch: upgrade docstrings

To post a comment you must log in.
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

let there be light

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-08-18 12:59:01 +0000
3+++ checkbox-touch/checkbox-touch.qml 2015-08-31 12:00:21 +0000
4@@ -155,7 +155,7 @@
5 "checkbox_touch" : applicationVersion,
6 "plainbox" : plainboxVersion
7 };
8- resumeOrStartSession(appSettings["providersDir"]);
9+ resumeOrStartSession();
10 }
11 onSessionReady: {
12 welcomePage.enableButton()
13@@ -163,7 +163,8 @@
14 Component.onCompleted: {
15 // register to py.initiated signal
16 py.onInitiated.connect(function() {
17- construct("checkbox_touch.create_app_object", []);
18+ construct("checkbox_touch.create_app_object", [
19+ appSettings["providersDir"]]);
20 });
21 }
22 }
23@@ -267,14 +268,14 @@
24
25 ResumeSessionPage {
26 id: resumeSessionPage
27- onRerunLast: app.resumeSession(true, appSettings["providersDir"], processNextTest)
28- onContinueSession: app.resumeSession(false, appSettings["providersDir"], processNextTest)
29+ onRerunLast: app.resumeSession(true, processNextTest)
30+ onContinueSession: app.resumeSession(false, processNextTest)
31 resumeText: i18n.tr("Checkbox did not finish completely.\nDo you want \
32 to rerun last test, continue to the next test, or restart from the beginning?")
33 onRestartSession: {
34 pageStack.clear();
35 pageStack.push(welcomePage);
36- app.startSession(appSettings["providersDir"]);
37+ app.startSession();
38 }
39 }
40
41@@ -407,10 +408,10 @@
42 } else {
43 if (result.errors_encountered) {
44 ErrorLogic.showError(mainView, i18n.tr("Could not resume session."),
45- app.startSession(appSettings["providersDir"]),
46+ app.startSession(),
47 i18n.tr("Start new session"));
48 } else {
49- app.startSession(appSettings["providersDir"]);
50+ app.startSession();
51 }
52 }
53 });
54@@ -502,7 +503,7 @@
55 resultsPage.endTesting.connect(function() {
56 pageStack.clear();
57 app.clearSession(function() {
58- app.startSession(appSettings["providersDir"]);
59+ app.startSession();
60 pageStack.push(welcomePage);
61 });
62 });
63@@ -535,13 +536,15 @@
64 }
65
66 function performManualTest(test) {
67- var manualIntroPage = Qt.createComponent(Qt.resolvedUrl("components/ManualIntroPage.qml")).createObject();
68- manualIntroPage.test = test
69- manualIntroPage.testDone.connect(completeTest);
70- manualIntroPage.continueClicked.connect(function() { showVerificationScreen(test); });
71- manualIntroPage.__customHeaderContents = progressHeader;
72- progressHeader.update(test);
73- pageStack.push(manualIntroPage);
74+ runTestActivity(test, function(test) {
75+ var manualIntroPage = Qt.createComponent(Qt.resolvedUrl("components/ManualIntroPage.qml")).createObject();
76+ manualIntroPage.test = test
77+ manualIntroPage.testDone.connect(completeTest);
78+ manualIntroPage.continueClicked.connect(function() { showVerificationScreen(test); });
79+ manualIntroPage.__customHeaderContents = progressHeader;
80+ progressHeader.update(test);
81+ pageStack.push(manualIntroPage);
82+ });
83 }
84
85 function performUserInteractVerifyTest(test) {
86@@ -592,25 +595,29 @@
87 }
88
89 function performQmlTest(test) {
90- var comp = Qt.createComponent(Qt.resolvedUrl("components/QmlNativePage.qml"))
91- console.log(comp.errorString());
92- var qmlNativePage = comp.createObject();
93- qmlNativePage.test = test
94- qmlNativePage.testDone.connect(completeTest);
95- qmlNativePage.__customHeaderContents = progressHeader;
96- progressHeader.update(test);
97- pageStack.push(qmlNativePage);
98+ runTestActivity(test, function(test) {
99+ var comp = Qt.createComponent(Qt.resolvedUrl("components/QmlNativePage.qml"))
100+ console.log(comp.errorString());
101+ var qmlNativePage = comp.createObject();
102+ qmlNativePage.test = test
103+ qmlNativePage.testDone.connect(completeTest);
104+ qmlNativePage.__customHeaderContents = progressHeader;
105+ progressHeader.update(test);
106+ pageStack.push(qmlNativePage);
107+ });
108 }
109 function performConfinedQmlTest(test) {
110- var comp = Qt.createComponent(Qt.resolvedUrl("components/QmlConfinedPage.qml"))
111- console.log(comp.errorString());
112- var qmlNativePage = comp.createObject();
113- qmlNativePage.test = test
114- qmlNativePage.applicationVersion = app.applicationVersion;
115- qmlNativePage.testDone.connect(completeTest);
116- qmlNativePage.__customHeaderContents = progressHeader;
117- progressHeader.update(test);
118- pageStack.push(qmlNativePage);
119+ runTestActivity(test, function(test) {
120+ var comp = Qt.createComponent(Qt.resolvedUrl("components/QmlConfinedPage.qml"))
121+ console.log(comp.errorString());
122+ var qmlNativePage = comp.createObject();
123+ qmlNativePage.test = test
124+ qmlNativePage.applicationVersion = app.applicationVersion;
125+ qmlNativePage.testDone.connect(completeTest);
126+ qmlNativePage.__customHeaderContents = progressHeader;
127+ progressHeader.update(test);
128+ pageStack.push(qmlNativePage);
129+ });
130 }
131
132 function showVerificationScreen(test) {
133
134=== modified file 'checkbox-touch/components/CheckboxTouchApplication.qml'
135--- checkbox-touch/components/CheckboxTouchApplication.qml 2015-07-29 20:50:05 +0000
136+++ checkbox-touch/components/CheckboxTouchApplication.qml 2015-08-31 12:00:21 +0000
137@@ -45,8 +45,8 @@
138 // Starts session in plainbox and runs all necessary setup actions.
139 // Calling this function will signal sessionReady() once it's finished
140 // doing setup.
141- function startSession(providersDir) {
142- request("start_session", [providersDir], function(result) {
143+ function startSession() {
144+ request("start_session", [], function(result) {
145 sessionDir = result['session_dir'];
146 sessionReady();
147 }, function(error) {
148@@ -57,8 +57,8 @@
149 i18n.tr("Quit"));
150 });
151 }
152- function resumeSession(rerunLastTest, providersDir, continuation) {
153- request("resume_session", [rerunLastTest, providersDir], function(result) {
154+ function resumeSession(rerunLastTest, continuation) {
155+ request("resume_session", [rerunLastTest], function(result) {
156 if (!result["session_id"]) {
157 pageStack.pop();
158 ErrorLogic.showError(mainView,
159
160=== modified file 'checkbox-touch/py/checkbox_touch.py'
161--- checkbox-touch/py/checkbox_touch.py 2015-07-31 13:07:18 +0000
162+++ checkbox-touch/py/checkbox_touch.py 2015-08-31 12:00:21 +0000
163@@ -32,7 +32,6 @@
164 import abc
165 import collections
166 import datetime
167-import itertools
168 import json
169 import logging
170 import os
171@@ -43,23 +42,10 @@
172
173 from plainbox.abc import IJobResult
174 from plainbox.impl import pod
175-from plainbox.impl.applogic import PlainBoxConfig
176 from plainbox.impl.clitools import ToolBase
177 from plainbox.impl.commands.inv_run import SilentUI
178 from plainbox.impl.result import JobResultBuilder
179-from plainbox.impl.runner import JobRunner
180-from plainbox.impl.secure.origin import Origin
181-from plainbox.impl.secure.qualifiers import FieldQualifier
182-from plainbox.impl.secure.qualifiers import OperatorMatcher
183-from plainbox.impl.secure.qualifiers import select_jobs
184-from plainbox.impl.session import SessionManager
185-from plainbox.impl.session import SessionMetaData
186-from plainbox.impl.session import SessionPeekHelper
187-from plainbox.impl.session import SessionResumeError
188-from plainbox.impl.session.storage import SessionStorageRepository
189-from plainbox.impl.unit.job import JobDefinition
190-from plainbox.impl.unit.validators import compute_value_map
191-from plainbox.public import get_providers
192+from plainbox.impl.session.assistant import SessionAssistant
193 import plainbox
194
195 from embedded_providers import EmbeddedProvider1PlugInCollection
196@@ -149,25 +135,22 @@
197
198 __version__ = (1, 2, 1, 'final', 0)
199
200- def __init__(self):
201+ def __init__(self, providers_dir):
202 if plainbox.__version__ < (0, 22):
203 raise SystemExit("plainbox 0.22 required, you have {}".format(
204 ToolBase.format_version_tuple(plainbox.__version__)))
205- # adjust_logging(logging.INFO, ['checkbox.touch'], True)
206- self.manager = None
207- self.context = None
208- self.runner = None
209- self.index = 0 # NOTE: next test index
210- # NOTE: This may also have "" representing None
211- self.desired_category_ids = frozenset()
212- self.desired_test_ids = frozenset()
213- self.test_plan_id = ""
214- self.resume_candidate_storage = None
215- self.session_storage_repo = None
216- self.timestamp = datetime.datetime.utcnow().isoformat()
217- self.config = PlainBoxConfig()
218+ self.assistant = SessionAssistant('checkbox-converged')
219+ self.ui = CheckboxTouchUI()
220+ self.index = 0
221 self._password = None
222- self.ui = CheckboxTouchUI()
223+ self._timestamp = None
224+ self._latest_session = None
225+ self.resume_candidate_storage = None
226+ self.assistant.use_alternate_repository(
227+ self._get_app_cache_directory())
228+ self.assistant.select_providers(
229+ '*',
230+ additional_providers=self._get_embedded_providers(providers_dir))
231
232 def __repr__(self):
233 return "app"
234@@ -182,186 +165,101 @@
235 }
236
237 @view
238- def start_session(self, providers_dir):
239- if self.manager is not None:
240- _logger.warning("start_session() should not be called twice!")
241- else:
242- self._init_session_storage_repo()
243- self.manager = SessionManager.create(self.session_storage_repo)
244- self.manager.add_local_device_context()
245- self.context = self.manager.default_device_context
246- # Add some all providers into the context
247- for provider in self._get_default_providers(providers_dir):
248- self.context.add_provider(provider)
249- # Fill in the meta-data
250- self.context.state.metadata.app_id = 'checkbox-touch'
251- self.context.state.metadata.title = 'Checkbox Touch Session'
252- self.context.state.metadata.flags.add('bootstrapping')
253- # Checkpoint the session so that we have something to see
254- self._checkpoint()
255-
256- # Prepare custom execution controller list
257- from plainbox.impl.ctrl import UserJobExecutionController
258- from sudo_with_pass_ctrl import \
259- RootViaSudoWithPassExecutionController
260- controllers = [
261- RootViaSudoWithPassExecutionController(
262- self.context.provider_list, self._password_provider),
263- UserJobExecutionController(self.context.provider_list),
264- ]
265- self.runner = JobRunner(
266- self.manager.storage.location,
267- self.context.provider_list,
268- # TODO: tie this with well-known-dirs helper
269- os.path.join(self.manager.storage.location, 'io-logs'),
270- execution_ctrl_list=controllers)
271- app_cache_dir = self._get_app_cache_directory()
272- if not os.path.exists(app_cache_dir):
273- os.makedirs(app_cache_dir)
274- with open(os.path.join(app_cache_dir, 'session_id'),
275- 'w') as f:
276- f.write(self.manager.storage.id)
277+ def start_session(self):
278+ """Start a new session."""
279+ self.assistant.start_new_session('Checkbox Converged session')
280+ self._timestamp = datetime.datetime.utcnow().isoformat()
281 return {
282- 'session_id': self.manager.storage.id,
283- 'session_dir': self.manager.storage.location
284+ 'session_id': self.assistant.get_session_id(),
285+ 'session_dir': self.assistant.get_session_dir()
286 }
287
288 @view
289- def resume_session(self, rerun_last_test, providers_dir):
290- all_units = list(itertools.chain(
291- *[p.unit_list for p in self._get_default_providers(
292- providers_dir)]))
293- try:
294- self.manager = SessionManager.load_session(
295- all_units, self.resume_candidate_storage)
296- except IOError as exc:
297- _logger.info("Exception raised when trying to resume"
298- "session: %s", str(exc))
299- return {
300- 'session_id': None
301- }
302- self.context = self.manager.default_device_context
303- metadata = self.context.state.metadata
304+ def resume_session(self, rerun_last_test):
305+ """
306+ Resume latest sesssion.
307+
308+ :param rerun_last_test:
309+ A bool stating whether runtime should repeat the test, that the app
310+ was executing when it was interrupted.
311+ """
312+ metadata = self.assistant.resume_session(self._latest_session)
313 app_blob = json.loads(metadata.app_blob.decode("UTF-8"))
314- self.runner = JobRunner(
315- self.manager.storage.location,
316- self.context.provider_list,
317- os.path.join(self.manager.storage.location, 'io-logs'))
318 self.index = app_blob['index_in_run_list']
319- self._init_test_plan_id(app_blob['test_plan_id'])
320- _logger.error(self.context.state.run_list)
321- _logger.error(self.index)
322+ self.test_plan_id = app_blob['test_plan_id']
323+ self.assistant.select_test_plan(self.test_plan_id)
324+ self.assistant.bootstrap()
325+
326 if not rerun_last_test:
327 # Skip current test
328 test = self.get_next_test()['result']
329 test['outcome'] = 'skip'
330 self.register_test_result(test)
331 return {
332- 'session_id': self.manager.storage.id
333+ 'session_id': self._latest_session
334 }
335
336 @view
337 def clear_session(self):
338- self.manager = None
339- self.context = None
340- self.runner = None
341+ """Reset app-custom state info about the session."""
342 self.index = 0
343- self.timestamp = datetime.datetime.utcnow().isoformat()
344- self.desired_category_ids = frozenset()
345- self.desired_test_ids = frozenset()
346- self.resume_candidate_storage = None
347- self.session_storage_repo = None
348- os.unlink(os.path.join(self._get_app_cache_directory(), 'session_id'))
349+ self._timestamp = datetime.datetime.utcnow().isoformat()
350
351 @view
352 def is_session_resumable(self):
353- """
354- Checks whether given session is resumable
355- """
356- resumable = False
357- try:
358- with open(os.path.join(self._get_app_cache_directory(),
359- 'session_id')) as f:
360- session_id = f.readline().rstrip('\n')
361- except (OSError, IOError):
362- session_id = None
363- self._init_session_storage_repo()
364- for storage in self.session_storage_repo.get_storage_list():
365- data = storage.load_checkpoint()
366- if len(data) == 0:
367- continue
368- try:
369- metadata = SessionPeekHelper().peek(data)
370- if (metadata.app_id == 'checkbox-touch'
371- and storage.id == session_id
372- and SessionMetaData.FLAG_INCOMPLETE in
373- metadata.flags):
374- self.resume_candidate_storage = storage
375- resumable = True
376- except SessionResumeError as exc:
377- _logger.info("Exception raised when trying to resume"
378- "session: %s", str(exc))
379- return {
380- 'resumable': False,
381- 'errors_encountered': True
382- }
383- return {
384- 'resumable': resumable,
385- 'errors_encountered': False
386- }
387+ """Check whether there is a session that can be resumed."""
388+ for session_id, session_md in self.assistant.get_resumable_sessions():
389+ # we're interested in the latest session only, this is why we
390+ # return early
391+ self._latest_session = session_id
392+ return {
393+ 'resumable': True,
394+ 'error_encountered': False,
395+ }
396+ else:
397+ return {
398+ 'resumable': False,
399+ 'error_encountered': False,
400+ }
401
402 @view
403 def get_testplans(self):
404- all_units = self.manager.default_device_context.unit_list
405+ """Get the list of available test plans."""
406+ test_plan_units = [self.assistant.get_test_plan(tp_id) for tp_id in
407+ self.assistant.get_test_plans()]
408 return {
409 'testplan_info_list': [{
410- "mod_id": unit.id,
411- "mod_name": unit.name,
412+ "mod_id": tp.id,
413+ "mod_name": tp.name,
414 "mod_selected": False,
415- } for unit in all_units if unit.Meta.name == 'test plan']
416+ } for tp in test_plan_units]
417 }
418
419 @view
420 def remember_testplan(self, test_plan_id):
421- self.context.invalidate_shared('potential_job_list')
422- self.context.invalidate_shared('potential_category_map')
423- self._init_test_plan_id(test_plan_id)
424+ """Pick the test plan as the one in force."""
425+ self.test_plan_id = test_plan_id
426+ self.assistant.select_test_plan(test_plan_id)
427+ self.assistant.bootstrap()
428
429 @view
430 def get_categories(self):
431- """
432- Get categories selection data.
433- """
434- potential_job_list = self.context.compute_shared(
435- 'potential_job_list', select_jobs,
436- self.context.state.job_list, [self.test_plan.get_qualifier()])
437- potential_category_map = self.context.compute_shared(
438- 'potential_category_map',
439- self.test_plan.get_effective_category_map, potential_job_list)
440- id_map = self.context.compute_shared(
441- 'id_map', compute_value_map, self.context, 'id')
442+ """Get categories selection data."""
443 category_info_list = [{
444 "mod_id": category.id,
445 "mod_name": category.name,
446 "mod_selected": True,
447 } for category in (
448- id_map[category_id][0]
449- for category_id in set(potential_category_map.values())
450+ self.assistant.get_category(category_id)
451+ for category_id in self.assistant.get_participating_categories()
452 )]
453- category_info_list.sort(key=lambda ci: ci['mod_name'])
454- return {
455- 'category_info_list': category_info_list
456- }
457+ return {'category_info_list': category_info_list}
458
459 @view
460 def remember_categories(self, selected_id_list):
461- """
462- Save category selection
463- """
464- self.desired_category_ids = frozenset(selected_id_list)
465- self.context.invalidate_shared('subset_job_list')
466- self.context.invalidate_shared('effective_category_map')
467- _logger.info("Selected categories: %s", self.desired_category_ids)
468+ """Save category selection."""
469+ _logger.info("Selected categories: %s", selected_id_list)
470+ self.assistant.filter_jobs_by_categories(selected_id_list)
471
472 @view
473 def get_available_tests(self):
474@@ -371,89 +269,65 @@
475 The response object will contain only tests with category matching
476 previously set list. Tests are sorted by (category, name)
477 """
478- subset_job_list = self.context.compute_shared(
479- 'subset_job_list', select_jobs,
480- self.context.state.job_list, [
481- # Select everything the test plan selected
482- self.test_plan.get_qualifier(),
483- # Except jobs not matching the selected group of categories
484- FieldQualifier(
485- JobDefinition.Meta.fields.category_id,
486- OperatorMatcher(not_contains, self.desired_category_ids),
487- Origin.get_caller_origin(), inclusive=False),
488- ])
489- effective_category_map = self.context.compute_shared(
490- 'effective_category_map',
491- self.test_plan.get_effective_category_map, subset_job_list)
492- for job_id, effective_category_id in effective_category_map.items():
493- job_state = self.context.state.job_state_map[job_id]
494- job_state.effective_category_id = effective_category_id
495- id_map = self.context.compute_shared(
496- 'id_map', compute_value_map, self.context, 'id')
497+ category_names = {
498+ cat_id: self.assistant.get_category(cat_id).tr_name() for
499+ cat_id in self.assistant.get_participating_categories()}
500+ job_units = [self.assistant.get_job(job_id) for job_id in
501+ self.assistant.get_static_todo_list()]
502 test_info_list = [{
503- "mod_id": job_id,
504- "mod_name": id_map[job_id][0].tr_summary(),
505- "mod_group": id_map[category_id][0].tr_name(),
506+ "mod_id": job.id,
507+ "mod_name": job.tr_summary(),
508+ "mod_group": category_names[job.category_id],
509 "mod_selected": True,
510- } for job_id, category_id in effective_category_map.items()]
511+ } for job in job_units]
512 test_info_list.sort(key=lambda ti: (ti['mod_group'], ti['mod_name']))
513- return {
514- 'test_info_list': test_info_list
515- }
516+ return {'test_info_list': test_info_list}
517+
518 @view
519 def get_rerun_candidates(self):
520+ """Get all the tests that might be selected for rerunning."""
521 def rerun_predicate(job_state):
522 return job_state.result.outcome in (
523 IJobResult.OUTCOME_FAIL, IJobResult.OUTCOME_CRASH)
524- id_map = self.context.compute_shared(
525- 'id_map', compute_value_map, self.context, 'id')
526 rerun_candidates = []
527- for job in self.manager.state.run_list:
528- if rerun_predicate(self.manager.state.job_state_map[job.id]):
529+ todo_list = self.assistant.get_static_todo_list()
530+ job_units = {job_id: self.assistant.get_job(job_id) for job_id
531+ in todo_list}
532+ job_states = {job_id: self.assistant.get_job_state(job_id) for job_id
533+ in todo_list}
534+ category_names = {
535+ cat_id: self.assistant.get_category(cat_id).tr_name() for cat_id
536+ in self.assistant.get_participating_categories()}
537+ for job_id, job_state in job_states.items():
538+ if rerun_predicate(job_state):
539 rerun_candidates.append({
540- "mod_id": job.id,
541- "mod_name": job.tr_summary(),
542- "mod_group": id_map[job.category_id][0].tr_name(),
543+ "mod_id": job_id,
544+ "mod_name": job_units[job_id].tr_summary(),
545+ "mod_group": category_names[job_units[job_id].category_id],
546 "mod_selected": False
547-
548 })
549 return rerun_candidates
550
551 @view
552 def remember_tests(self, selected_id_list):
553- """
554- Save test selection
555- """
556- self.desired_test_ids = frozenset(selected_id_list)
557- _logger.info("Selected tests: %s", self.desired_test_ids)
558+ """Save test selection."""
559 self.index = 0
560- self.context.invalidate_shared('desired_job_list')
561- desired_job_list = self.context.compute_shared(
562- 'desired_job_list', select_jobs,
563- self.context.state.job_list, [
564- # Select everything the test plan selected
565- self.test_plan.get_qualifier(),
566- # Except all the jobs that weren't marked by the user
567- FieldQualifier(
568- JobDefinition.Meta.fields.id,
569- OperatorMatcher(not_contains, self.desired_test_ids),
570- Origin.get_caller_origin(), inclusive=False)])
571- _logger.info("Desired job list: %s", desired_job_list)
572- self.context.state.update_desired_job_list(desired_job_list)
573- _logger.info("Run job list: %s", self.context.state.run_list)
574- self.context.state.metadata.flags.add('incomplete')
575- self._checkpoint()
576+ self.assistant.use_alternate_selection(selected_id_list)
577+ self.assistant.update_app_blob(self._get_app_blob())
578+ _logger.info("Selected tests: %s", selected_id_list)
579+ return
580
581 @view
582 def get_next_test(self):
583 """
584- Get next text that is scheduled to run
585- Returns test object or None if all tests are completed
586+ Get next text that is scheduled to run.
587+
588+ :returns:
589+ Dictionary resembling JobDefinition or None if all tests are completed
590 """
591- if self.index < len(self.context.state.run_list):
592- job = self.context.state.run_list[self.index]
593- job_state = self.context.state.job_state_map[job.id]
594- # support for description field splitted into 3 subfields
595+ todo_list = self.assistant.get_static_todo_list()
596+ if self.index < len(todo_list):
597+ job = self.assistant.get_job(todo_list[self.index])
598 description = ""
599 if job.tr_purpose() is not None:
600 description = job.tr_purpose() + "\n"
601@@ -472,97 +346,65 @@
602 "user": job.user,
603 "qml_file": job.qml_file,
604 "start_time": time.time(),
605- "test_number": self.index,
606- "tests_count": len(self.context.state.run_list),
607+ "test_number": todo_list.index(job.id),
608+ "tests_count": len(todo_list),
609 "command": job.command,
610 "flags": job.get_flag_set()
611 }
612- if not job_state.can_start():
613- test["outcome"] = "skip"
614- test["comments"] = job_state.get_readiness_description()
615- self.register_test_result(test)
616- return self.get_next_test()["result"]
617 return test
618 else:
619 return {}
620
621 @view
622 def register_test_result(self, test):
623- """
624- Registers outcome of a test
625- """
626+ """Registers outcome of a test."""
627 _logger.info("Storing test result: %s", test)
628 job_id = test['id']
629- job = self.context.state.job_state_map[job_id].job
630 builder_kwargs = {
631 'outcome': test['outcome'],
632- 'comments': test.get('comments', pod.UNSET)
633+ 'comments': test.get('comments', pod.UNSET),
634+ 'execution_duration': time.time() - test['start_time']
635 }
636- # some result may already have been saved if the job had some activity
637- # to run, i.e. result object is available in the test object
638 try:
639+ # if we're registering skipped test as an outcome of resuming
640+ # session, the result field of the test object will be missing
641 builder_kwargs['io_log_filename'] = test['result'].io_log_filename
642 except KeyError:
643- builder_kwargs['execution_duration'] = (
644- time.time() - test['start_time'])
645+ pass
646+
647 result = JobResultBuilder(**builder_kwargs).get_result()
648- self.context.state.update_job_result(job, result)
649+ self.assistant.use_job_result(job_id, result)
650 self.index += 1
651- self._checkpoint()
652+ self.assistant.update_app_blob(self._get_app_blob())
653
654 @view
655 def run_test_activity(self, test):
656- """
657- Run command associated with given test
658- """
659- job_id = test['id']
660- job_state = self.context.state.job_state_map[job_id]
661- job = job_state.job
662- self.context.state.running_job_name = job_id
663- self._checkpoint()
664- try:
665- result = self.runner.run_job(job, job_state, self.config, self.ui)
666- except OSError as exc:
667- result = JobResultBuilder(
668- outcome='fail',
669- comment=str(exc),
670- ).get_result()
671- self.context.state.running_job_name = None
672- self._checkpoint()
673- test['outcome'] = result.outcome
674- test['result'] = result
675+ """Run command associated with given test."""
676+ plugins_handled_natively = ['qml']
677+ res_builder = self.assistant.run_job(
678+ test['id'], self.ui, test['plugin'] in plugins_handled_natively)
679+ test['outcome'] = res_builder.outcome
680+ test['result'] = res_builder
681 return test
682
683 @view
684 def get_results(self):
685- """
686- Get results object
687- """
688- self.context.state.metadata.flags.remove('incomplete')
689- self._checkpoint()
690- stats = collections.defaultdict(int)
691- for job_state in self.context.state.job_state_map.values():
692- stats[job_state.result.outcome] += 1
693+ """Get results object."""
694+ self.assistant.finalize_session()
695+ stats = self.assistant.get_summary()
696 return {
697 'totalPassed': stats[IJobResult.OUTCOME_PASS],
698 'totalFailed': stats[IJobResult.OUTCOME_FAIL],
699- 'totalSkipped': stats[IJobResult.OUTCOME_SKIP],
700+ 'totalSkipped': stats[IJobResult.OUTCOME_SKIP] +
701+ stats[IJobResult.OUTCOME_NOT_SUPPORTED]
702 }
703
704 @view
705 def export_results(self, output_format, option_list):
706- """
707- Export results to file
708- """
709- # Export results in the user's Documents directory
710+ """Export results to file(s) in the user's 'Documents' directory.."""
711 dirname = self._get_user_directory_documents()
712- exporter = self.manager.create_exporter(output_format, option_list)
713- extension = exporter.unit.file_extension
714- filename = ''.join(['submission_', self.timestamp, '.', extension])
715- output_file = os.path.join(dirname, filename)
716- with open(output_file, 'wb') as stream:
717- exporter.dump_from_session_manager(self.manager, stream)
718- return output_file
719+ return self.assistant.export_to_file(
720+ output_format, option_list, dirname)
721
722 def _get_user_directory_documents(self):
723 xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \
724@@ -601,11 +443,6 @@
725 raise IOError("{} exists and is not a directory".format(path))
726 return path
727
728-
729- def _checkpoint(self):
730- self.context.state.metadata.app_blob = self._get_app_blob()
731- self.manager.checkpoint()
732-
733 def _get_app_blob(self):
734 """
735 Get json dump of with app-specific blob
736@@ -615,51 +452,22 @@
737 'version': 1,
738 'test_plan_id': self.test_plan_id,
739 'index_in_run_list': self.index,
740+ 'session_timestamp': self._timestamp,
741 }).encode("UTF-8")
742
743- def _init_test_plan_id(self, test_plan_id):
744- """
745- Validates and stores test_plan_id
746- """
747- if not isinstance(test_plan_id, str):
748- raise TypeError("test_plan_id must be a string")
749- # Look up the test plan with the specified identifier
750- id_map = self.context.compute_shared(
751- 'id_map', compute_value_map, self.context, 'id')
752- try:
753- test_plan = id_map[test_plan_id][0]
754- except KeyError:
755- raise ValueError(
756- "cannot find any unit with id: {!r}".format(test_plan_id))
757- if test_plan.Meta.name != 'test plan':
758- raise ValueError(
759- "unit {!r} is not a test plan".format(test_plan_id))
760- self.test_plan_id = test_plan_id
761- self.test_plan = test_plan
762-
763- def _init_session_storage_repo(self):
764- """
765- Init storage repository.
766- """
767- self.session_storage_repo = SessionStorageRepository(
768- self._get_app_cache_directory())
769-
770- def _get_default_providers(self, providers_dir):
771- """
772- Get providers
773+ def _get_embedded_providers(self, providers_dir):
774+ """
775+ Get providers included with the app
776
777 :param providers_dir:
778 Path within application tree from which to load providers
779 :returns:
780 list of loaded providers
781 """
782- provider_list = get_providers()
783- # when running on ubuntu-touch device, APP_DIR env var is present
784- # and points to touch application top directory
785+ provider_list = []
786 app_root_dir = os.path.normpath(os.getenv(
787 'APP_DIR', os.path.join(os.path.dirname(__file__), '..')))
788- path = os.path.join(app_root_dir,
789- os.path.normpath(providers_dir))
790+ path = os.path.join(app_root_dir, os.path.normpath(providers_dir))
791 _logger.info("Loading all providers from %s", path)
792 if os.path.exists(path):
793 embedded_providers = EmbeddedProvider1PlugInCollection(path)
794@@ -683,7 +491,7 @@
795
796
797 def get_qml_logger(default_level):
798- logging_level = collections.defaultdict(lambda:logging.INFO, {
799+ logging_level = collections.defaultdict(lambda: logging.INFO, {
800 "debug": logging.DEBUG,
801 "warning": logging.WARNING,
802 "warn": logging.WARN,
803
804=== modified file 'checkbox-touch/tests/autopilot/checkbox_touch/test_checkbox_touch.py'
805--- checkbox-touch/tests/autopilot/checkbox_touch/test_checkbox_touch.py 2015-07-23 08:25:48 +0000
806+++ checkbox-touch/tests/autopilot/checkbox_touch/test_checkbox_touch.py 2015-08-31 12:00:21 +0000
807@@ -62,41 +62,44 @@
808 ]
809 self.process_sequence_of_clicks_on_pages(next_steps)
810 self.skip_test('manualIntroPage')
811- next_steps = [
812- ('userInteractVerifyIntroPage', 'startTestButton'),
813- ('testVerificationPage', 'passButton'),
814- ('userInteractVerifyIntroPage', 'startTestButton'),
815- ('testVerificationPage', 'failButton')
816- ]
817- self.process_sequence_of_clicks_on_pages(next_steps)
818- self.skip_test('userInteractVerifyIntroPage')
819- next_steps = [
820- ('userInteractVerifyIntroPage', 'startTestButton'),
821- ('testVerificationPage', 'passButton'),
822- ('userInteractVerifyIntroPage', 'startTestButton'),
823- ('testVerificationPage', 'failButton')
824- ]
825- self.process_sequence_of_clicks_on_pages(next_steps)
826- self.skip_test('userInteractVerifyIntroPage')
827- next_steps = [
828- ('userInteractVerifyIntroPage', 'startTestButton'),
829- ('userInteractSummary', 'continueButton'),
830- ('userInteractVerifyIntroPage', 'startTestButton'),
831- ('userInteractSummary', 'continueButton'),
832- ]
833- self.process_sequence_of_clicks_on_pages(next_steps)
834- self.skip_test('userInteractVerifyIntroPage')
835- next_steps = [
836- ('userInteractVerifyIntroPage', 'startTestButton'),
837- ('testVerificationPage', 'passButton'),
838- ]
839- self.process_sequence_of_clicks_on_pages(next_steps)
840 # now we use long_wait because we have a long test to wait for (>10s)
841 self.long_wait_select_single(
842- self.app, objectName='qmlNativePage', visible=True)
843+ self.app, objectName='userInteractVerifyIntroPage', visible=True)
844+ next_steps = [
845+ ('userInteractVerifyIntroPage', 'startTestButton'),
846+ ('testVerificationPage', 'passButton'),
847+ ]
848+ self.process_sequence_of_clicks_on_pages(next_steps)
849 next_steps = [
850 ('qmlNativePage', 'continueButton'),
851 ('qmlTestPage', 'passButton'),
852+ ]
853+ self.process_sequence_of_clicks_on_pages(next_steps)
854+ next_steps = [
855+ ('userInteractVerifyIntroPage', 'startTestButton'),
856+ ('userInteractSummary', 'continueButton'),
857+ ('userInteractVerifyIntroPage', 'startTestButton'),
858+ ('userInteractSummary', 'continueButton'),
859+ ]
860+ self.process_sequence_of_clicks_on_pages(next_steps)
861+ self.skip_test('userInteractVerifyIntroPage')
862+ next_steps = [
863+ ('userInteractVerifyIntroPage', 'startTestButton'),
864+ ('testVerificationPage', 'passButton'),
865+ ('userInteractVerifyIntroPage', 'startTestButton'),
866+ ('testVerificationPage', 'failButton')
867+ ]
868+ self.process_sequence_of_clicks_on_pages(next_steps)
869+ self.skip_test('userInteractVerifyIntroPage')
870+ next_steps = [
871+ ('userInteractVerifyIntroPage', 'startTestButton'),
872+ ('testVerificationPage', 'passButton'),
873+ ('userInteractVerifyIntroPage', 'startTestButton'),
874+ ('testVerificationPage', 'failButton')
875+ ]
876+ self.process_sequence_of_clicks_on_pages(next_steps)
877+ self.skip_test('userInteractVerifyIntroPage')
878+ next_steps = [
879 ('rerunSelectionPage', 'continueButton')
880 ]
881 self.process_sequence_of_clicks_on_pages(next_steps)
882
883=== modified file 'plainbox/plainbox/impl/session/assistant.py'
884--- plainbox/plainbox/impl/session/assistant.py 2015-08-28 13:11:25 +0000
885+++ plainbox/plainbox/impl/session/assistant.py 2015-08-31 12:00:21 +0000
886@@ -371,7 +371,10 @@
887 self.session_available(self._manager.storage.id)
888 _logger.debug("New session created: %s", title)
889 UsageExpectation.of(self).allowed_calls = {
890- self.select_test_plan: "select the test plan to execute"
891+ self.select_test_plan: "select the test plan to execute",
892+ self.get_session_id: "to get the id of currently running session",
893+ self.get_session_dir: ("to get the path where current session is"
894+ "stored"),
895 }
896
897 @raises(KeyError, UnexpectedMethodCall)
898@@ -412,8 +415,9 @@
899 execution_ctrl_list=self._execution_ctrl_list)
900 self.session_available(self._manager.storage.id)
901 _logger.debug("Session resumed: %s", session_id)
902- UsageExpectation.of(self).allowed_calls = (
903- self._get_allowed_calls_in_normal_state())
904+ UsageExpectation.of(self).allowed_calls = {
905+ self.select_test_plan: "to save test plan selection",
906+ }
907 return self._resume_candidates[session_id][1]
908
909 @raises(UnexpectedMethodCall)
910@@ -556,6 +560,8 @@
911 to actually allowing the user to know what jobs are available.
912 """
913 UsageExpectation.of(self).enforce()
914+ UsageExpectation.of(self).allowed_calls = (
915+ self._get_allowed_calls_in_normal_state())
916 return [unit.id for unit in self._context.unit_list
917 if unit.Meta.name == 'test plan']
918
919@@ -1036,6 +1042,32 @@
920
921 return stats
922
923+ @raises(UnexpectedMethodCall)
924+ def finalize_session(self) -> None:
925+ """
926+ Finish the execution of the current session.
927+
928+ :raises UnexpectedMethodCall:
929+ If the call is made at an unexpected time. Do not catch this error.
930+ It is a bug in your program. The error message will indicate what
931+ is the likely cause.
932+
933+ Mark the session as complete, which prohibits running (or rerunning)
934+ any job.
935+ """
936+ UsageExpectation.of(self).enforce()
937+ if SessionMetaData.FLAG_SUBMITTED not in self._metadata.flags:
938+ _logger.warning("Finalizing session that hasn't been submitted "
939+ "anywhere: %s", self._manager.storage.id)
940+ self._metadata.flags.remove(SessionMetaData.FLAG_INCOMPLETE)
941+ self._manager.checkpoint()
942+ UsageExpectation.of(self).allowed_calls = {
943+ self.export_to_transport: "to export the results and send them",
944+ self.export_to_file: "to export the results to a file",
945+ self.get_resumable_sessions: "to get resume candidates",
946+ self.start_new_session: "to create a new session",
947+ }
948+
949 @raises(KeyError, TransportError, UnexpectedMethodCall)
950 def export_to_transport(
951 self, exporter_id: str, transport: ISessionStateTransport
952@@ -1186,6 +1218,8 @@
953 # XXX: should this be available right off the bat or should we wait
954 # until all of the mandatory jobs have been executed.
955 self.export_to_transport: "to export the results and send them",
956+ self.export_to_file: "to export the results to a file",
957+ self.finalize_session: "to mark the session as complete",
958 }
959
960

Subscribers

People subscribed via source and target branches