Merge lp:~kissiel/checkbox/qml-native into lp:checkbox

Proposed by Maciej Kisielewski
Status: Merged
Approved by: Zygmunt Krynicki
Approved revision: 3556
Merged at revision: 3554
Proposed branch: lp:~kissiel/checkbox/qml-native
Merge into: lp:checkbox
Diff against target: 1669 lines (+1324/-7)
29 files modified
cep/CEP-5.txt (+87/-0)
checkbox-touch/components/QmlNativePage.qml (+133/-0)
checkbox-touch/main.qml (+14/-0)
checkbox-touch/py/checkbox_touch.py (+1/-0)
plainbox/docs/conf.py (+2/-0)
plainbox/docs/manpages/plainbox-job-units.rst (+3/-0)
plainbox/docs/manpages/plainbox-qml-shell.rst (+18/-0)
plainbox/plainbox/data/qml-shell/pipe_handler.py (+42/-0)
plainbox/plainbox/data/qml-shell/plainbox_qml_shell.qml (+111/-0)
plainbox/plainbox/impl/ctrl.py (+157/-0)
plainbox/plainbox/impl/providers/stubbox/data/qml-navigation.qml (+68/-0)
plainbox/plainbox/impl/providers/stubbox/data/qml-simple.qml (+40/-0)
plainbox/plainbox/impl/providers/stubbox/units/jobs/categories.pxu (+4/-0)
plainbox/plainbox/impl/providers/stubbox/units/jobs/stub.pxu (+22/-0)
plainbox/plainbox/impl/runner.py (+73/-0)
plainbox/plainbox/impl/test_ctrl.py (+108/-0)
plainbox/plainbox/impl/unit/job.py (+39/-3)
plainbox/plainbox/impl/unit/test_job.py (+38/-2)
plainbox/plainbox/qml_shell/qml_shell.py (+141/-0)
plainbox/plainbox/qml_shell/qml_shell.qml (+75/-0)
plainbox/plainbox/test_provider_manager.py (+1/-1)
plainbox/setup.py (+1/-0)
providers/2015.com.canonical.certification:qml-tests/.bzrignore (+2/-0)
providers/2015.com.canonical.certification:qml-tests/.gitignore (+2/-0)
providers/2015.com.canonical.certification:qml-tests/README.md (+5/-0)
providers/2015.com.canonical.certification:qml-tests/data/camera-test.qml (+101/-0)
providers/2015.com.canonical.certification:qml-tests/manage.py (+21/-0)
providers/2015.com.canonical.certification:qml-tests/units/qml-tests.pxu (+14/-0)
support/develop-providers (+1/-1)
To merge this branch: bzr merge lp:~kissiel/checkbox/qml-native
Reviewer Review Type Date Requested Status
Zygmunt Krynicki (community) Approve
Review via email: mp+247960@code.launchpad.net

Description of the change

This MR includes same changes as https://code.launchpad.net/~kissiel/checkbox/qml-native-test/+merge/247759, but has completely rewrittenhistory for more clarity.

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

16:35 <@zyga> kissiel: love the man page
16:35 <@zyga> kissiel: let's merge it
16:35 < kissiel> zyga, \o/
16:36 <@zyga> kissiel: thanks for doing all the work and for iterating :)

review: Approve
Revision history for this message
Daniel Manrique (roadmr) wrote :
Download full text (11.2 KiB)

The attempt to merge lp:~kissiel/checkbox/qml-native into lp:checkbox failed. Below is the output from the failed tests.

[precise] starting container
[precise] (timing) 0.06user 0.01system 0:04.18elapsed 1%CPU (0avgtext+0avgdata 7864maxresident)k
[precise] (timing) 0inputs+32outputs (0major+5788minor)pagefaults 0swaps
[precise] provisioning container
[precise] (timing) 36.91user 10.70system 1:19.85elapsed 59%CPU (0avgtext+0avgdata 51680maxresident)k
[precise] (timing) 0inputs+16408outputs (0major+4472567minor)pagefaults 0swaps
[precise-testing] Starting tests...
Found a test script: ./checkbox-gui/requirements/container-tests-checkbox-gui-build
[precise-testing] container-tests-checkbox-gui-build: PASS
[precise-testing] (timing) 33.14user 2.50system 0:36.09elapsed 98%CPU (0avgtext+0avgdata 116468maxresident)k
[precise-testing] (timing) 0inputs+4216outputs (0major+477769minor)pagefaults 0swaps
Found a test script: ./checkbox-ng/requirements/container-tests-checkbox-ng-unit
[precise-testing] container-tests-checkbox-ng-unit: PASS
[precise-testing] (timing) 0.56user 0.11system 0:01.43elapsed 46%CPU (0avgtext+0avgdata 39924maxresident)k
[precise-testing] (timing) 0inputs+3480outputs (0major+20653minor)pagefaults 0swaps
Found a test script: ./checkbox-support/requirements/container-tests-checkbox-support
[precise-testing] container-tests-checkbox-support: PASS
[precise-testing] (timing) 16.92user 0.16system 0:17.20elapsed 99%CPU (0avgtext+0avgdata 83492maxresident)k
[precise-testing] (timing) 0inputs+1032outputs (0major+28807minor)pagefaults 0swaps
Found a test script: ./checkbox-touch/requirements/container-tests-touch-unit-tests
[precise-testing] container-tests-touch-unit-tests: PASS
[precise-testing] (timing) 0.00user 0.00system 0:00.01elapsed 42%CPU (0avgtext+0avgdata 2020maxresident)k
[precise-testing] (timing) 0inputs+8outputs (0major+2346minor)pagefaults 0swaps
Found a test script: ./plainbox/plainbox/impl/providers/categories/requirements/container-tests-provider-categories
[precise-testing] container-tests-provider-categories: PASS
[precise-testing] (timing) 0.14user 0.05system 0:00.20elapsed 92%CPU (0avgtext+0avgdata 13520maxresident)k
[precise-testing] (timing) 0inputs+176outputs (0major+5874minor)pagefaults 0swaps
Found a test script: ./plainbox/requirements/001-container-tests-plainbox-egg-info
[precise-testing] 001-container-tests-plainbox-egg-info: PASS
[precise-testing] (timing) 0.15user 0.03system 0:00.19elapsed 93%CPU (0avgtext+0avgdata 10520maxresident)k
[precise-testing] (timing) 0inputs+88outputs (0major+4989minor)pagefaults 0swaps
Found a test script: ./plainbox/requirements/container-tests-plainbox
[precise-testing] container-tests-plainbox: PASS
[precise-testing] (timing) 10.64user 0.72system 0:11.57elapsed 98%CPU (0avgtext+0avgdata 66500maxresident)k
[precise-testing] (timing) 0inputs+2776outputs (0major+103963minor)pagefaults 0swaps
Found a test script: ./plainbox/requirements/container-tests-plainbox-documentation
[precise-testing] container-tests-plainbox-documentation: FAIL
[precise-testing] stdout: http://...

lp:~kissiel/checkbox/qml-native updated
3553. By Maciej Kisielewski

plainbox:qml_shell: add standalone qml-native shell

This patch adds standalone qml-shell that can be run without a need to run
'plainbox run (...)', which would require provider to be created and loaded.

Signed-off-by: Maciej Kisielewski <email address hidden>

3554. By Maciej Kisielewski

plainbox:doc: add manpage for plainbox-qml-shell

Signed-off-by: Maciej Kisielewski <email address hidden>

3555. By Maciej Kisielewski

plainbox: add plainbox-qml-shell to setup.py

This patch adds plainbox-qml-shell to console_scripts in setup.py

Signed-off-by: Maciej Kisielewski <email address hidden>

3556. By Maciej Kisielewski

providers: add qml-tests provider

This patch adds provider with qml-native jobs. At start there is only one test
- camera test that shows feed from camera attached to the system. The provider
  is added to venv.

Signed-off-by: Maciej Kisielewski <email address hidden>

Revision history for this message
Zygmunt Krynicki (zyga) wrote :

FYI:

