Merge lp:~rhuddie/ubuntu-push/push-autopilot-tests into lp:ubuntu-push/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
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

To post a comment you must log in.
Revision history for this message
John Lenton (chipaca) wrote :

Already approved by everybody and their mom in the previous one.

review: Approve

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()

Subscribers

People subscribed via source and target branches