Merge lp:~kissiel/checkbox/qml-native into lp:checkbox
- qml-native
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Zygmunt Krynicki (community) | Approve | ||
Review via email: mp+247960@code.launchpad.net |
Commit message
Description of the change
This MR includes same changes as https:/
Daniel Manrique (roadmr) wrote : | # |
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+
[precise] provisioning container
[precise] (timing) 36.91user 10.70system 1:19.85elapsed 59%CPU (0avgtext+0avgdata 51680maxresident)k
[precise] (timing) 0inputs+
[precise-testing] Starting tests...
Found a test script: ./checkbox-
[precise-testing] container-
[precise-testing] (timing) 33.14user 2.50system 0:36.09elapsed 98%CPU (0avgtext+0avgdata 116468maxresident)k
[precise-testing] (timing) 0inputs+4216outputs (0major+
Found a test script: ./checkbox-
[precise-testing] container-
[precise-testing] (timing) 0.56user 0.11system 0:01.43elapsed 46%CPU (0avgtext+0avgdata 39924maxresident)k
[precise-testing] (timing) 0inputs+3480outputs (0major+
Found a test script: ./checkbox-
[precise-testing] container-
[precise-testing] (timing) 16.92user 0.16system 0:17.20elapsed 99%CPU (0avgtext+0avgdata 83492maxresident)k
[precise-testing] (timing) 0inputs+1032outputs (0major+
Found a test script: ./checkbox-
[precise-testing] container-
[precise-testing] (timing) 0.00user 0.00system 0:00.01elapsed 42%CPU (0avgtext+0avgdata 2020maxresident)k
[precise-testing] (timing) 0inputs+8outputs (0major+
Found a test script: ./plainbox/
[precise-testing] container-
[precise-testing] (timing) 0.14user 0.05system 0:00.20elapsed 92%CPU (0avgtext+0avgdata 13520maxresident)k
[precise-testing] (timing) 0inputs+176outputs (0major+
Found a test script: ./plainbox/
[precise-testing] 001-container-
[precise-testing] (timing) 0.15user 0.03system 0:00.19elapsed 93%CPU (0avgtext+0avgdata 10520maxresident)k
[precise-testing] (timing) 0inputs+88outputs (0major+
Found a test script: ./plainbox/
[precise-testing] container-
[precise-testing] (timing) 10.64user 0.72system 0:11.57elapsed 98%CPU (0avgtext+0avgdata 66500maxresident)k
[precise-testing] (timing) 0inputs+2776outputs (0major+
Found a test script: ./plainbox/
[precise-testing] container-
[precise-testing] stdout: http://...
- 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>
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
Preview Diff
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 ) |
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 :)