Merge lp:~rhuddie/ubuntu-push/push-autopilot-tests into lp:ubuntu-push/automatic
- push-autopilot-tests
- Merge into automatic
Proposed by
John Lenton
Status: | Merged |
---|---|
Approved by: | John Lenton |
Approved revision: | 141 |
Merged at revision: | 237 |
Proposed branch: | lp:~rhuddie/ubuntu-push/push-autopilot-tests |
Merge into: | lp:ubuntu-push/automatic |
Diff against target: |
985 lines (+930/-0) 10 files modified
tests/autopilot/push_notifications/README (+74/-0) tests/autopilot/push_notifications/__init__.py (+20/-0) tests/autopilot/push_notifications/config/__init__.py (+39/-0) tests/autopilot/push_notifications/config/push.conf (+5/-0) tests/autopilot/push_notifications/config/testing.cert (+10/-0) tests/autopilot/push_notifications/data.py (+98/-0) tests/autopilot/push_notifications/helpers/__init__.py (+18/-0) tests/autopilot/push_notifications/helpers/push_notifications_helper.py (+280/-0) tests/autopilot/push_notifications/tests/__init__.py (+245/-0) tests/autopilot/push_notifications/tests/test_broadcast_notifications.py (+141/-0) |
To merge this branch: | bzr merge lp:~rhuddie/ubuntu-push/push-autopilot-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John Lenton (community) | Approve | ||
Review via email: mp+226274@code.launchpad.net |
Commit message
autopilot test framework and basic coverage of broadcast notifications
Description of the change
Work around for excessive list of reviewers on https:/
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'tests' |
2 | === added directory 'tests/autopilot' |
3 | === added directory 'tests/autopilot/push_notifications' |
4 | === added file 'tests/autopilot/push_notifications/README' |
5 | --- tests/autopilot/push_notifications/README 1970-01-01 00:00:00 +0000 |
6 | +++ tests/autopilot/push_notifications/README 2014-07-10 10:50:28 +0000 |
7 | @@ -0,0 +1,74 @@ |
8 | +================== |
9 | +README |
10 | +================== |
11 | + |
12 | +To run ubuntu-push autopilot tests you need to have a push server available. This can be running locally using loopback (127.0.0.1) or remotely on the same network. |
13 | + |
14 | +---------------------------------- |
15 | +To configure and build the server: |
16 | +---------------------------------- |
17 | + |
18 | +1) export GOPATH=${PWD}/push |
19 | +2) mkdir -p push/src/launchpad.net |
20 | +3) cd push/src/launchpad.net |
21 | +4) bzr branch lp:ubuntu-push |
22 | +5) Edit ubuntu-push/sampleconfigs/dev.json with the correct server IP address and ports. |
23 | + E.g. for a server IP address 192.168.1.2 use: |
24 | + "addr": "192.168.1.2:9090", |
25 | + "http_addr": "192.168.1.2:8080", |
26 | +6) cd ubuntu-push |
27 | +7) ensure all dependencies are installed: sudo apt-get build-dep ubuntu-push |
28 | +8) install additional cm tools: sudo apt-get install git mercurial |
29 | +9) make bootstrap |
30 | +10) make run-server-dev |
31 | + Following output should be observed: |
32 | + INFO listening for http on 192.168.1.2:8080 |
33 | + INFO listening for devices on 192.168.1.2:9090 |
34 | + |
35 | +------------------------ |
36 | +To configure the client: |
37 | +------------------------ |
38 | + |
39 | +Install depenendencies: |
40 | + |
41 | +1) sudo apt-get install unity8-autopilot unity-click-scope |
42 | + Note: unity-click-scope is required for unity8-autopilot tests and is currently required for running tests on emulator. |
43 | +2) bzr branch lp:ubuntu-push |
44 | +3) Edit ip address and ports to match environment: ubuntu-push/tests/autopilot/push_notifications/config/push.conf: |
45 | + [config] |
46 | + addr = 192.168.1.2 |
47 | + listener_port = 8080 |
48 | + device_port = 9090 |
49 | + cert_pem_file = testing.cert |
50 | +4) initctl stop unity8 |
51 | +5) cd ubuntu-push/tests/autopilot |
52 | +6) autopilot3 list push_notifications |
53 | +7) autopilot3 run push_notifications |
54 | +8) To run a specific test case use the test case identifier from the list command: |
55 | + - e.g. autopilot3 run push_notifications.tests.test_broadcast_notifications.TestPushClientBroadcast.test_broadcast_push_notification |
56 | + |
57 | +---------------- |
58 | +Troubleshooting: |
59 | +---------------- |
60 | + |
61 | +1) Ping from client to server to ensure connectivity is correct |
62 | +2) Delete ~/.local/share/ubuntu-push-client/levels.db if no notifications are being displayed: |
63 | + rm ~/.local/share-ubuntu-push-client/levels.db |
64 | +3) Check client log file at ~/.cache/upstart/ubuntu-push-client.log: |
65 | + tail -f --line=30 ~/.cache/upstart/ubuntu-push-client.log |
66 | +4) To send a notification manually: |
67 | + echo '{"channel":"system", "data": {"ubuntu-touch/utopic-proposed/mako": [94, ""]}, "expire_on": "2015-12-19T16:39:57-08:00"}' | POST -c application/json http://192.168.1.2:8080/broadcast |
68 | + Response should be: |
69 | + {"ok":true} |
70 | + Note that: |
71 | + - The channel and device names must match the client. |
72 | + - The build number must be greater than current installed version in order to trigger an update message. |
73 | + - The expiration time must be in the future. |
74 | +5) Ensure unity8-autopilot is installed |
75 | +6) Ensure unity8 is not running before executing the tests: |
76 | + - initctl stop unity8 |
77 | +7) Unity8 has a 2 minute timeout period, so stopping and starting it can take up to this long. |
78 | +8) If device/emulator is unresponsive then reboot and stop unity8 before re-running the tests (initctl stop unity8). |
79 | +9) To get additional autopilot logging use -v option: |
80 | + - autopilot3 run -v push_notifications |
81 | + |
82 | |
83 | === added file 'tests/autopilot/push_notifications/__init__.py' |
84 | --- tests/autopilot/push_notifications/__init__.py 1970-01-01 00:00:00 +0000 |
85 | +++ tests/autopilot/push_notifications/__init__.py 2014-07-10 10:50:28 +0000 |
86 | @@ -0,0 +1,20 @@ |
87 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
88 | +# |
89 | +# Push Notifications Autopilot Test Suite |
90 | +# Copyright (C) 2014 Canonical |
91 | +# |
92 | +# This program is free software: you can redistribute it and/or modify |
93 | +# it under the terms of the GNU General Public License as published by |
94 | +# the Free Software Foundation, either version 3 of the License, or |
95 | +# (at your option) any later version. |
96 | +# |
97 | +# This program is distributed in the hope that it will be useful, |
98 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
99 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
100 | +# GNU General Public License for more details. |
101 | +# |
102 | +# You should have received a copy of the GNU General Public License |
103 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
104 | +# |
105 | + |
106 | +"""push-notifications autopilot tests.""" |
107 | |
108 | === added directory 'tests/autopilot/push_notifications/config' |
109 | === added file 'tests/autopilot/push_notifications/config/__init__.py' |
110 | --- tests/autopilot/push_notifications/config/__init__.py 1970-01-01 00:00:00 +0000 |
111 | +++ tests/autopilot/push_notifications/config/__init__.py 2014-07-10 10:50:28 +0000 |
112 | @@ -0,0 +1,39 @@ |
113 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
114 | +# |
115 | +# Push Notifications Autopilot Test Suite |
116 | +# Copyright (C) 2014 Canonical |
117 | +# |
118 | +# This program is free software: you can redistribute it and/or modify |
119 | +# it under the terms of the GNU General Public License as published by |
120 | +# the Free Software Foundation, either version 3 of the License, or |
121 | +# (at your option) any later version. |
122 | +# |
123 | +# This program is distributed in the hope that it will be useful, |
124 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
125 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
126 | +# GNU General Public License for more details. |
127 | +# |
128 | +# You should have received a copy of the GNU General Public License |
129 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
130 | +# |
131 | + |
132 | + |
133 | +import os |
134 | + |
135 | +CONFIG_FILE = 'push.conf' |
136 | + |
137 | + |
138 | +def get_config_file(): |
139 | + """ |
140 | + Return the path for the config file |
141 | + """ |
142 | + config_dir = os.path.dirname(__file__) |
143 | + return os.path.join(config_dir, CONFIG_FILE) |
144 | + |
145 | + |
146 | +def get_cert_file(cert_file_name): |
147 | + """ |
148 | + Return the path for the testing certificate file |
149 | + """ |
150 | + config_dir = os.path.dirname(__file__) |
151 | + return os.path.join(config_dir, cert_file_name) |
152 | |
153 | === added file 'tests/autopilot/push_notifications/config/push.conf' |
154 | --- tests/autopilot/push_notifications/config/push.conf 1970-01-01 00:00:00 +0000 |
155 | +++ tests/autopilot/push_notifications/config/push.conf 2014-07-10 10:50:28 +0000 |
156 | @@ -0,0 +1,5 @@ |
157 | +[config] |
158 | +addr = 192.168.1.3 |
159 | +listener_port = 8080 |
160 | +device_port = 9090 |
161 | +cert_pem_file = testing.cert |
162 | |
163 | === added file 'tests/autopilot/push_notifications/config/testing.cert' |
164 | --- tests/autopilot/push_notifications/config/testing.cert 1970-01-01 00:00:00 +0000 |
165 | +++ tests/autopilot/push_notifications/config/testing.cert 2014-07-10 10:50:28 +0000 |
166 | @@ -0,0 +1,10 @@ |
167 | +-----BEGIN CERTIFICATE----- |
168 | +MIIBYzCCAQ+gAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD |
169 | +bzAeFw0xMzEyMTkyMDU1NDNaFw0yMzEyMTcyMDU1NDNaMBIxEDAOBgNVBAoTB0Fj |
170 | +bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAPw+niki17X2qALE2A2AzE1q5dvK |
171 | +9CI4OduRtT9IgbFLC6psqAT21NA+QbY17nWSSpyP65zkMkwKXrbDzstwLPkCAwEA |
172 | +AaNUMFIwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud |
173 | +EwEB/wQFMAMBAf8wGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAsGCSqGSIb3 |
174 | +DQEBBQNBAFqiVI+Km2XPSO+pxITaPvhmuzg+XG3l1+2di3gL+HlDobocjBqRctRU |
175 | +YySO32W07acjGJmCHUKpCJuq9X8hpmk= |
176 | +-----END CERTIFICATE----- |
177 | |
178 | === added file 'tests/autopilot/push_notifications/data.py' |
179 | --- tests/autopilot/push_notifications/data.py 1970-01-01 00:00:00 +0000 |
180 | +++ tests/autopilot/push_notifications/data.py 2014-07-10 10:50:28 +0000 |
181 | @@ -0,0 +1,98 @@ |
182 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
183 | +# |
184 | +# Push Notifications Autopilot Test Suite |
185 | +# Copyright (C) 2014 Canonical |
186 | +# |
187 | +# This program is free software: you can redistribute it and/or modify |
188 | +# it under the terms of the GNU General Public License as published by |
189 | +# the Free Software Foundation, either version 3 of the License, or |
190 | +# (at your option) any later version. |
191 | +# |
192 | +# This program is distributed in the hope that it will be useful, |
193 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
194 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
195 | +# GNU General Public License for more details. |
196 | +# |
197 | +# You should have received a copy of the GNU General Public License |
198 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
199 | +# |
200 | + |
201 | + |
202 | +"""Push-Notifications autopilot data structure classes""" |
203 | + |
204 | + |
205 | +class PushNotificationMessage: |
206 | + """ |
207 | + Class to hold all the details required for a |
208 | + push notification message |
209 | + """ |
210 | + |
211 | + def __init__(self, channel='system', data=None, expire_after=None): |
212 | + """ |
213 | + Constructor |
214 | + :param channel: Name of channel |
215 | + :param data: Data value |
216 | + :param expire_after: expiration time |
217 | + """ |
218 | + self.channel = channel |
219 | + self.data = data |
220 | + self.expire_after = expire_after |
221 | + |
222 | + def to_json(self): |
223 | + """ |
224 | + Return JSON representation of message |
225 | + :return: JSON representation of message |
226 | + """ |
227 | + json_str = '{{"channel":"{0}", "data":{{{1}}}, "expire_on":"{2}"}}' |
228 | + return json_str.format(self.channel, self.data, self.expire_after) |
229 | + |
230 | + |
231 | +class DeviceNotificationData: |
232 | + """ |
233 | + Class to represent device's data used for sending notification, including: |
234 | + - Device software channel |
235 | + - Device build number |
236 | + - Device model |
237 | + - Device last update |
238 | + - Data for the notification |
239 | + """ |
240 | + |
241 | + def __init__(self, channel=None, device=None, build_number=None, |
242 | + last_update=None, version=None, data=None): |
243 | + """ |
244 | + Constructor |
245 | + :param channel: Name of channel |
246 | + :param device: Name of device |
247 | + :param build_number: Build number |
248 | + :param last_update: Last update time |
249 | + :param version: Build version |
250 | + :param data: Device specific data |
251 | + """ |
252 | + self.channel = channel |
253 | + self.build_number = build_number |
254 | + self.device = device |
255 | + self.last_update = last_update |
256 | + self.version = version |
257 | + self.data = data |
258 | + |
259 | + def inc_build_number(self): |
260 | + """ |
261 | + Increment build number |
262 | + """ |
263 | + self.build_number = str(int(self.build_number) + 1) |
264 | + |
265 | + def dec_build_number(self): |
266 | + """ |
267 | + Decrement build number |
268 | + """ |
269 | + self.build_number = str(int(self.build_number) - 1) |
270 | + |
271 | + def to_json(self): |
272 | + """ |
273 | + Return json representation of info based: |
274 | + "IMAGE-CHANNEL/DEVICE-MODEL": [BUILD-NUMBER, CHANNEL-ALIAS]" |
275 | + :return: JSON representation of device data |
276 | + """ |
277 | + json_str = '"{0}/{1}": [{2}, "{3}"]' |
278 | + return json_str.format(self.channel, self.device, self.build_number, |
279 | + self.data) |
280 | |
281 | === added directory 'tests/autopilot/push_notifications/helpers' |
282 | === added file 'tests/autopilot/push_notifications/helpers/__init__.py' |
283 | --- tests/autopilot/push_notifications/helpers/__init__.py 1970-01-01 00:00:00 +0000 |
284 | +++ tests/autopilot/push_notifications/helpers/__init__.py 2014-07-10 10:50:28 +0000 |
285 | @@ -0,0 +1,18 @@ |
286 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
287 | +# |
288 | +# Push Notifications Autopilot Test Suite |
289 | +# Copyright (C) 2014 Canonical |
290 | +# |
291 | +# This program is free software: you can redistribute it and/or modify |
292 | +# it under the terms of the GNU General Public License as published by |
293 | +# the Free Software Foundation, either version 3 of the License, or |
294 | +# (at your option) any later version. |
295 | +# |
296 | +# This program is distributed in the hope that it will be useful, |
297 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
298 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
299 | +# GNU General Public License for more details. |
300 | +# |
301 | +# You should have received a copy of the GNU General Public License |
302 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
303 | +# |
304 | |
305 | === added file 'tests/autopilot/push_notifications/helpers/push_notifications_helper.py' |
306 | --- tests/autopilot/push_notifications/helpers/push_notifications_helper.py 1970-01-01 00:00:00 +0000 |
307 | +++ tests/autopilot/push_notifications/helpers/push_notifications_helper.py 2014-07-10 10:50:28 +0000 |
308 | @@ -0,0 +1,280 @@ |
309 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
310 | +# |
311 | +# Push Notifications Autopilot Test Suite |
312 | +# Copyright (C) 2014 Canonical |
313 | +# |
314 | +# This program is free software: you can redistribute it and/or modify |
315 | +# it under the terms of the GNU General Public License as published by |
316 | +# the Free Software Foundation, either version 3 of the License, or |
317 | +# (at your option) any later version. |
318 | +# |
319 | +# This program is distributed in the hope that it will be useful, |
320 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
321 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
322 | +# GNU General Public License for more details. |
323 | +# |
324 | +# You should have received a copy of the GNU General Public License |
325 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
326 | +# |
327 | + |
328 | +import configparser |
329 | +import datetime |
330 | +import http.client as http |
331 | +import json |
332 | +import os |
333 | +import subprocess |
334 | +import systemimage.config as sys_info |
335 | + |
336 | +from push_notifications import config as push_config |
337 | +from push_notifications.data import ( |
338 | + PushNotificationMessage, |
339 | + DeviceNotificationData |
340 | +) |
341 | + |
342 | + |
343 | +class PushClientConfig: |
344 | + """ |
345 | + Container class to read and hold all required server config |
346 | + - Server listener address |
347 | + - Server device address |
348 | + - Certificate PEM file path |
349 | + """ |
350 | + |
351 | + @staticmethod |
352 | + def read_config(config_file_path): |
353 | + """ |
354 | + Return PushClientConfig object containing all test config parameters |
355 | + which have been read from specified config file. |
356 | + :param config_file_path: path to required config file |
357 | + :return: PushClientConfig object containing all config parameters |
358 | + """ |
359 | + KEY_ADDR = 'addr' |
360 | + KEY_LISTENER_PORT = 'listener_port' |
361 | + KEY_DEVICE_PORT = 'device_port' |
362 | + KEY_CONFIG = 'config' |
363 | + KEY_CERT_PEM_FILE = 'cert_pem_file' |
364 | + |
365 | + config = PushClientConfig() |
366 | + parser = configparser.ConfigParser() |
367 | + parser.read(config_file_path) |
368 | + server_addr = parser[KEY_CONFIG][KEY_ADDR] |
369 | + device_port = parser[KEY_CONFIG][KEY_DEVICE_PORT] |
370 | + listener_port = parser[KEY_CONFIG][KEY_LISTENER_PORT] |
371 | + addr_fmt = '{0}:{1}' |
372 | + config.server_listener_addr = addr_fmt.format( |
373 | + server_addr, listener_port) |
374 | + config.server_device_addr = addr_fmt.format(server_addr, device_port) |
375 | + config.cert_pem_file = push_config.get_cert_file( |
376 | + parser[KEY_CONFIG][KEY_CERT_PEM_FILE]) |
377 | + return config |
378 | + |
379 | + |
380 | +class PushClientController: |
381 | + """ |
382 | + Class used to reconfigure and re-start ubuntu-push-client for testing |
383 | + """ |
384 | + |
385 | + PUSH_CLIENT_DEFAULT_CONFIG_FILE = '/etc/xdg/ubuntu-push-client/config.json' |
386 | + PUSH_CLIENT_CONFIG_FILE = '~/.config/ubuntu-push-client/config.json' |
387 | + |
388 | + def restart_push_client_using_config(self, client_config=None): |
389 | + """ |
390 | + Restart the push client using the config provided |
391 | + If the config is none then revert to default client behaviour |
392 | + :param client_config: PushClientConfig object containing |
393 | + required config |
394 | + """ |
395 | + if client_config is None: |
396 | + # just delete the local custom config file |
397 | + # client will then just use the original config |
398 | + abs_config_file = self.get_abs_local_config_file_path() |
399 | + if os.path.exists(abs_config_file): |
400 | + os.remove(abs_config_file) |
401 | + else: |
402 | + # write the config to local config file |
403 | + self.write_client_test_config(client_config) |
404 | + |
405 | + # Now re-start the client |
406 | + self.restart_push_client() |
407 | + |
408 | + def write_client_test_config(self, client_config): |
409 | + """ |
410 | + Write the test server address and certificate path |
411 | + to the client config file |
412 | + :param client_config: PushClientConfig object containing |
413 | + required config |
414 | + """ |
415 | + # read the original push client config file |
416 | + with open(self.PUSH_CLIENT_DEFAULT_CONFIG_FILE) as config_file: |
417 | + config = json.load(config_file) |
418 | + # change server address |
419 | + config['addr'] = client_config.server_device_addr |
420 | + # add certificate file path |
421 | + config['cert_pem_file'] = client_config.cert_pem_file |
422 | + # write the config json out to the ~.local address |
423 | + # creating the directory if it doesn't already exist |
424 | + abs_config_file = self.get_abs_local_config_file_path() |
425 | + config_dir = os.path.dirname(abs_config_file) |
426 | + if not os.path.exists(config_dir): |
427 | + os.makedirs(config_dir) |
428 | + with open(abs_config_file, 'w+') as outfile: |
429 | + json.dump(config, outfile, indent=4) |
430 | + outfile.close() |
431 | + |
432 | + def get_abs_local_config_file_path(self): |
433 | + """ |
434 | + Return absolute path of ~.local config file |
435 | + """ |
436 | + return os.path.expanduser(self.PUSH_CLIENT_CONFIG_FILE) |
437 | + |
438 | + def control_client(self, command): |
439 | + """ |
440 | + start/stop/restart the ubuntu-push-client using initctl |
441 | + """ |
442 | + subprocess.call( |
443 | + ['/sbin/initctl', command, 'ubuntu-push-client'], |
444 | + stdout=subprocess.DEVNULL) |
445 | + |
446 | + def stop_push_client(self): |
447 | + """ |
448 | + Stop the push client |
449 | + """ |
450 | + self.control_client('stop') |
451 | + |
452 | + def start_push_client(self): |
453 | + """ |
454 | + Start the push client |
455 | + """ |
456 | + self.control_client('start') |
457 | + |
458 | + def restart_push_client(self): |
459 | + """ |
460 | + Restart the push client |
461 | + """ |
462 | + self.stop_push_client() |
463 | + self.start_push_client() |
464 | + |
465 | + |
466 | +class PushNotificationHelper: |
467 | + """ |
468 | + Utility class to create and send push notification messages |
469 | + """ |
470 | + |
471 | + DEFAULT_BROADCAST_URL = '/broadcast' |
472 | + |
473 | + def get_device_info(self): |
474 | + """ |
475 | + Discover the device's model and build info |
476 | + - device name e.g. mako |
477 | + - channel name e.g. ubuntu-touch/utopic-proposed |
478 | + - build_number e.g. 101 |
479 | + :return: DeviceNotificationData object containing device info |
480 | + """ |
481 | + # channel info needs to be read from file |
482 | + parser = configparser.ConfigParser() |
483 | + channel_config_file = '/etc/system-image/channel.ini' |
484 | + parser.read(channel_config_file) |
485 | + channel = parser['service']['channel'] |
486 | + return DeviceNotificationData( |
487 | + device=sys_info.config.device, |
488 | + channel=channel, |
489 | + build_number=sys_info.config.build_number) |
490 | + |
491 | + def send_push_broadcast_notification(self, msg_json, server_addr, |
492 | + url=DEFAULT_BROADCAST_URL): |
493 | + """ |
494 | + Send the specified push message to the server broadcast url |
495 | + using an HTTP POST command |
496 | + :param msg_json: JSON representation of message to send |
497 | + :param url: destination server url to send message to |
498 | + """ |
499 | + headers = {'Content-type': 'application/json'} |
500 | + conn = http.HTTPConnection(server_addr) |
501 | + conn.request( |
502 | + 'POST', |
503 | + url, |
504 | + headers=headers, |
505 | + body=msg_json) |
506 | + return conn.getresponse() |
507 | + |
508 | + def create_push_message(self, channel='system', data=None, |
509 | + expire_after=None): |
510 | + """ |
511 | + Return a new push message |
512 | + If no expiry time is given, a future date will be assigned |
513 | + :param channel: name of the channel |
514 | + :param data: data value of the message |
515 | + :param expire_after: expiry time for message |
516 | + :return: PushNotificationMessage object containing specified parameters |
517 | + """ |
518 | + if expire_after is None: |
519 | + expire_after = self.get_future_iso_time() |
520 | + return PushNotificationMessage( |
521 | + channel=channel, |
522 | + data=data, |
523 | + expire_after=expire_after) |
524 | + |
525 | + def get_past_iso_time(self): |
526 | + """ |
527 | + Return time 1 year in past in ISO format |
528 | + """ |
529 | + return self.get_iso_time(year_offset=-1) |
530 | + |
531 | + def get_near_past_iso_time(self): |
532 | + """ |
533 | + Return time 5 seconds in past in ISO format |
534 | + """ |
535 | + return self.get_iso_time(sec_offset=-5) |
536 | + |
537 | + def get_near_future_iso_time(self): |
538 | + """ |
539 | + Return time 5 seconds in future in ISO format |
540 | + """ |
541 | + return self.get_iso_time(sec_offset=5) |
542 | + |
543 | + def get_future_iso_time(self): |
544 | + """ |
545 | + Return time 1 year in future in ISO format |
546 | + """ |
547 | + return self.get_iso_time(year_offset=1) |
548 | + |
549 | + def get_current_iso_time(self): |
550 | + """ |
551 | + Return current time in ISO format |
552 | + """ |
553 | + return self.get_iso_time() |
554 | + |
555 | + def get_iso_time(self, year_offset=0, month_offset=0, day_offset=0, |
556 | + hour_offset=0, min_offset=0, sec_offset=0, |
557 | + tz_hour_offset=0, tz_min_offset=0): |
558 | + """ |
559 | + Return an ISO8601 format date-time string, including time-zone |
560 | + offset: YYYY-MM-DDTHH:MM:SS-HH:MM |
561 | + :param year_offset: number of years to offset |
562 | + :param month_offset: number of months to offset |
563 | + :param day_offset: number of days to offset |
564 | + :param hour_offset: number of hours to offset |
565 | + :param min_offset: number of minutes to offset |
566 | + :param sec_offset: number of seconds to offset |
567 | + :param tz_hour_offset: number of hours to offset time zone |
568 | + :param tz_min_offset: number of minutes to offset time zone |
569 | + :return: string representation of required time in ISO8601 format |
570 | + """ |
571 | + # calulate target time based on current time and format it |
572 | + now = datetime.datetime.now() |
573 | + target_time = datetime.datetime( |
574 | + year=now.year + year_offset, |
575 | + month=now.month + month_offset, |
576 | + day=now.day + day_offset, |
577 | + hour=now.hour + hour_offset, |
578 | + minute=now.minute + min_offset, |
579 | + second=now.second + sec_offset) |
580 | + target_time_fmt = target_time.strftime('%Y-%m-%dT%H:%M:%S') |
581 | + # format time zone offset |
582 | + tz = datetime.time( |
583 | + hour=tz_hour_offset, |
584 | + minute=tz_min_offset) |
585 | + tz_fmt = tz.strftime('%H:%M') |
586 | + # combine target time and time zone offset |
587 | + iso_time = '{0}-{1}'.format(target_time_fmt, tz_fmt) |
588 | + return iso_time |
589 | |
590 | === added directory 'tests/autopilot/push_notifications/tests' |
591 | === added file 'tests/autopilot/push_notifications/tests/__init__.py' |
592 | --- tests/autopilot/push_notifications/tests/__init__.py 1970-01-01 00:00:00 +0000 |
593 | +++ tests/autopilot/push_notifications/tests/__init__.py 2014-07-10 10:50:28 +0000 |
594 | @@ -0,0 +1,245 @@ |
595 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
596 | +# |
597 | +# Push Notifications Autopilot Test Suite |
598 | +# Copyright (C) 2014 Canonical |
599 | +# |
600 | +# This program is free software: you can redistribute it and/or modify |
601 | +# it under the terms of the GNU General Public License as published by |
602 | +# the Free Software Foundation, either version 3 of the License, or |
603 | +# (at your option) any later version. |
604 | +# |
605 | +# This program is distributed in the hope that it will be useful, |
606 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
607 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
608 | +# GNU General Public License for more details. |
609 | +# |
610 | +# You should have received a copy of the GNU General Public License |
611 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
612 | +# |
613 | + |
614 | +"""push-notifications autopilot tests.""" |
615 | + |
616 | +import copy |
617 | + |
618 | +import evdev |
619 | + |
620 | +from autopilot.introspection import dbus |
621 | +from autopilot.matchers import Eventually |
622 | +from push_notifications import config as push_config |
623 | +import push_notifications.helpers.push_notifications_helper as push_helper |
624 | +from testtools.matchers import Equals, NotEquals |
625 | +from unity8.shell.tests import UnityTestCase |
626 | + |
627 | + |
628 | +class PushNotificationTestBase(UnityTestCase): |
629 | + """ |
630 | + Base class for push notification test cases |
631 | + """ |
632 | + |
633 | + @classmethod |
634 | + def setUpClass(cls): |
635 | + """ |
636 | + Executed once before all the tests run |
637 | + Restart the push client using the test config |
638 | + """ |
639 | + test_config = push_helper.PushClientConfig.read_config( |
640 | + push_config.get_config_file()) |
641 | + push_client_controller = push_helper.PushClientController() |
642 | + push_client_controller.restart_push_client_using_config(test_config) |
643 | + |
644 | + @classmethod |
645 | + def tearDownClass(cls): |
646 | + """ |
647 | + Executed once after all tests have completed |
648 | + Reset the push client to use the device's original config |
649 | + """ |
650 | + push_client_controller = push_helper.PushClientController() |
651 | + push_client_controller.restart_push_client_using_config(None) |
652 | + |
653 | + def setUp(self): |
654 | + """ |
655 | + Setup phase executed before each test |
656 | + """ |
657 | + # setup |
658 | + super(PushNotificationTestBase, self).setUp() |
659 | + |
660 | + # read and store the test config data |
661 | + self.test_config = push_helper.PushClientConfig.read_config( |
662 | + push_config.get_config_file()) |
663 | + # create a push helper object which will do all the message sending |
664 | + self.push_helper = push_helper.PushNotificationHelper() |
665 | + # create a push controller object |
666 | + self.push_client_controller = push_helper.PushClientController() |
667 | + # get and store device and build info |
668 | + self.device_info = self.push_helper.get_device_info() |
669 | + # start unity8 |
670 | + self._qml_mock_enabled = False |
671 | + self._data_dirs_mock_enabled = False |
672 | + self.unity = self.launch_unity() |
673 | + # dismiss any outstanding dialog |
674 | + self.dismiss_outstanding_dialog() |
675 | + |
676 | + def create_device_info_copy(self): |
677 | + """ |
678 | + Return a copy of the device's model and build info |
679 | + :return: DeviceNotificationData object containging device's model |
680 | + and build info |
681 | + """ |
682 | + return copy.deepcopy(self.device_info) |
683 | + |
684 | + def press_power_button(self): |
685 | + """ |
686 | + Simulate a power key press event |
687 | + """ |
688 | + uinput = evdev.UInput(name='push-autopilot-power-button', |
689 | + devnode='/dev/autopilot-uinput') |
690 | + # One press and release to turn screen off (locking unity) |
691 | + uinput.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_POWER, 1) |
692 | + uinput.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_POWER, 0) |
693 | + uinput.syn() |
694 | + |
695 | + def unlock_greeter(self): |
696 | + """ |
697 | + Unlock the greeter to display home screen |
698 | + """ |
699 | + self.main_window.get_greeter().swipe() |
700 | + |
701 | + def validate_response(self, response, expected_status_code=200): |
702 | + """ |
703 | + Validate the received response status code against expected code |
704 | + :param response: response to validate |
705 | + :param expected_status_code: value of expected http status code |
706 | + """ |
707 | + self.assertThat(response.status, Equals(expected_status_code)) |
708 | + |
709 | + def assert_notification_dialog(self, notification, summary=None, |
710 | + body=None, icon=True, secondary_icon=False, |
711 | + opacity=None): |
712 | + """ |
713 | + Assert that the properties of the notification are as |
714 | + expected |
715 | + :param notification: notification object to validate |
716 | + :param summary: expected notification summary value |
717 | + :param body: expected notification body value |
718 | + :param icon: expected icon status |
719 | + :param secondary_icon: expected secondary icon status |
720 | + :param opacity: expected opacity value |
721 | + """ |
722 | + if summary is not None: |
723 | + self.assertThat(notification.summary, Eventually(Equals(summary))) |
724 | + if body is not None: |
725 | + self.assertThat(notification.body, Eventually(Equals(body))) |
726 | + if opacity is not None: |
727 | + self.assertThat(notification.opacity, Eventually(Equals(opacity))) |
728 | + |
729 | + if icon: |
730 | + self.assertThat( |
731 | + notification.iconSource, Eventually(NotEquals(''))) |
732 | + else: |
733 | + self.assertThat( |
734 | + notification.iconSource, Eventually(Equals(''))) |
735 | + |
736 | + if secondary_icon: |
737 | + self.assertThat( |
738 | + notification.secondaryIconSource, Eventually(NotEquals(''))) |
739 | + else: |
740 | + self.assertThat( |
741 | + notification.secondaryIconSource, Eventually(Equals(''))) |
742 | + |
743 | + def validate_notification_not_displayed(self, wait=True): |
744 | + """ |
745 | + Validate that the notification is not displayed |
746 | + If wait is True then wait for default timeout period |
747 | + If wait is False then do not wait at all |
748 | + :param wait: wait status |
749 | + """ |
750 | + found = True |
751 | + try: |
752 | + if wait is True: |
753 | + self.main_window.wait_select_single( |
754 | + 'Notification', objectName='notification1') |
755 | + else: |
756 | + self.main_window.select_single( |
757 | + 'Notification', objectName='notification1') |
758 | + except dbus.StateNotFoundError: |
759 | + found = False |
760 | + self.assertFalse(found) |
761 | + |
762 | + def send_push_broadcast_message(self): |
763 | + """ |
764 | + Send a push broadcast message which should trigger a notification |
765 | + to be displayed on the client |
766 | + """ |
767 | + # create a copy of the device's build info |
768 | + device_info = self.create_device_info_copy() |
769 | + # increment the build number to trigger an update |
770 | + device_info.inc_build_number() |
771 | + # create push message based on the device data |
772 | + push_msg = self.push_helper.create_push_message( |
773 | + data=device_info.to_json()) |
774 | + # send the notification message to the server and check response |
775 | + response = self.push_helper.send_push_broadcast_notification( |
776 | + push_msg.to_json(), self.test_config.server_listener_addr) |
777 | + self.validate_response(response) |
778 | + |
779 | + def get_notification_dialog(self, wait=True): |
780 | + """ |
781 | + Get the notification dialog being displaye on screen |
782 | + If wait is True then wait for default timeout period |
783 | + If wait is False then do not wait at all |
784 | + :param wait: wait status |
785 | + :return: dialog introspection object |
786 | + """ |
787 | + if wait is True: |
788 | + dialog = self.main_window.wait_select_single( |
789 | + 'Notification', objectName='notification1') |
790 | + else: |
791 | + dialog = self.main_window.select_single( |
792 | + 'Notification', objectName='notification1') |
793 | + return dialog |
794 | + |
795 | + def validate_and_dismiss_notification_dialog(self, message): |
796 | + """ |
797 | + Validate a notification dialog is displayed and dismiss it |
798 | + :param message: expected message displayed in summary |
799 | + """ |
800 | + # get the dialog |
801 | + dialog = self.get_notification_dialog() |
802 | + # validate dialog |
803 | + self.assert_notification_dialog( |
804 | + dialog, summary=message) |
805 | + # wait for dialog to dismiss automatically |
806 | + self.wait_until_dialog_dismissed(dialog) |
807 | + # check the dialog is no longer displayed |
808 | + self.validate_notification_not_displayed(wait=False) |
809 | + |
810 | + def wait_until_dialog_dismissed(self, dialog): |
811 | + """Wait for the dialog to dismiss automatically""" |
812 | + dialog_disappeared = False |
813 | + try: |
814 | + # waiting for this property to change will cause a not found |
815 | + # exception once the dialog disappears |
816 | + dialog.visible.wait_for(False) |
817 | + except dbus.StateNotFoundError: |
818 | + dialog_disappeared = True |
819 | + # check that the dialog did disappear |
820 | + self.assertTrue(dialog_disappeared) |
821 | + |
822 | + def press_notification_dialog(self, dialog): |
823 | + """ |
824 | + Press the dialog to dismiss it |
825 | + """ |
826 | + self.touch.tap_object(dialog) |
827 | + |
828 | + def dismiss_outstanding_dialog(self): |
829 | + """ |
830 | + Dismiss outstanding notification dialog that may be displayed |
831 | + from an aborted previous test |
832 | + """ |
833 | + try: |
834 | + dialog = self.main_window.select_single( |
835 | + 'Notification', objectName='notification1') |
836 | + except dbus.StateNotFoundError: |
837 | + dialog = None |
838 | + if dialog: |
839 | + self.wait_until_dialog_dismissed(dialog) |
840 | |
841 | === added file 'tests/autopilot/push_notifications/tests/test_broadcast_notifications.py' |
842 | --- tests/autopilot/push_notifications/tests/test_broadcast_notifications.py 1970-01-01 00:00:00 +0000 |
843 | +++ tests/autopilot/push_notifications/tests/test_broadcast_notifications.py 2014-07-10 10:50:28 +0000 |
844 | @@ -0,0 +1,141 @@ |
845 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
846 | +# |
847 | +# Push Notifications Autopilot Test Suite |
848 | +# Copyright (C) 2014 Canonical |
849 | +# |
850 | +# This program is free software: you can redistribute it and/or modify |
851 | +# it under the terms of the GNU General Public License as published by |
852 | +# the Free Software Foundation, either version 3 of the License, or |
853 | +# (at your option) any later version. |
854 | +# |
855 | +# This program is distributed in the hope that it will be useful, |
856 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
857 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
858 | +# GNU General Public License for more details. |
859 | +# |
860 | +# You should have received a copy of the GNU General Public License |
861 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
862 | +# |
863 | + |
864 | +"""Tests broadcast push notifications sent to the client""" |
865 | + |
866 | +import time |
867 | + |
868 | +from push_notifications.tests import PushNotificationTestBase |
869 | + |
870 | + |
871 | +class TestPushClientBroadcast(PushNotificationTestBase): |
872 | + """ |
873 | + Test cases for broadcast push notifications |
874 | + """ |
875 | + |
876 | + DEFAULT_DISPLAY_MESSAGE = 'There\'s an updated system image.' |
877 | + |
878 | + def test_broadcast_push_notification_screen_off(self): |
879 | + """ |
880 | + Send a push message whilst the device's screen is turned off |
881 | + Notification should still be displayed when it is turned on |
882 | + """ |
883 | + # Assumes greeter starts in locked state |
884 | + # Turn display off |
885 | + self.press_power_button() |
886 | + # send message |
887 | + self.send_push_broadcast_message() |
888 | + # wait before turning screen on |
889 | + time.sleep(2) |
890 | + # Turn display on |
891 | + self.press_power_button() |
892 | + self.validate_and_dismiss_notification_dialog( |
893 | + self.DEFAULT_DISPLAY_MESSAGE) |
894 | + |
895 | + def test_broadcast_push_notification_locked_greeter(self): |
896 | + """ |
897 | + Positive test case to send a valid broadcast push notification |
898 | + to the client and validate that a notification message is displayed |
899 | + whist the greeter screen is displayed |
900 | + """ |
901 | + # Assumes greeter starts in locked state |
902 | + self.send_push_broadcast_message() |
903 | + self.validate_and_dismiss_notification_dialog( |
904 | + self.DEFAULT_DISPLAY_MESSAGE) |
905 | + |
906 | + def test_broadcast_push_notification(self): |
907 | + """ |
908 | + Positive test case to send a valid broadcast push notification |
909 | + to the client and validate that a notification message is displayed |
910 | + """ |
911 | + # Assumes greeter starts in locked state |
912 | + self.unlock_greeter() |
913 | + self.send_push_broadcast_message() |
914 | + self.validate_and_dismiss_notification_dialog( |
915 | + self.DEFAULT_DISPLAY_MESSAGE) |
916 | + |
917 | + def test_broadcast_push_notification_on_connect(self): |
918 | + """ |
919 | + Send a broadcast notification whilst the push client is disconnected |
920 | + from the server. Then reconnect and ensure message is displayed |
921 | + """ |
922 | + |
923 | + # Assumes greeter starts in locked state |
924 | + self.unlock_greeter() |
925 | + self.push_client_controller.stop_push_client() |
926 | + self.send_push_broadcast_message() |
927 | + self.push_client_controller.start_push_client() |
928 | + self.validate_and_dismiss_notification_dialog( |
929 | + self.DEFAULT_DISPLAY_MESSAGE) |
930 | + |
931 | + def test_expired_broadcast_push_notification(self): |
932 | + """ |
933 | + Send an expired broadcast notification message to server |
934 | + """ |
935 | + # Assumes greeter starts in locked state |
936 | + self.unlock_greeter() |
937 | + # create notification message using past expiry time |
938 | + device_info = self.create_device_info_copy() |
939 | + device_info.inc_build_number() |
940 | + push_msg = self.push_helper.create_push_message( |
941 | + data=device_info.to_json(), |
942 | + expire_after=self.push_helper.get_past_iso_time()) |
943 | + # send message |
944 | + response = self.push_helper.send_push_broadcast_notification( |
945 | + push_msg.to_json(), |
946 | + self.test_config.server_listener_addr) |
947 | + # 400 status is received for an expired message |
948 | + self.validate_response(response, expected_status_code=400) |
949 | + # validate no notification is displayed |
950 | + self.validate_notification_not_displayed() |
951 | + |
952 | + def test_older_version_broadcast_push_notification(self): |
953 | + """ |
954 | + Send an old version broadcast notification message to server |
955 | + """ |
956 | + # Assumes greeter starts in locked state |
957 | + self.unlock_greeter() |
958 | + # create notification message using previous build number |
959 | + device_info = self.create_device_info_copy() |
960 | + device_info.dec_build_number() |
961 | + push_msg = self.push_helper.create_push_message( |
962 | + data=device_info.to_json()) |
963 | + response = self.push_helper.send_push_broadcast_notification( |
964 | + push_msg.to_json(), |
965 | + self.test_config.server_listener_addr) |
966 | + self.validate_response(response) |
967 | + # validate no notification is displayed |
968 | + self.validate_notification_not_displayed() |
969 | + |
970 | + def test_equal_version_broadcast_push_notification(self): |
971 | + """ |
972 | + Send an equal version broadcast notification message to server |
973 | + """ |
974 | + # Assumes greeter starts in locked state |
975 | + self.unlock_greeter() |
976 | + # create notification message using equal build number |
977 | + device_info = self.create_device_info_copy() |
978 | + push_msg = self.push_helper.create_push_message( |
979 | + data=device_info.to_json()) |
980 | + response = self.push_helper.send_push_broadcast_notification( |
981 | + push_msg.to_json(), |
982 | + self.test_config.server_listener_addr) |
983 | + self.validate_response(response) |
984 | + # validate no notification is displayed |
985 | + self.validate_notification_not_displayed() |
Already approved by everybody and their mom in the previous one.