Merge lp:~rhuddie/ubuntu-push/push-autopilot-tests into lp:ubuntu-push/automatic

Proposed by Richard Huddie
Status: Superseded
Proposed branch: lp:~rhuddie/ubuntu-push/push-autopilot-tests
Merge into: lp:ubuntu-push/automatic
Diff against target: 844 lines (+789/-0)
10 files modified
tests/autopilot/push_notifications/README (+57/-0)
tests/autopilot/push_notifications/__init__.py (+8/-0)
tests/autopilot/push_notifications/config/__init__.py (+27/-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 (+85/-0)
tests/autopilot/push_notifications/helpers/__init__.py (+6/-0)
tests/autopilot/push_notifications/helpers/push_notifications_helper.py (+259/-0)
tests/autopilot/push_notifications/tests/__init__.py (+219/-0)
tests/autopilot/push_notifications/tests/test_broadcast_notifications.py (+113/-0)
To merge this branch: bzr merge lp:~rhuddie/ubuntu-push/push-autopilot-tests
Reviewer Review Type Date Requested Status
Leo Arias Pending
Allan LeSage Pending
VĂ­ctor R. Ruiz Pending
Review via email: mp+216733@code.launchpad.net

This proposal supersedes a proposal from 2014-04-15.

Description of the change

A new autopilot test framework for testing push notifications.

The tests will configure the client to run against a specified push server and then send through a push notification message which is displayed on device's screen.

To post a comment you must log in.
Revision history for this message
Leo Arias (elopio) wrote : Posted in a previous version of this proposal

Hey Richard. You need to resubmit your MP against lp:ubuntu-push/automatic instead of trunk. I'll forward you the mail pedronis sent about this.

review: Needs Fixing
Revision history for this message
Leo Arias (elopio) wrote : Posted in a previous version of this proposal

53 +5) autopilot3 list push-notifications
54 +6) autopilot3 run push-notifications

You have a typo there ^. It should be push_notifications.

166 +class PushNotificationMessage:
187 +class NotificationData:

I would put these classes in a push_notifications/data.py module.

210 + if dbus_info != None:
215 + elif copy_obj != None:

You should make checks like these with 'is not' instead of !=.
That's from pep8: "Comparisons to singletons like None should always be done with is or is not, never the equality operators."

203 + def __init__(self, dbus_info=None, copy_obj=None):

I find this to be a confusing constructor. I think it would be clearer to make two @classmethods, like
def from_dbus_info(dbus_info=None)
def copy(original_object=None)

Actually, I'm not sure you would need that copy. You could use the copy module.

review: Needs Fixing
Revision history for this message
Richard Huddie (rhuddie) wrote :

Thanks Leo for your comments.

I have re-submitted the proposal against lp:ubuntu-push/automatic

I have also addressed all the other points you mentioned and fixed all the pep8 warnings.

There is still lots of work to do on this branch (e.g. add the dialog validation and add more tests etc.), but it's very good to have your feedback.

Thanks,
Richard.

112. By Richard Huddie

remove dbus dependency so it can run without root

113. By Richard Huddie

unity8 dialog validation

114. By Richard Huddie

added some new tests including locked greeter

115. By Richard Huddie

re-structure tests so that client is not re-started before each test

116. By Richard Huddie

restructure to use helper classes

117. By Richard Huddie

tidy and add test for device screen off

118. By Richard Huddie

fix flake8

119. By Richard Huddie

clear notifications before running test and move stuff into base class

120. By Richard Huddie

added cert_pem_file configuration to client

121. By Richard Huddie

certificate file path update

122. By Richard Huddie

rename tests for broadcast

123. By Richard Huddie

minor change

124. By Richard Huddie

tidy up

125. By Richard Huddie

update dialog assertion

126. By Richard Huddie

tidy up and add documentation to methods

127. By Richard Huddie

remove comment

128. By Richard Huddie

Readme updates

129. By Richard Huddie

disable mocking and minor updates

130. By Richard Huddie

move display message from base class

131. By Richard Huddie

fix review comments

132. By Richard Huddie

fix review comment

133. By Richard Huddie

Copyright header updates

134. By Richard Huddie

split greeter changes

135. By Richard Huddie

tidied up and added extra test

136. By Richard Huddie

merge with trunk

137. By Richard Huddie

update comment

138. By Richard Huddie

revert split greeter changes

139. By Richard Huddie

use /sbin/initctl

140. By Richard Huddie

update readme including emulator info

141. By Richard Huddie

wait for notification to dismiss automatically

Unmerged revisions

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

Subscribers

People subscribed via source and target branches