23:25 < kissiel> zyga, omg
23:25 < kissiel> zyga, i found the problem
23:25 < kissiel> zyga, uncommitted __init__
23:25 < kissiel> in qml_shell module...
23:25 < kissiel> which isn't a module :D
23:26 <@zyga> kissiel: ah
23:26 <@zyga> kissiel: well, python3.4 doesn't need init
23:26 <@zyga> kissiel: so it works there
23:27 < kissiel> zyga, right :)
23:27 <@zyga> kissiel: in 3.2 you still need (even empty) __init__.py

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'cep/CEP-5.txt'
2--- cep/CEP-5.txt 1970-01-01 00:00:00 +0000
3+++ cep/CEP-5.txt 2015-01-29 23:33:32 +0000
4@@ -0,0 +1,87 @@
5+Summary
6+=======
7+
8+Checkbox stack lacks facilities for running GUI driven tests natively.
9+This CEP describes a new type of checkbox job, the 'QML job'.
10+
11+Rationale
12+=========
13+
14+Some of the tests require some form of graphical user interface. Current way of
15+handling that is to run some binary (like qmlscene) driving the GUI. This
16+approach is acceptable on window-driven environments like Ubuntu Desktop, but
17+unfeasible on Ubuntu Touch. Moreover, Checkbox front-end for Ubuntu Touch is
18+already driven by qmlscene, so GUI jobs might be as well defined as a
19+component resembling native parts of Checkbox Touch.
20+
21+What qml native test should be able to do (aka Requirements)
22+============================================================
23+
24+1. Determine its own outcome and propagate it to the shell
25+2. Run full screen (without system bar, headers, etc.)
26+3. Run its own internal page flow mechanism (like PageStack, tabs)
27+4. Use all QML APIs that a normal app would use
28+5. Get job meta-info (i.e. job id, job name, etc.)
29+6. Be fully translatable
30+7. Print out ``console.log()``
31+
32+Constraints
33+===========
34+
35+All that consitutes the job (qml files, translations, images, media files,
36+etc.) must be contained within ``data`` directory of the provider.
37+
38+The API
39+=======
40+
41+Test definition
42+---------------
43+From testing shell perspective, the test is a root ``Item { }`` of the file
44+located in path specified by ``qml_file`` field of the job definition.
45+
46+When creating the test component, testing environment will initiate
47+``testingShell`` property with a hook to the testing shell object (satisfying
48+requirement no 5.), and connect to ``testDone(test)`` signal. ``test`` object
49+has to have ``outcome`` field, in order to satisfy requirement no 1.
50+
51+Test boilerplate
52+----------------
53+
54+.. code-block:: guess
55+
56+ // boilerplate.qml
57+ import QtQuick 2.0
58+ Item {
59+ property var testingShell;
60+ signal testDone(test);
61+ }
62+
63+
64+Testing Shell object API:
65+=========================
66+
67+``function getTest()`` - get test meta-information object
68+
69+``string name`` - name of the testing shell
70+
71+``var pageStack`` - pageStack object to use when implementing internal test
72+navigation. This page stack is independent from the page stack used by outside
73+application (the one containing testing shell).
74+
75+Full screen and page stacks
76+===========================
77+
78+Test shell is responsible for clearing current screen, i.e. hiding toolbars,
79+headers, etc., and for providing clear page stack to work on.
80+This internal page stack is implicitly cleared and destroyed when testDone is
81+signalled.
82+
83+
84+Impact
85+======
86+Introduction of new job type requires changes in plainbox backend (new value of
87+plugin field in job definition) and all checkbox front-ends. For front-ends to
88+display qml job, shell that runs the job has to be present. For Checkbox Touch
89+this shell will be native to the application, for other front-ends auxiliary
90+qmlscene has to be run. All changes required by this CEP should not impact
91+exisiting tests.
92
93=== added file 'checkbox-touch/components/QmlNativePage.qml'
94--- checkbox-touch/components/QmlNativePage.qml 1970-01-01 00:00:00 +0000
95+++ checkbox-touch/components/QmlNativePage.qml 2015-01-29 23:33:32 +0000
96@@ -0,0 +1,133 @@
97+/*
98+ * This file is part of Checkbox
99+ *
100+ * Copyright 2014 Canonical Ltd.
101+ *
102+ * Authors:
103+ * - Maciej Kisielewski <maciej.kisielewski@canonical.com>
104+ *
105+ * This program is free software; you can redistribute it and/or modify
106+ * it under the terms of the GNU General Public License as published by
107+ * the Free Software Foundation; version 3.
108+ *
109+ * This program is distributed in the hope that it will be useful,
110+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
111+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
112+ * GNU General Public License for more details.
113+ *
114+ * You should have received a copy of the GNU General Public License
115+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
116+ */
117+
118+/*! \brief Page for Qml native test
119+*/
120+
121+import QtQuick 2.0
122+import Ubuntu.Components 1.1
123+import QtQuick.Layouts 1.1
124+import "ConfirmationLogic.js" as ConfirmationLogic
125+
126+Page {
127+ id: qmlNativePage
128+ property var test: { "name": "", "description": "", "test_number": 0, "tests_count": 0}
129+
130+ signal testDone(var test);
131+
132+ objectName: "qmlNativePage"
133+ title: i18n.tr("Test Description")
134+
135+ Object {
136+ id: testingShell
137+ property string name: "Checkbox-touch qml shell"
138+ property alias pageStack: qmlNativePage.pageStack
139+ function getTest() {
140+ return test;
141+ }
142+ }
143+
144+ head {
145+ actions: [
146+ Action {
147+ id: skipAction
148+ iconName: "media-seek-forward"
149+ text: i18n.tr("Skip")
150+ onTriggered: {
151+ var confirmationOptions = {
152+ question : i18n.tr("Do you really want to skip this test?"),
153+ remember : true,
154+ }
155+ ConfirmationLogic.confirmRequest(qmlNativePage,
156+ confirmationOptions, function(res) {
157+ if (res) {
158+ test["outcome"] = "skip";
159+ testDone(test);
160+ }
161+ });
162+ }
163+ }
164+ ]
165+ }
166+
167+ ColumnLayout {
168+ id: body
169+ spacing: units.gu(3)
170+ anchors.fill: parent
171+ anchors.margins: units.gu(3)
172+
173+ Label {
174+ fontSize: "large"
175+ Layout.fillWidth: true
176+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
177+ text: test["name"]
178+ font.bold: true
179+ }
180+
181+ Flickable {
182+ Layout.fillWidth: true
183+ Layout.fillHeight: true
184+ contentHeight: childrenRect.height
185+ flickableDirection: Flickable.VerticalFlick
186+ clip: true
187+ Label {
188+ fontSize: "medium"
189+ anchors.fill: parent
190+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
191+ text: test["description"]
192+ }
193+ }
194+
195+ LatchButton {
196+ objectName: "continueButton"
197+ color: UbuntuColors.green
198+ Layout.fillWidth: true
199+ text: i18n.tr("Continue")
200+ onClicked: {
201+ pageStack.pop(); // pop the description page
202+ // altough there shouldn't be anything on the page stack
203+ // dump it to savedStack and clear page stack,
204+ // to let qml job use fresh playground
205+ var savedStack = [];
206+ while(pageStack.depth) {
207+ savedStack.push(pageStack.currentPage);
208+ pageStack.pop();
209+ }
210+ // prepare page with the test
211+ var testItemComponent = Qt.createComponent(Qt.resolvedUrl(test['qml_file']));
212+ if (testItemComponent.status == Component.Error) {
213+ console.log("Error creating testPageComponent:", testPageComponent.errorString());
214+ }
215+
216+ var testItem = testItemComponent.createObject(null, {"testingShell": testingShell});
217+ testItem.testDone.connect(function(testResult) {
218+ test['outcome'] = testResult['outcome'];
219+ test['result'] = testResult;
220+ pageStack.clear(); // clean test's left-overs from the stack
221+ while(savedStack.length) {
222+ pageStack.push(savedStack.pop());
223+ }
224+ testDone(test);
225+ });
226+ }
227+ }
228+ }
229+}
230
231=== modified file 'checkbox-touch/main.qml'
232--- checkbox-touch/main.qml 2015-01-19 14:56:12 +0000
233+++ checkbox-touch/main.qml 2015-01-29 23:33:32 +0000
234@@ -327,6 +327,9 @@
235 case 'user-interact':
236 performUserInteractTest(test);
237 break;
238+ case 'qml':
239+ performQmlTest(test);
240+ break;
241 default:
242 test.outcome = "skip";
243 completeTest(test);
244@@ -430,6 +433,17 @@
245 pageStack.push(InteractIntroPage);
246 }
247
248+ function performQmlTest(test) {
249+ var comp = Qt.createComponent(Qt.resolvedUrl("components/QmlNativePage.qml"))
250+ console.log(comp.errorString());
251+ var qmlNativePage = comp.createObject();
252+ qmlNativePage.test = test
253+ qmlNativePage.testDone.connect(completeTest);
254+ qmlNativePage.__customHeaderContents = progressHeader;
255+ progressHeader.update(test);
256+ pageStack.push(qmlNativePage);
257+ }
258+
259 function showVerificationScreen(test) {
260 var verificationPage = Qt.createComponent(Qt.resolvedUrl("components/TestVerificationPage.qml")).createObject();
261 verificationPage.test = test
262
263=== modified file 'checkbox-touch/py/checkbox_touch.py'
264--- checkbox-touch/py/checkbox_touch.py 2015-01-07 14:52:42 +0000
265+++ checkbox-touch/py/checkbox_touch.py 2015-01-29 23:33:32 +0000
266@@ -602,6 +602,7 @@
267 job.tr_verification() is not None else description,
268 "plugin": job.plugin,
269 "id": job.id,
270+ "qml_file": job.qml_file,
271 "start_time": time.time(),
272 "test_number": self.index,
273 "tests_count": len(self.context.state.run_list)
274
275=== modified file 'plainbox/docs/conf.py'
276--- plainbox/docs/conf.py 2015-01-27 14:35:15 +0000
277+++ plainbox/docs/conf.py 2015-01-29 23:33:32 +0000
278@@ -298,6 +298,8 @@
279 'list and describe various objects', _authors, 1),
280 ('manpages/plainbox-device', 'plainbox-device',
281 'device management commands', _authors, 1),
282+ ('manpages/plainbox-qml-shell', 'plainbox-qml-shell',
283+ 'standalone qml-native shell', _authors, 1),
284 # Section 5
285 ('manpages/plainbox.conf', 'plainbox.conf',
286 'plainbox configuration file', _authors, 5),
287
288=== modified file 'plainbox/docs/manpages/plainbox-job-units.rst'
289--- plainbox/docs/manpages/plainbox-job-units.rst 2014-11-25 11:33:53 +0000
290+++ plainbox/docs/manpages/plainbox-job-units.rst 2015-01-29 23:33:32 +0000
291@@ -62,6 +62,9 @@
292 :resource: A job whose command output results in a set of rfc822
293 records, containing key/value pairs, and that can be used in other
294 jobs' ``requires`` expressions.
295+ :qml: jobs that run a custom QML payload within a test shell (QML
296+ application or a generic, minimalistic QML test shell) using test API
297+ described in CEP-5
298
299 .. warning::
300 The following plugin names are deprecated:
301
302=== added file 'plainbox/docs/manpages/plainbox-qml-shell.rst'
303--- plainbox/docs/manpages/plainbox-qml-shell.rst 1970-01-01 00:00:00 +0000
304+++ plainbox/docs/manpages/plainbox-qml-shell.rst 2015-01-29 23:33:32 +0000
305@@ -0,0 +1,18 @@
306+======================
307+plainbox-qml-shell (1)
308+======================
309+
310+.. argparse::
311+ :ref: plainbox.qml_shell.qml_shell.get_parser_for_sphinx
312+ :prog: plainbox-qml-shell
313+ :manpage:
314+ :nodefault:
315+
316+ This command runs qml job provided by specified file.
317+
318+
319+
320+See Also
321+========
322+
323+:doc:`plainbox-run`
324
325=== added directory 'plainbox/plainbox/data/qml-shell'
326=== added file 'plainbox/plainbox/data/qml-shell/pipe_handler.py'
327--- plainbox/plainbox/data/qml-shell/pipe_handler.py 1970-01-01 00:00:00 +0000
328+++ plainbox/plainbox/data/qml-shell/pipe_handler.py 2015-01-29 23:33:32 +0000
329@@ -0,0 +1,42 @@
330+# This file is part of Checkbox.
331+#
332+# Copyright 2012-2014 Canonical Ltd.
333+# Written by:
334+# Maciej Kisielewski <maciej.kisielewski@canonical.com>
335+#
336+# Checkbox is free software: you can redistribute it and/or modify
337+# it under the terms of the GNU General Public License version 3,
338+# as published by the Free Software Foundation.
339+#
340+# Checkbox is distributed in the hope that it will be useful,
341+# but WITHOUT ANY WARRANTY; without even the implied warranty of
342+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
343+# GNU General Public License for more details.
344+#
345+# You should have received a copy of the GNU General Public License
346+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
347+
348+import os
349+
350+
351+def write_and_close(s, fd: str) -> None:
352+ """
353+ Write ``s`` to file descriptor ``fd`` and close ``fd`` afterwards.
354+
355+ fd is of type str because calling code is written in javascript that
356+ doesn't support notion of ints.
357+ """
358+ with os.fdopen(int(fd), 'wt', encoding='utf-8') as stream:
359+ stream.write(s)
360+
361+
362+def read_and_close(fd: str) -> str:
363+ """
364+ Read from ``fd`` file descriptor and close ``fd`` afterwards.
365+ returns read string
366+
367+ fd is of type str because calling code is written in javascript that
368+ doesn't support notion of ints.
369+ """
370+ with os.fdopen(int(fd), 'rt', encoding='utf-8') as stream:
371+ return stream.read()
372
373=== added file 'plainbox/plainbox/data/qml-shell/plainbox_qml_shell.qml'
374--- plainbox/plainbox/data/qml-shell/plainbox_qml_shell.qml 1970-01-01 00:00:00 +0000
375+++ plainbox/plainbox/data/qml-shell/plainbox_qml_shell.qml 2015-01-29 23:33:32 +0000
376@@ -0,0 +1,111 @@
377+/*
378+ * This file is part of Checkbox
379+ *
380+ * Copyright 2014 Canonical Ltd.
381+ *
382+ * Authors:
383+ * - Maciej Kisielewski <maciej.kisielewski@canonical.com>
384+ *
385+ * This program is free software; you can redistribute it and/or modify
386+ * it under the terms of the GNU General Public License as published by
387+ * the Free Software Foundation; version 3.
388+ *
389+ * This program is distributed in the hope that it will be useful,
390+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
391+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
392+ * GNU General Public License for more details.
393+ *
394+ * You should have received a copy of the GNU General Public License
395+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
396+ */
397+/*! \brief QML standalone shell
398+
399+ This component serves as QML shell that embeds native QML plainbox jobs.
400+ The job it loads is specified by --job argument passed to qmlscene
401+ launching this component.
402+ Job to be run must have testingShell property that be used as a hook to
403+ 'outside world'. The job should also define testDone signal that gets
404+ signalled once test is finished. The signal should pass result object
405+ containing following fields:
406+ 'outcome' (mandatory): outcome of a test, e.g. 'pass', 'fail', 'undecided'.
407+ 'suggestedOutcome': if outcome is 'undecided', than this suggestion will be
408+ presented to the user, letting them decide the final outcome of a test.
409+*/
410+import QtQuick 2.0
411+import Ubuntu.Components 0.1
412+import io.thp.pyotherside 1.2
413+
414+MainView {
415+ id: mainView
416+ width: units.gu(100)
417+ height: units.gu(75)
418+
419+ Python {
420+ id: py
421+ Component.onCompleted: {
422+ addImportPath(Qt.resolvedUrl('.'));
423+ py.importModule('pipe_handler', function() {
424+ console.log('pipe_handler.py imported');
425+ py.readAndClose(args.values['fd-in'], function(job_repr) {
426+ testingShell.getTest = function() {
427+ return JSON.parse(job_repr);
428+ }
429+ loader.setSource(args.values.job,
430+ {'testingShell': testingShell});
431+ });
432+ });
433+ }
434+
435+ onError: console.error("python error: " + traceback)
436+ onReceived: console.log("pyotherside.send: " + data)
437+
438+ function writeAndClose(str, fd, continuation) {
439+ py.call('pipe_handler.write_and_close', [str, fd], continuation);
440+ }
441+ function readAndClose(fd, continuation) {
442+ py.call('pipe_handler.read_and_close', [fd], continuation);
443+ }
444+ }
445+
446+ // information and functionality passed to qml job component
447+ property var testingShell: {
448+ "name": "Plainbox qml shell",
449+ "pageStack": pageStack
450+ }
451+
452+ Arguments {
453+ id: args
454+ Argument {
455+ name: "job"
456+ help: "QML-native job to run"
457+ required: true
458+ valueNames: ["PATH"]
459+ }
460+ Argument {
461+ name: "fd-out"
462+ help: "Descriptor number of pipe to write to"
463+ required: false
464+ valueNames: ["N"]
465+ }
466+ Argument {
467+ name: "fd-in"
468+ help: "Descriptor number of pipe to read from"
469+ required: false
470+ valueNames: ["N"]
471+ }
472+ }
473+
474+ Loader {
475+ id: loader
476+ visible: false
477+ onLoaded: loader.item.testDone.connect(testDone)
478+ }
479+
480+ function testDone(res) {
481+ py.writeAndClose(JSON.stringify(res), args.values['fd-out'], Qt.quit);
482+ }
483+ PageStack {
484+ id: pageStack
485+ }
486+
487+}
488
489=== modified file 'plainbox/plainbox/impl/ctrl.py'
490--- plainbox/plainbox/impl/ctrl.py 2015-01-09 21:48:46 +0000
491+++ plainbox/plainbox/impl/ctrl.py 2015-01-29 23:33:32 +0000
492@@ -35,11 +35,13 @@
493
494 import abc
495 import contextlib
496+import errno
497 try:
498 import grp
499 except ImportError:
500 grp = None
501 import itertools
502+import json
503 import logging
504 import os
505 try:
506@@ -54,6 +56,7 @@
507 from plainbox.abc import IJobResult
508 from plainbox.abc import ISessionStateController
509 from plainbox.i18n import gettext as _
510+from plainbox.impl import get_plainbox_dir
511 from plainbox.impl.depmgr import DependencyDuplicateError
512 from plainbox.impl.depmgr import DependencyMissingError
513 from plainbox.impl.resource import ExpressionCannotEvaluateError
514@@ -806,6 +809,160 @@
515 return 1
516
517
518+class QmlJobExecutionController(CheckBoxExecutionController):
519+ """
520+ An execution controller that is able to run jobs in QML shell.
521+ """
522+
523+ QML_SHELL_PATH = os.path.join(get_plainbox_dir(), 'data', 'qml-shell',
524+ 'plainbox_qml_shell.qml')
525+
526+ def get_execution_command(self, job, job_state, config, session_dir,
527+ nest_dir, shell_out_fd, shell_in_fd):
528+ """
529+ Get the command to execute the specified job
530+
531+ :param job:
532+ job definition with the command and environment definitions
533+ :param job_state:
534+ The JobState associated to the job to execute.
535+ :param config:
536+ A PlainBoxConfig instance which can be used to load missing
537+ environment definitions that apply to all jobs. Ignored.
538+ :param session_dir:
539+ Base directory of the session this job will execute in.
540+ This directory is used to co-locate some data that is unique to
541+ this execution as well as data that is shared by all executions.
542+ Ignored.
543+ :param nest_dir:
544+ A directory with a nest of symlinks to all executables required to
545+ execute the specified job. Ingored.
546+ :param shell_out_fd:
547+ File descriptor number which is used to pipe through result object
548+ from the qml shell to plainbox.
549+ :param shell_in_fd:
550+ File descriptor number which is used to pipe through test meta
551+ information from plainbox to qml shell.
552+ :returns:
553+ List of command arguments
554+
555+ """
556+ cmd = ['qmlscene', '--job', job.qml_file, '--fd-out', shell_out_fd,
557+ '--fd-in', shell_in_fd, self.QML_SHELL_PATH]
558+ return cmd
559+
560+ def get_checkbox_score(self, job):
561+ """
562+ Compute how applicable this controller is for the specified job.
563+
564+ :returns:
565+ 4 if the job is a qml job or -1 otherwise
566+ """
567+ if job.plugin == 'qml':
568+ return 4
569+ else:
570+ return -1
571+
572+ def gen_job_repr(self, job):
573+ """
574+ Generate simplified job representation for use in qml shell
575+ :returns:
576+ dictionary with simplified job representation
577+ """
578+ logger.debug(_("Generating job repr for job: %r"), job)
579+ return {
580+ "id": job.id,
581+ "summary": job.tr_summary(),
582+ "description": job.tr_description(),
583+ }
584+
585+ def execute_job(self, job, job_state, config, session_dir, extcmd_popen):
586+ """
587+ Execute the specified job using the specified subprocess-like object,
588+ passing fd with opened pipe for qml-shell->plainbox communication.
589+
590+ :param job:
591+ The JobDefinition to execute
592+ :param config:
593+ A PlainBoxConfig instance which can be used to load missing
594+ environment definitions that apply to all jobs. It is used to
595+ provide values for missing environment variables that are required
596+ by the job (as expressed by the environ key in the job definition
597+ file).
598+ :param session_dir:
599+ Base directory of the session this job will execute in.
600+ This directory is used to co-locate some data that is unique to
601+ this execution as well as data that is shared by all executions.
602+ :param extcmd_popen:
603+ A subprocess.Popen like object
604+ :returns:
605+ The return code of the command, as returned by subprocess.call()
606+ """
607+
608+ class DuplexPipe:
609+ """
610+ Helper context creating two pipes, ensuring they are closed
611+ properly
612+ """
613+ def __enter__(self):
614+ self.a_read, self.b_write = os.pipe()
615+ self.b_read, self.a_write = os.pipe()
616+ return self.a_read, self.b_write, self.b_read, self.a_write
617+
618+ def __exit__(self, *args):
619+ for pipe in (self.a_read, self.b_write,
620+ self.b_read, self.a_write):
621+ # typically those pipes are already closed; trying to
622+ # re-close them causes OSError (errno == 9) to be raised
623+ try:
624+ os.close(pipe)
625+ except OSError as exc:
626+ if exc.errno != errno.EBADF:
627+ raise
628+ # CHECKBOX_DATA is where jobs can share output.
629+ # It has to be an directory that scripts can assume exists.
630+ if not os.path.isdir(self.get_CHECKBOX_DATA(session_dir)):
631+ os.makedirs(self.get_CHECKBOX_DATA(session_dir))
632+ # Setup the executable nest directory
633+ with self.configured_filesystem(job, config) as nest_dir:
634+ with DuplexPipe() as (plainbox_read, shell_write,
635+ shell_read, plainbox_write):
636+ # Get the command and the environment.
637+ # of this execution controller
638+ cmd = self.get_execution_command(
639+ job, job_state, config, session_dir, nest_dir,
640+ str(shell_write), str(shell_read))
641+ env = self.get_execution_environment(
642+ job, job_state, config, session_dir, nest_dir)
643+ with self.temporary_cwd(job, config) as cwd_dir:
644+ job_json = json.dumps(self.gen_job_repr(job))
645+ pipe_out = os.fdopen(plainbox_write, 'wt')
646+ pipe_out.write(job_json)
647+ pipe_out.close()
648+ # run the command
649+ logger.debug(_("job[%s] executing %r with"
650+ "env %r in cwd %r"),
651+ job.id, cmd, env, cwd_dir)
652+ ret = extcmd_popen.call(cmd, env=env, cwd=cwd_dir,
653+ pass_fds=[shell_write, shell_read])
654+ os.close(shell_read)
655+ os.close(shell_write)
656+ pipe_in = os.fdopen(plainbox_read)
657+ res_object_json_string = pipe_in.read()
658+ pipe_in.close()
659+ if ret != 0:
660+ return ret
661+ try:
662+ result = json.loads(res_object_json_string)
663+ if result['outcome'] == "pass":
664+ return 0
665+ else:
666+ return 1
667+ except ValueError:
668+ # qml-job did not print proper json object
669+ return 1
670+
671+
672 class CheckBoxDifferentialExecutionController(CheckBoxExecutionController):
673 """
674 A CheckBoxExecutionController subclass that uses differential environment.
675
676=== added file 'plainbox/plainbox/impl/providers/stubbox/data/qml-navigation.qml'
677--- plainbox/plainbox/impl/providers/stubbox/data/qml-navigation.qml 1970-01-01 00:00:00 +0000
678+++ plainbox/plainbox/impl/providers/stubbox/data/qml-navigation.qml 2015-01-29 23:33:32 +0000
679@@ -0,0 +1,68 @@
680+import QtQuick 2.0
681+import Ubuntu.Components 0.1
682+import QtQuick.Layouts 1.1
683+
684+Item {
685+ id: root
686+ signal testDone(var test);
687+ property var testingShell;
688+
689+ Component.onCompleted: testingShell.pageStack.push(mainPage)
690+
691+ Page {
692+ id: mainPage
693+ title: i18n.tr("A simple test")
694+
695+ ColumnLayout {
696+ spacing: units.gu(10)
697+ anchors {
698+ margins: units.gu(5)
699+ fill: parent
700+ }
701+
702+ Button {
703+ Layout.fillWidth: true; Layout.fillHeight: true
704+ text: i18n.tr("Next screen")
705+ color: "#38B44A"
706+ onClicked: {
707+ testingShell.pageStack.push(subPage);
708+ }
709+ }
710+ }
711+ }
712+
713+
714+ Page {
715+ id: subPage
716+ visible: false
717+ ColumnLayout {
718+ spacing: units.gu(10)
719+ anchors {
720+ margins: units.gu(5)
721+ fill: parent
722+ }
723+
724+ Text {
725+ text: i18n.tr("You can use toolbar to nagivage back")
726+ }
727+
728+ Button {
729+ Layout.fillWidth: true; Layout.fillHeight: true
730+ text: i18n.tr("Pass")
731+ color: "#38B44A"
732+ onClicked: {
733+ testDone({'outcome': 'pass'});
734+ }
735+ }
736+
737+ Button {
738+ Layout.fillWidth: true; Layout.fillHeight: true
739+ text: i18n.tr("Fail")
740+ color: "#DF382C"
741+ onClicked: {
742+ testDone({"outcome": "fail"});
743+ }
744+ }
745+ }
746+ }
747+}
748
749=== added file 'plainbox/plainbox/impl/providers/stubbox/data/qml-simple.qml'
750--- plainbox/plainbox/impl/providers/stubbox/data/qml-simple.qml 1970-01-01 00:00:00 +0000
751+++ plainbox/plainbox/impl/providers/stubbox/data/qml-simple.qml 2015-01-29 23:33:32 +0000
752@@ -0,0 +1,40 @@
753+import QtQuick 2.0
754+import Ubuntu.Components 0.1
755+import QtQuick.Layouts 1.1
756+
757+Item {
758+ id: root
759+ signal testDone(var test);
760+ property var testingShell;
761+
762+ Component.onCompleted: testingShell.pageStack.push(testPage)
763+
764+ Page {
765+ id: testPage
766+ ColumnLayout {
767+ spacing: units.gu(10)
768+ anchors {
769+ margins: units.gu(5)
770+ fill: parent
771+ }
772+
773+ Button {
774+ Layout.fillWidth: true; Layout.fillHeight: true
775+ text: i18n.tr("Pass")
776+ color: "#38B44A"
777+ onClicked: {
778+ testDone({'outcome': 'pass'});
779+ }
780+ }
781+
782+ Button {
783+ Layout.fillWidth: true; Layout.fillHeight: true
784+ text: i18n.tr("Fail")
785+ color: "#DF382C"
786+ onClicked: {
787+ testDone({"outcome": "fail"});
788+ }
789+ }
790+ }
791+ }
792+}
793
794=== modified file 'plainbox/plainbox/impl/providers/stubbox/units/jobs/categories.pxu'
795--- plainbox/plainbox/impl/providers/stubbox/units/jobs/categories.pxu 2014-12-01 11:31:15 +0000
796+++ plainbox/plainbox/impl/providers/stubbox/units/jobs/categories.pxu 2015-01-29 23:33:32 +0000
797@@ -29,3 +29,7 @@
798 id: overridden
799 unit: category
800 _name: Overridden Category
801+
802+id: qml-native
803+unit: category
804+_name: QML-native tests
805
806=== modified file 'plainbox/plainbox/impl/providers/stubbox/units/jobs/stub.pxu'
807--- plainbox/plainbox/impl/providers/stubbox/units/jobs/stub.pxu 2015-01-26 15:38:31 +0000
808+++ plainbox/plainbox/impl/providers/stubbox/units/jobs/stub.pxu 2015-01-29 23:33:32 +0000
809@@ -421,3 +421,25 @@
810 dd if=/dev/zero bs=1M count=16384
811 estimated_duration: 750
812 category_id: stress
813+
814+id: stub/qml-simple
815+_summary: A QML job that runs simple GUI
816+_description:
817+ This job displays a GUI that has two buttons determining outcome of the test.
818+ It's similar to user-interact-verify, but this job is QML native.
819+plugin: qml
820+qml_file: qml-simple.qml
821+flags: preserve-locale
822+estimated_duration: 10
823+category_id: qml-native
824+
825+id: stub/qml-navigation
826+_summary: A QML job that has its own navigation
827+_description:
828+ This job displays a GUI with multiple screens using its own (independent) flow
829+ control mechanism (page stack).
830+plugin: qml
831+qml_file: qml-navigation.qml
832+flags: preserve-locale
833+estimated_duration: 20
834+category_id: qml-native
835
836=== modified file 'plainbox/plainbox/impl/runner.py'
837--- plainbox/plainbox/impl/runner.py 2015-01-26 15:38:31 +0000
838+++ plainbox/plainbox/impl/runner.py 2015-01-29 23:33:32 +0000
839@@ -296,12 +296,14 @@
840 from plainbox.impl.ctrl import RootViaPTL1ExecutionController
841 from plainbox.impl.ctrl import RootViaSudoExecutionController
842 from plainbox.impl.ctrl import UserJobExecutionController
843+ from plainbox.impl.ctrl import QmlJobExecutionController
844 execution_ctrl_list = [
845 RootViaPTL1ExecutionController(provider_list),
846 RootViaPkexecExecutionController(provider_list),
847 # XXX: maybe this one should be only used on command line
848 RootViaSudoExecutionController(provider_list),
849 UserJobExecutionController(provider_list),
850+ QmlJobExecutionController(provider_list),
851 ]
852 elif sys.platform == 'win32':
853 from plainbox.impl.ctrl import UserJobExecutionController
854@@ -680,6 +682,77 @@
855 result_cmd.outcome = IJobResult.OUTCOME_UNDECIDED
856 return result_cmd
857
858+ def run_qml_job(self, job, job_state, config):
859+ """
860+ Method called to run a job with plugin field equal to 'qml'
861+
862+ The 'qml' job implements the following scenario:
863+
864+ * Maybe display the description to the user
865+ * Run qmlscene with provided test and wait for it to finish
866+ * Decide on the outcome based on the result object returned by qml
867+ shell
868+ * The method ends here
869+
870+ .. note::
871+ QML jobs are fully manual jobs with graphical user interface
872+ implemented in QML. They implement proposal described in CEP-5.
873+ """
874+ if job.plugin != "qml":
875+ # TRANSLATORS: please keep 'plugin' untranslated
876+ raise ValueError(_("bad job plugin value"))
877+ try:
878+ ctrl = self._get_ctrl_for_job(job)
879+ except LookupError:
880+ return MemoryJobResult({
881+ 'outcome': IJobResult.OUTCOME_NOT_SUPPORTED,
882+ 'comment': _('No suitable execution controller is available)'),
883+ })
884+ # Run the embedded command
885+ start_time = time.time()
886+ delegate, io_log_gen = self._prepare_io_handling(job, config)
887+ # Create a subprocess.Popen() like object that uses the delegate
888+ # system to observe all IO as it occurs in real time.
889+ delegate_cls = self._get_delegate_cls(config)
890+ extcmd_popen = delegate_cls(delegate)
891+ # Stream all IOLogRecord entries to disk
892+ record_path = os.path.join(
893+ self._jobs_io_log_dir, "{}.record.gz".format(
894+ slugify(job.id)))
895+ with gzip.open(record_path, mode='wb') as gzip_stream, \
896+ io.TextIOWrapper(
897+ gzip_stream, encoding='UTF-8') as record_stream:
898+ writer = IOLogRecordWriter(record_stream)
899+ io_log_gen.on_new_record.connect(writer.write_record)
900+ try:
901+ # Start the process and wait for it to finish getting the
902+ # result code. This will actually call a number of callbacks
903+ # while the process is running. It will also spawn a few
904+ # threads although all callbacks will be fired from a single
905+ # thread (which is _not_ the main thread)
906+ logger.debug(
907+ _("job[%s] starting qml shell: %s"), job.id, job.qml_file)
908+ # Run the job command using extcmd
909+ return_code = self._run_extcmd(job, job_state, config,
910+ extcmd_popen, ctrl)
911+ logger.debug(
912+ _("job[%s] shell return code: %r"), job.id, return_code)
913+ finally:
914+ io_log_gen.on_new_record.disconnect(writer.write_record)
915+ execution_duration = time.time() - start_time
916+ # Convert the return of the command to the outcome of the job
917+ if return_code == 0:
918+ outcome = IJobResult.OUTCOME_PASS
919+ else:
920+ outcome = IJobResult.OUTCOME_FAIL
921+ # Create a result object and return it
922+ return DiskJobResult({
923+ 'outcome': outcome,
924+ 'return_code': return_code,
925+ 'io_log_filename': record_path,
926+ 'execution_duration': execution_duration
927+ })
928+
929 def _get_dry_run_result(self, job):
930 """
931 Internal method of JobRunner.
932
933=== modified file 'plainbox/plainbox/impl/test_ctrl.py'
934--- plainbox/plainbox/impl/test_ctrl.py 2015-01-09 21:48:46 +0000
935+++ plainbox/plainbox/impl/test_ctrl.py 2015-01-29 23:33:32 +0000
936@@ -34,6 +34,7 @@
937 from plainbox.impl.applogic import PlainBoxConfig
938 from plainbox.impl.ctrl import CheckBoxExecutionController
939 from plainbox.impl.ctrl import CheckBoxSessionStateController
940+from plainbox.impl.ctrl import QmlJobExecutionController
941 from plainbox.impl.ctrl import RootViaPTL1ExecutionController
942 from plainbox.impl.ctrl import RootViaPkexecExecutionController
943 from plainbox.impl.ctrl import RootViaSudoExecutionController
944@@ -1097,3 +1098,110 @@
945 self.job.user = None
946 # Ensure that we get a negative score for this controller
947 self.assertEqual(self.ctrl.get_checkbox_score(self.job), -1)
948+
949+
950+class QmlJobExecutionControllerTests(CheckBoxExecutionControllerTestsMixIn,
951+ TestCase):
952+ """
953+ Tests for QmlJobExecutionController
954+ """
955+ CLS = QmlJobExecutionController
956+
957+ SHELL_OUT_FD = 6
958+ SHELL_IN_FD = 7
959+
960+ def test_job_repr(self):
961+ self.assertEqual(
962+ self.ctrl.gen_job_repr(self.job),
963+ {'id': self.job.id,
964+ 'summary': self.job.tr_summary(),
965+ 'description': self.job.tr_description()})
966+
967+ def test_get_execution_command(self):
968+ """
969+ Tests gluing of commandline arguments when running QML exec. ctrl.
970+ """
971+ self.assertEqual(
972+ self.ctrl.get_execution_command(
973+ self.job, self.job_state, self.config, self.SESSION_DIR,
974+ self.NEST_DIR, self.SHELL_OUT_FD, self.SHELL_IN_FD),
975+ ['qmlscene', '--job', self.job.qml_file, '--fd-out',
976+ self.SHELL_OUT_FD, '--fd-in', self.SHELL_IN_FD,
977+ self.ctrl.QML_SHELL_PATH])
978+
979+ @mock.patch('os.path.isdir')
980+ @mock.patch('os.fdopen')
981+ @mock.patch('os.pipe')
982+ @mock.patch('os.write')
983+ @mock.patch('os.close')
984+ def test_execute_job(self, mock_os_close, mock_os_write, mock_os_pipe,
985+ mock_os_fdopen, mock_os_path_isdir):
986+ """
987+ Test if qml exec. ctrl. correctly runs piping
988+ """
989+ mock_os_pipe.side_effect = [("pipe0_r", "pipe0_w"),
990+ ("pipe1_r", "pipe1_w")]
991+ with mock.patch.object(self.ctrl, 'get_execution_command'), \
992+ mock.patch.object(self.ctrl, 'get_execution_environment'), \
993+ mock.patch.object(self.ctrl, 'configured_filesystem'), \
994+ mock.patch.object(self.ctrl, 'temporary_cwd'), \
995+ mock.patch.object(self.ctrl, 'gen_job_repr', return_value={}):
996+ retval = self.ctrl.execute_job(
997+ self.job, self.job_state, self.config, self.SESSION_DIR,
998+ self.extcmd_popen)
999+ # Ensure that call was invoked with command end environment (passed
1000+ # as keyword argument). Extract the return value of
1001+ # configured_filesystem() as nest_dir so that we can pass it to
1002+ # other calls to get their mocked return values.
1003+ # Urgh! is this doable somehow without all that?
1004+ nest_dir = self.ctrl.configured_filesystem().__enter__()
1005+ cwd_dir = self.ctrl.temporary_cwd().__enter__()
1006+ self.extcmd_popen.call.assert_called_with(
1007+ self.ctrl.get_execution_command(
1008+ self.job, self.config, self.SESSION_DIR, nest_dir),
1009+ env=self.ctrl.get_execution_environment(
1010+ self.job, self.config, self.SESSION_DIR, nest_dir),
1011+ cwd=cwd_dir,
1012+ pass_fds=["pipe0_w", "pipe1_r"])
1013+ # Ensure that execute_job() returns the return value of call()
1014+ self.assertEqual(retval, self.extcmd_popen.call())
1015+ # Ensure that presence of CHECKBOX_DATA directory was checked for
1016+ mock_os_path_isdir.assert_called_with(
1017+ self.ctrl.get_CHECKBOX_DATA(self.SESSION_DIR))
1018+ self.assertEqual(mock_os_pipe.call_count, 2)
1019+ self.assertEqual(mock_os_fdopen.call_count, 2)
1020+ self.assertEqual(mock_os_close.call_count, 6)
1021+
1022+ @mock.patch('os.path.isdir')
1023+ @mock.patch('os.fdopen')
1024+ @mock.patch('os.pipe')
1025+ @mock.patch('os.write')
1026+ @mock.patch('os.close')
1027+ def test_pipes_closed_when_cmd_raises(
1028+ self, mock_os_close, mock_os_write, mock_os_pipe, mock_os_fdopen,
1029+ mock_os_path_isdir):
1030+ """
1031+ Test if all pipes used by execute_job() are properly closed if
1032+ exception is raised during execution of command
1033+ """
1034+ mock_os_pipe.side_effect = [("pipe0_r", "pipe0_w"),
1035+ ("pipe1_r", "pipe1_w")]
1036+ with mock.patch.object(self.ctrl, 'get_execution_command'), \
1037+ mock.patch.object(self.ctrl, 'get_execution_environment'), \
1038+ mock.patch.object(self.ctrl, 'configured_filesystem'), \
1039+ mock.patch.object(self.ctrl, 'temporary_cwd'), \
1040+ mock.patch.object(self.ctrl, 'gen_job_repr', return_value={}), \
1041+ mock.patch.object(self.extcmd_popen, 'call',
1042+ side_effect=Exception('Boom')):
1043+ with self.assertRaises(Exception):
1044+ self.ctrl.execute_job(
1045+ self.job, self.job_state, self.config, self.SESSION_DIR,
1046+ self.extcmd_popen)
1047+ os.close.assert_any_call('pipe0_r')
1048+ os.close.assert_any_call('pipe1_r')
1049+ os.close.assert_any_call('pipe0_w')
1050+ os.close.assert_any_call('pipe1_w')
1051+
1052+ def test_get_checkbox_score_for_qml_job(self):
1053+ self.job.plugin = 'qml'
1054+ self.assertEqual(self.ctrl.get_checkbox_score(self.job), 4)
1055
1056=== modified file 'plainbox/plainbox/impl/unit/job.py'
1057--- plainbox/plainbox/impl/unit/job.py 2015-01-07 14:55:36 +0000
1058+++ plainbox/plainbox/impl/unit/job.py 2015-01-29 23:33:32 +0000
1059@@ -24,6 +24,7 @@
1060
1061 import logging
1062 import re
1063+import os
1064
1065 from plainbox.abc import IJobDefinition
1066 from plainbox.i18n import gettext as _
1067@@ -103,6 +104,7 @@
1068 user_interact = "user-interact"
1069 user_interact_verify = "user-interact-verify"
1070 shell = 'shell'
1071+ qml = 'qml'
1072
1073
1074 class JobDefinition(UnitWithId, JobDefinitionLegacyAPI, IJobDefinition):
1075@@ -304,6 +306,22 @@
1076 'category_id', '2013.com.canonical.plainbox::uncategorised'))
1077
1078 @property
1079+ def qml_file(self):
1080+ """
1081+ path to a QML file that implements tests UI for this job
1082+
1083+ This property exposes a path to QML file that follows the Plainbox QML
1084+ Test Specification. The file will be loaded either in the native test
1085+ shell of the application using plainbox or with a helper, generic
1086+ loader for all command-line applications.
1087+
1088+ To use this property, the plugin type should be set to 'qml'.
1089+ """
1090+ qml_file = self.get_record_value('qml_file')
1091+ if qml_file is not None and self.provider is not None:
1092+ return os.path.join(self.provider.data_dir, qml_file)
1093+
1094+ @property
1095 def estimated_duration(self):
1096 """
1097 estimated duration of this job in seconds.
1098@@ -547,6 +565,7 @@
1099 purpose = 'purpose'
1100 steps = 'steps'
1101 verification = 'verification'
1102+ qml_file = 'qml_file'
1103
1104 field_validators = {
1105 fields.name: [
1106@@ -599,11 +618,11 @@
1107 # All jobs except for manual must have a command
1108 PresentFieldValidator(
1109 message=_("command is mandatory for non-manual jobs"),
1110- onlyif=lambda unit: unit.plugin != 'manual'),
1111+ onlyif=lambda unit: unit.plugin not in ('manual', 'qml')),
1112 # Manual jobs cannot have a command
1113 UselessFieldValidator(
1114- message=_("command on a manual job makes no sense"),
1115- onlyif=lambda unit: unit.plugin == 'manual'),
1116+ message=_("command on a manual or qml job makes no sense"),
1117+ onlyif=lambda unit: unit.plugin in ('manual', 'qml')),
1118 # We don't want to refer to CHECKBOX_SHARE anymore
1119 CorrectFieldValueValidator(
1120 lambda command: "CHECKBOX_SHARE" not in command,
1121@@ -792,5 +811,22 @@
1122 ' non-C locale then set the preserve-locale flag'
1123 ),
1124 onlyif=lambda unit: unit.command),
1125+ ],
1126+ fields.qml_file: [
1127+ UntranslatableFieldValidator,
1128+ TemplateInvariantFieldValidator,
1129+ PresentFieldValidator(
1130+ onlyif=lambda unit: unit.plugin == 'qml'),
1131+ CorrectFieldValueValidator(
1132+ lambda value: value.endswith('.qml'),
1133+ Problem.wrong, Severity.advice,
1134+ message=_('use the .qml extension for all QML files'),
1135+ onlyif=lambda unit: (unit.plugin == 'qml'
1136+ and unit.qml_file)),
1137+ CorrectFieldValueValidator(
1138+ lambda value, unit: os.path.isfile(unit.qml_file),
1139+ message=_('please point to an existing QML file'),
1140+ onlyif=lambda unit: (unit.plugin == 'qml'
1141+ and unit.qml_file)),
1142 ]
1143 }
1144
1145=== modified file 'plainbox/plainbox/impl/unit/test_job.py'
1146--- plainbox/plainbox/impl/unit/test_job.py 2015-01-07 14:53:47 +0000
1147+++ plainbox/plainbox/impl/unit/test_job.py 2015-01-29 23:33:32 +0000
1148@@ -126,6 +126,32 @@
1149 self.assertEqual(job.flags, "flags-value")
1150 self.assertEqual(job.category_id, "category_id-value")
1151
1152+ def test_qml_file_property_none_when_missing_provider(self):
1153+ """
1154+ Ensure that qml_file property is set to None when provider is not set.
1155+ """
1156+ job = JobDefinition({
1157+ 'qml_file': 'qml_file-value'
1158+ }, raw_data={
1159+ 'qml_file': 'qml_file-raw'
1160+ })
1161+ self.assertEqual(job.qml_file, None)
1162+
1163+ def test_qml_file_property(self):
1164+ """
1165+ Ensure that qml_file property is properly constructed
1166+ """
1167+ mock_provider = mock.Mock()
1168+ type(mock_provider).data_dir = mock.PropertyMock(return_value='data')
1169+ job = JobDefinition({
1170+ 'qml_file': 'qml_file-value'
1171+ }, raw_data={
1172+ 'qml_file': 'qml_file-raw'
1173+ }, provider=mock_provider)
1174+ with mock.patch('os.path.join', return_value='path') as mock_join:
1175+ self.assertEqual(job.qml_file, 'path')
1176+ mock_join.assert_called_with('data', 'qml_file-value')
1177+
1178 def test_properties_default_values(self):
1179 """
1180 Ensure that all properties default to None
1181@@ -138,6 +164,7 @@
1182 self.assertEqual(job.shell, 'bash')
1183 self.assertEqual(job.flags, None)
1184 self.assertEqual(job.category_id, '2013.com.canonical.plainbox::uncategorised')
1185+ self.assertEqual(job.qml_file, None)
1186
1187 def test_checksum_smoke(self):
1188 job1 = JobDefinition({'plugin': 'plugin', 'user': 'root'})
1189@@ -291,7 +318,7 @@
1190 'plugin': 'foo'
1191 }, provider=self.provider).check()
1192 message = ("field 'plugin', valid values are: attachment, local,"
1193- " manual, resource, shell, user-interact,"
1194+ " manual, qml, resource, shell, user-interact,"
1195 " user-interact-verify, user-verify")
1196 self.assertIssueFound(issue_list, self.unit_cls.Meta.fields.plugin,
1197 Problem.wrong, Severity.error, message)
1198@@ -329,7 +356,7 @@
1199
1200 def test_command__present__on_non_manual(self):
1201 for plugin in self.unit_cls.plugin.symbols.get_all_symbols():
1202- if plugin == 'manual':
1203+ if plugin in ('manual', 'qml'):
1204 continue
1205 # TODO: switch to subTest() once we depend on python3.4
1206 issue_list = self.unit_cls({
1207@@ -348,6 +375,15 @@
1208 issue_list, self.unit_cls.Meta.fields.command,
1209 Problem.useless, Severity.warning)
1210
1211+ def test_command__useless__on_qml(self):
1212+ issue_list = self.unit_cls({
1213+ 'plugin': 'qml',
1214+ 'command': 'command'
1215+ }, provider=self.provider).check()
1216+ self.assertIssueFound(
1217+ issue_list, self.unit_cls.Meta.fields.command,
1218+ Problem.useless, Severity.warning)
1219+
1220 def test_command__not_using_CHECKBOX_SHARE(self):
1221 issue_list = self.unit_cls({
1222 'command': '$CHECKBOX_SHARE'
1223
1224=== added directory 'plainbox/plainbox/qml_shell'
1225=== added file 'plainbox/plainbox/qml_shell/__init__.py'
1226=== added file 'plainbox/plainbox/qml_shell/qml_shell.py'
1227--- plainbox/plainbox/qml_shell/qml_shell.py 1970-01-01 00:00:00 +0000
1228+++ plainbox/plainbox/qml_shell/qml_shell.py 2015-01-29 23:33:32 +0000
1229@@ -0,0 +1,141 @@
1230+# This file is part of Checkbox.
1231+#
1232+# Copyright 2014 Canonical Ltd.
1233+# Written by:
1234+# Maciej Kisielewski <maciej.kisielewski@canonical.com>
1235+#
1236+# Checkbox is free software: you can redistribute it and/or modify
1237+# it under the terms of the GNU General Public License version 3,
1238+# as published by the Free Software Foundation.
1239+#
1240+# Checkbox is distributed in the hope that it will be useful,
1241+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1242+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1243+# GNU General Public License for more details.
1244+#
1245+# You should have received a copy of the GNU General Public License
1246+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1247+
1248+import json
1249+import os
1250+import subprocess
1251+import sys
1252+
1253+from plainbox import __version__ as plainbox_version
1254+from plainbox.i18n import docstring
1255+from plainbox.i18n import gettext as _
1256+from plainbox.i18n import gettext_noop as N_
1257+from plainbox.impl import get_plainbox_dir
1258+from plainbox.impl.applogic import PlainBoxConfig
1259+from plainbox.impl.clitools import SingleCommandToolMixIn
1260+from plainbox.impl.commands import PlainBoxCommand
1261+from plainbox.impl.commands import PlainBoxToolBase
1262+
1263+
1264+@docstring(
1265+ # TRANSLATORS: please leave various options (both long and short forms),
1266+ # environment variables and paths in their original form. Also keep the
1267+ # special @EPILOG@ string. The first line of the translation is special and
1268+ # is used as the help message. Please keep the pseudo-statement form and
1269+ # don't finish the sentence with a dot. Pay extra attention to whitespace.
1270+ # It must be correctly preserved or the result won't work. In particular
1271+ # the leading whitespace *must* be preserved and *must* have the same
1272+ # length on each line.
1273+ N_("""
1274+ run qml job in standalone shell
1275+
1276+ Runs specified file as it would be a plainbox' qml job.
1277+ Returns 0 if job returned 'pass', 1 if job returned 'fail', or
1278+ other value in case of an error.
1279+
1280+ @EPILOG@
1281+
1282+ General purpose of this command is to make development of qml-native jobs
1283+ faster, by making it easier to test qml file(s) that constitute to job
1284+ without resorting to installation of provider and running plainbox run.
1285+ Typical approach to the development of new qml job would be as follows:
1286+
1287+ - have an idea for a job
1288+
1289+ - create a qml file in Ubuntu-SDK or Your Favourite Editor
1290+
1291+ - hack on the file and iterate using qmlscene (or use plainbox-qml-shell
1292+ immediately if you start with next point)
1293+
1294+ - make it conformant to plainbox qml-native API described in CEP-5
1295+ (calling test-done at the end)
1296+
1297+ - copy qml file over to data dir of a provider and add a job unit to it
1298+
1299+ """))
1300+class QmlShellCommand(PlainBoxCommand):
1301+ def register_parser(self, subparsers):
1302+ parser = subparsers.add_parser(
1303+ "plainbox-qml-shell",
1304+ help=_("run qml-native test in a standalone shell"))
1305+
1306+ self.register_arguments(parser)
1307+
1308+ def register_arguments(self, parser):
1309+ parser.set_defaults(command=self)
1310+ parser.add_argument('QML_FILE', help=_("qml file with job to be run"),
1311+ metavar='QML-FILE')
1312+
1313+ def invoked(self, ns):
1314+ QML_SHELL_PATH = os.path.join(get_plainbox_dir(), 'qml_shell',
1315+ 'qml_shell.qml')
1316+
1317+ test_result_object_prefix = "qml: __test_result_object:"
1318+ test_res = None
1319+ p = subprocess.Popen(['qmlscene', '--job',
1320+ os.path.abspath(ns.QML_FILE), QML_SHELL_PATH],
1321+ stderr=subprocess.PIPE)
1322+ for line in iter(p.stderr.readline, ''):
1323+ line = line.decode(sys.stderr.encoding)
1324+ if not line:
1325+ break
1326+ if line.startswith(test_result_object_prefix):
1327+ obj_json = line[len(test_result_object_prefix):]
1328+ test_res = json.loads(obj_json)
1329+ else:
1330+ print(line)
1331+
1332+ if not test_res:
1333+ return _("Job did not return any result")
1334+
1335+ print(_("Test outcome: {}").format(test_res['outcome']))
1336+ return test_res['outcome'] != "pass"
1337+
1338+
1339+class PlainboxQmlShellTool(SingleCommandToolMixIn, PlainBoxToolBase):
1340+ def get_command(self):
1341+ return QmlShellCommand()
1342+
1343+ @classmethod
1344+ def get_exec_name(cls):
1345+ return "plainbox-qml-shell"
1346+
1347+ @classmethod
1348+ def get_exec_version(cls):
1349+ """
1350+ Get the version reported by this executable
1351+ """
1352+ return cls.format_version_tuple(plainbox_version)
1353+
1354+ @classmethod
1355+ def get_config_cls(cls):
1356+ """
1357+ Get the Config class that is used by this implementation.
1358+
1359+ This can be overridden by subclasses to use a different config
1360+ class that is suitable for the particular application.
1361+ """
1362+ return PlainBoxConfig
1363+
1364+
1365+def main(argv=None):
1366+ raise SystemExit(PlainboxQmlShellTool().main(argv))
1367+
1368+
1369+def get_parser_for_sphinx():
1370+ return PlainboxQmlShellTool().construct_parser()
1371
1372=== added file 'plainbox/plainbox/qml_shell/qml_shell.qml'
1373--- plainbox/plainbox/qml_shell/qml_shell.qml 1970-01-01 00:00:00 +0000
1374+++ plainbox/plainbox/qml_shell/qml_shell.qml 2015-01-29 23:33:32 +0000
1375@@ -0,0 +1,75 @@
1376+/*
1377+ * This file is part of Checkbox
1378+ *
1379+ * Copyright 2014 Canonical Ltd.
1380+ *
1381+ * Authors:
1382+ * - Maciej Kisielewski <maciej.kisielewski@canonical.com>
1383+ *
1384+ * This program is free software; you can redistribute it and/or modify
1385+ * it under the terms of the GNU General Public License as published by
1386+ * the Free Software Foundation; version 3.
1387+ *
1388+ * This program is distributed in the hope that it will be useful,
1389+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1390+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1391+ * GNU General Public License for more details.
1392+ *
1393+ * You should have received a copy of the GNU General Public License
1394+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1395+ */
1396+/*! \brief QML standalone shell
1397+
1398+ This component serves as QML shell that embeds native QML plainbox jobs.
1399+ The job it loads is specified by --job argument passed to qmlscene
1400+ launching this component.
1401+ Job to be run must have testingShell property that be used as a hook to
1402+ 'outside world'. The job should also define testDone signal that gets
1403+ signalled once test is finished. The signal should pass result object
1404+ containing following fields:
1405+ 'outcome' (mandatory): outcome of a test, e.g. 'pass', 'fail', 'undecided'.
1406+ 'suggestedOutcome': if outcome is 'undecided', than this suggestion will be
1407+ presented to the user, letting them decide the final outcome of a test.
1408+*/
1409+import QtQuick 2.0
1410+import Ubuntu.Components 1.1
1411+
1412+MainView {
1413+ id: mainView
1414+ width: units.gu(100)
1415+ height: units.gu(75)
1416+
1417+ // information and functionality passed to qml job component
1418+ property var testingShell: {
1419+ "name": "Standalone testing shell",
1420+ "pageStack": pageStack
1421+ }
1422+
1423+ Arguments {
1424+ id: args
1425+ Argument {
1426+ name: "job"
1427+ help: "QML-native job to run"
1428+ required: true
1429+ valueNames: ["PATH"]
1430+ }
1431+ }
1432+
1433+ Loader {
1434+ id: loader
1435+ anchors.fill: parent
1436+ onLoaded: loader.item.testDone.connect(testDone)
1437+ }
1438+ PageStack {
1439+ id: pageStack
1440+ }
1441+
1442+ function testDone(res) {
1443+ console.error("__test_result_object:"+JSON.stringify(res));
1444+ Qt.quit();
1445+ }
1446+
1447+ Component.onCompleted: {
1448+ loader.setSource(args.values.job, {'testingShell': testingShell});
1449+ }
1450+}
1451
1452=== modified file 'plainbox/plainbox/test_provider_manager.py'
1453--- plainbox/plainbox/test_provider_manager.py 2014-09-18 10:59:17 +0000
1454+++ plainbox/plainbox/test_provider_manager.py 2015-01-29 23:33:32 +0000
1455@@ -322,7 +322,7 @@
1456 test_io.stdout, (
1457 "jobs/broken.txt:1-2: job '2014.com.example::broken', field"
1458 " 'plugin': allowed values are: attachment, local, manual,"
1459- " resource, shell, user-interact, user-interact-verify,"
1460+ " qml, resource, shell, user-interact, user-interact-verify,"
1461 " user-verify\n"))
1462
1463 def test_validate__broken_useless_field(self):
1464
1465=== modified file 'plainbox/setup.py'
1466--- plainbox/setup.py 2015-01-29 01:55:49 +0000
1467+++ plainbox/setup.py 2015-01-29 23:33:32 +0000
1468@@ -81,6 +81,7 @@
1469 'stubbox=plainbox.impl.box:stubbox_main',
1470 ('plainbox-trusted-launcher-1='
1471 'plainbox.impl.secure.launcher1:main'),
1472+ 'plainbox-qml-shell=plainbox.qml_shell.qml_shell:main',
1473 ],
1474 'plainbox.exporter': [
1475 'text=plainbox.impl.exporter.text:TextSessionStateExporter',
1476
1477=== added directory 'providers/2015.com.canonical.certification:qml-tests'
1478=== added file 'providers/2015.com.canonical.certification:qml-tests/.bzrignore'
1479--- providers/2015.com.canonical.certification:qml-tests/.bzrignore 1970-01-01 00:00:00 +0000
1480+++ providers/2015.com.canonical.certification:qml-tests/.bzrignore 2015-01-29 23:33:32 +0000
1481@@ -0,0 +1,2 @@
1482+dist/*.tar.gz
1483+build/mo/*
1484\ No newline at end of file
1485
1486=== added file 'providers/2015.com.canonical.certification:qml-tests/.gitignore'
1487--- providers/2015.com.canonical.certification:qml-tests/.gitignore 1970-01-01 00:00:00 +0000
1488+++ providers/2015.com.canonical.certification:qml-tests/.gitignore 2015-01-29 23:33:32 +0000
1489@@ -0,0 +1,2 @@
1490+dist/*.tar.gz
1491+build/mo/*
1492\ No newline at end of file
1493
1494=== added file 'providers/2015.com.canonical.certification:qml-tests/README.md'
1495--- providers/2015.com.canonical.certification:qml-tests/README.md 1970-01-01 00:00:00 +0000
1496+++ providers/2015.com.canonical.certification:qml-tests/README.md 2015-01-29 23:33:32 +0000
1497@@ -0,0 +1,5 @@
1498+Qml Native Tests
1499+================
1500+
1501+This provider cointains jobs defined using qml.
1502+Its purpose is to demonstrate possible QML usage in system tests.
1503
1504=== added directory 'providers/2015.com.canonical.certification:qml-tests/data'
1505=== added file 'providers/2015.com.canonical.certification:qml-tests/data/camera-test.qml'
1506--- providers/2015.com.canonical.certification:qml-tests/data/camera-test.qml 1970-01-01 00:00:00 +0000
1507+++ providers/2015.com.canonical.certification:qml-tests/data/camera-test.qml 2015-01-29 23:33:32 +0000
1508@@ -0,0 +1,101 @@
1509+/*
1510+ * This file is part of Checkbox.
1511+ *
1512+ * Copyright 2015 Canonical Ltd.
1513+ * Written by:
1514+ * Maciej Kisielewski <maciej.kisielewski@canonical.com>
1515+ *
1516+ * Checkbox is free software: you can redistribute it and/or modify
1517+ * it under the terms of the GNU General Public License version 3,
1518+ * as published by the Free Software Foundation.
1519+ *
1520+ * Checkbox is distributed in the hope that it will be useful,
1521+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1522+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1523+ * GNU General Public License for more details.
1524+ *
1525+ * You should have received a copy of the GNU General Public License
1526+ * along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1527+ */
1528+import QtQuick 2.0
1529+import Ubuntu.Components 1.1
1530+import QtMultimedia 5.0
1531+import QtQuick.Layouts 1.1
1532+Item {
1533+ id: root
1534+ signal testDone(var test)
1535+ property var testingShell
1536+
1537+ Component.onCompleted: testingShell.pageStack.push(introPage)
1538+
1539+ Page {
1540+ id: introPage
1541+ title: i18n.tr("Camera test")
1542+ visible: false
1543+
1544+ ColumnLayout {
1545+ spacing: units.gu(5)
1546+ anchors.fill: parent
1547+ anchors.margins: units.gu(3)
1548+ Label {
1549+ fontSize: "large"
1550+ Layout.fillWidth: true
1551+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
1552+ text: i18n.tr("On the next screen you'll see video feed from \
1553+your camera. Decide whether camera is working correctly, and tap corresponding \
1554+button.")
1555+ font.bold: true
1556+ }
1557+ Button {
1558+ text: i18n.tr("Start the test")
1559+ Layout.fillWidth: true
1560+ onClicked: {
1561+ testingShell.pageStack.push(subpage);
1562+ }
1563+ }
1564+ }
1565+ }
1566+
1567+ Page {
1568+ id: subpage
1569+ visible: false
1570+ title: i18n.tr("Camera test")
1571+ VideoOutput {
1572+ id: viewfinder
1573+ source: cam
1574+ anchors.fill: parent
1575+ orientation: 270
1576+ }
1577+
1578+ Camera {
1579+ id: cam
1580+ }
1581+
1582+ ColumnLayout {
1583+ spacing: units.gu(1)
1584+ anchors {
1585+ fill: parent
1586+ }
1587+ Button {
1588+ width: units.gu(10)
1589+ height: units.gu(5)
1590+ Layout.alignment: Qt.AlignHCenter
1591+ text: i18n.tr("Camera works")
1592+ color: UbuntuColors.green
1593+ onClicked: {
1594+ testDone({'outcome': 'pass'});
1595+ }
1596+ }
1597+ Button {
1598+ width: units.gu(10)
1599+ height: units.gu(5)
1600+ Layout.alignment: Qt.AlignHCenter
1601+ text: i18n.tr("Camera doesn't work")
1602+ color: UbuntuColors.red
1603+ onClicked: {
1604+ testDone({'outcome': 'fail'});
1605+ }
1606+ }
1607+ }
1608+ }
1609+}
1610
1611=== added file 'providers/2015.com.canonical.certification:qml-tests/manage.py'
1612--- providers/2015.com.canonical.certification:qml-tests/manage.py 1970-01-01 00:00:00 +0000
1613+++ providers/2015.com.canonical.certification:qml-tests/manage.py 2015-01-29 23:33:32 +0000
1614@@ -0,0 +1,21 @@
1615+#!/usr/bin/env python3
1616+from plainbox.provider_manager import setup, N_
1617+
1618+# You can inject other stuff here but please don't go overboard.
1619+#
1620+# In particular, if you need comprehensive compilation support to get
1621+# your bin/ populated then please try to discuss that with us in the
1622+# upstream project IRC channel #checkbox on irc.freenode.net.
1623+
1624+# NOTE: one thing that you could do here, that makes a lot of sense,
1625+# is to compute version somehow. This may vary depending on the
1626+# context of your provider. Future version of PlainBox will offer git,
1627+# bzr and mercurial integration using the versiontools library
1628+# (optional)
1629+
1630+setup(
1631+ name='2015.com.canonical.certification:qml-tests',
1632+ version="1.0",
1633+ description=N_("The 2015.com.canonical.certification:qml-tests provider"),
1634+ gettext_domain="2015_com_canonical_certification_qml-tests",
1635+)
1636\ No newline at end of file
1637
1638=== added directory 'providers/2015.com.canonical.certification:qml-tests/units'
1639=== added file 'providers/2015.com.canonical.certification:qml-tests/units/qml-tests.pxu'
1640--- providers/2015.com.canonical.certification:qml-tests/units/qml-tests.pxu 1970-01-01 00:00:00 +0000
1641+++ providers/2015.com.canonical.certification:qml-tests/units/qml-tests.pxu 2015-01-29 23:33:32 +0000
1642@@ -0,0 +1,14 @@
1643+
1644+unit: test plan
1645+_description:
1646+ Test plan containing tests in form of QML programs
1647+id: qml-tests
1648+_name: QML-native tests
1649+estimated_duration: 20
1650+include: .*
1651+
1652+id: qml-camera
1653+plugin: qml
1654+_description: Camera test
1655+qml_file: camera-test.qml
1656+estimated_duration: 20
1657
1658=== modified file 'support/develop-providers'
1659--- support/develop-providers 2014-06-18 15:45:05 +0000
1660+++ support/develop-providers 2015-01-29 23:33:32 +0000
1661@@ -34,7 +34,7 @@
1662 exit 101
1663 fi
1664
1665-for provider in $CHECKBOX_TOP/providers/plainbox-provider-*; do
1666+for provider in $CHECKBOX_TOP/providers/plainbox-provider-* $CHECKBOX_TOP/providers/2015.com.canonical.certification:qml-tests; do
1667 provider=$(basename "$provider")
1668 echo "I: running 'develop' on $provider"
1669 ( cd $CHECKBOX_TOP/providers/$provider && python3 manage.py develop --force --directory=$PROVIDERPATH )

Subscribers

People subscribed via source and target branches