Merge lp:~canonical-platform-qa/ubuntu-system-tests/snap-channel-support into lp:ubuntu-system-tests

Proposed by Richard Huddie
Status: Merged
Approved by: Santiago Baldassin
Approved revision: 524
Merged at revision: 521
Proposed branch: lp:~canonical-platform-qa/ubuntu-system-tests/snap-channel-support
Merge into: lp:ubuntu-system-tests
Diff against target: 398 lines (+213/-22)
6 files modified
ubuntu_system_tests/helpers/terminal/app.py (+36/-4)
ubuntu_system_tests/host/commands.py (+55/-9)
ubuntu_system_tests/host/target_setup.py (+44/-2)
ubuntu_system_tests/host/targets.py (+11/-0)
ubuntu_system_tests/selftests/test_commands.py (+60/-4)
ubuntu_system_tests/tests/test_launch_apps.py (+7/-3)
To merge this branch: bzr merge lp:~canonical-platform-qa/ubuntu-system-tests/snap-channel-support
Reviewer Review Type Date Requested Status
Santiago Baldassin (community) Approve
platform-qa-bot continuous-integration Approve
Review via email: mp+320393@code.launchpad.net

Commit message

Add support for installing snaps from beta and edge channels.

Description of the change

Changes include:
- Check the beta channel first and use this version, or use edge channel if beta is not found.
- Update target_setup.py to do this during snap install
- Update Target class to do this during test using --upgrade option
- Get the core series and architecture from target rather than using parameters
- Update command helpers to check for snap channel revisions when creating --setup-commands for upgrade
- Update the Terminal app classes to work with both deb and snap versions as snap version would be required when testing on snap based image.
- Update self tests and add new ones for checking snap channel

This should be landed in parallel with: https://code.launchpad.net/~canonical-platform-qa/qa-jenkins-jobs/snap-channel-support/+merge/320420

To post a comment you must log in.
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
524. By Richard Huddie

Merge from trunk.

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Santiago Baldassin (sbaldassin) wrote :

Looks good in general. I just have a couple of questions:

1. Why is beta the default channel? Once a "snap" silo is ready for QA, will the snap in question be automatically available in the beta channel?

2. I'm not sure yet about the workflow with Bileto but if the app is not available in the beta channel, I would just make the setup to fail. Installing the snap from --edge could lead us to test the wrong version of the app. right?

For example, let's say that there's a new silo for the calc snap v1.2, stable has 1.0 and edge has 1.1, if 1.2 is not uploaded to --beta, then we would end up executing the test cases against 1.1 and approving the 1.2 silo based on the 1.1 results

Thoughts?

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

> 1. Why is beta the default channel? Once a "snap" silo is ready for QA, will
> the snap in question be automatically available in the beta channel?

This change was requested due to the poor state of snap testing, so we could use more stable versions, rather than always taking the latest development version from edge which might not be fit for testing.

This doesn't take into account silos, I'm not sure how that process works for snaps. It would be possible to add a command option to force it to take a specific channel/revision for specific cases.

>
> 2. I'm not sure yet about the workflow with Bileto but if the app is not
> available in the beta channel, I would just make the setup to fail. Installing
> the snap from --edge could lead us to test the wrong version of the app.
> right?
>
> For example, let's say that there's a new silo for the calc snap v1.2, stable
> has 1.0 and edge has 1.1, if 1.2 is not uploaded to --beta, then we would end
> up executing the test cases against 1.1 and approving the 1.2 silo based on
> the 1.1 results
>
> Thoughts?

I'm also unsure how that process would work. But we could easily add some command options to force a specific channel and revision and fail if it can't find it.

These changes would also be related to the other app changes we're discussing about only having 1 version installed, so I think it would be better to look at adding these options again when we have that sorted.

Revision history for this message
Santiago Baldassin (sbaldassin) wrote :

Looks good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntu_system_tests/helpers/terminal/app.py'
2--- ubuntu_system_tests/helpers/terminal/app.py 2017-03-13 14:28:26 +0000
3+++ ubuntu_system_tests/helpers/terminal/app.py 2017-03-30 18:35:16 +0000
4@@ -18,18 +18,50 @@
5 # along with this program. If not, see <http://www.gnu.org/licenses/>.
6 #
7
8-from ubuntu_system_tests.helpers.application import Deb
9+from ubuntu_system_tests.helpers import autopilot
10+from ubuntu_system_tests.helpers.application import (
11+ Deb,
12+ Snap,
13+)
14+from ubuntu_system_tests.helpers.processes import get_process_id
15
16+APP = 'terminal'
17 APP_NAME = 'Terminal'
18 APP_ID = 'com.ubuntu.terminal'
19 APP_PACKAGE_ID = 'ubuntu-terminal-app'
20
21
22-class Terminal(Deb):
23+class TerminalDeb(Deb):
24
25 def __init__(self):
26 super().__init__(APP_NAME, APP_ID, APP_PACKAGE_ID)
27
28 def get_main_view(self):
29- from ubuntu_system_tests.helpers.terminal import cpo # NOQA
30- return self.get_proxy_object().main_view
31+ return get_main_view()
32+
33+
34+class TerminalSnap(Snap):
35+
36+ def __init__(self):
37+ super().__init__(APP_NAME, APP_ID)
38+
39+ def get_main_view(self):
40+ return get_main_view()
41+
42+
43+def get_main_view():
44+ from ubuntu_system_tests.helpers.terminal import cpo # NOQA
45+ pid = get_process_id(APP)
46+ return autopilot.get_proxy_object(pid=pid).main_view
47+
48+
49+def get_terminal_app(mode):
50+ """Return terminal application class of required type.
51+ :param mode: Either 'snap' or 'deb'.
52+ :return: Either TerminalDeb or TerminalSnap class reference.
53+ """
54+ app_modes = {
55+ 'deb': TerminalDeb,
56+ 'snap': TerminalSnap
57+ }
58+ return app_modes[mode]
59
60=== modified file 'ubuntu_system_tests/host/commands.py'
61--- ubuntu_system_tests/host/commands.py 2017-03-09 17:21:07 +0000
62+++ ubuntu_system_tests/host/commands.py 2017-03-30 18:35:16 +0000
63@@ -19,6 +19,7 @@
64 # along with this program. If not, see <http://www.gnu.org/licenses/>.
65 #
66
67+import requests
68 import subprocess
69
70 from ubuntu_system_tests.common import (
71@@ -44,6 +45,23 @@
72 legacy_cmds = None
73
74 XENIAL = '16.04'
75+SNAP_STORE_QUERY_URL = (
76+ 'https://search.apps.ubuntu.com/api/v1/snaps/details/{n}?channel={c}'
77+)
78+# List of apps to be installed as snaps on a deb based session.
79+# Anything else will be installed as a deb.
80+# This is used for upgrade command where a list of packages is
81+# specified which must be upgraded before running a test.
82+SNAP_APPS = [
83+ 'address-book-app',
84+ 'camera-app',
85+ 'gallery-app',
86+ 'ubuntu-calculator-app',
87+ 'ubuntu-calendar-app',
88+ 'ubuntu-clock-app',
89+ 'ubuntu-filemanager-app',
90+ 'webbrowser-app',
91+]
92
93
94 def use_legacy_commands():
95@@ -158,20 +176,48 @@
96 def _get_upgrade_setup_command(target, upgrade):
97 """Return setup command to upgrade csv list of components."""
98 if upgrade:
99+ upgrade_list = upgrade.split(',')
100+ cmd = ''
101+ series = target.core_series()
102+ arch = target.architecture()
103 if target.snap_session():
104- cmd = ''
105- for name in upgrade.split(','):
106- cmd += 'snap refresh --devmode --edge {}; '.format(name)
107- cmd = cmd.strip()
108+ # For a snap session all apps must be snaps
109+ for name in upgrade_list:
110+ cmd += _get_snap_channel_upgrade_command(series, arch, name)
111 else:
112- upgrade = upgrade.replace(',', ' ')
113- cmd = (
114- 'apt-get -y --no-install-recommends install '
115- '{}'.format(upgrade))
116- return ['--setup-commands', _get_fs_rw_command(target) + cmd]
117+ # For a deb session, the apps could be either deb or snap
118+ for name in upgrade_list:
119+ if name in SNAP_APPS:
120+ cmd += _get_snap_channel_upgrade_command(
121+ series, arch, name)
122+ else:
123+ cmd += _get_deb_upgrade_command(name)
124+ return ['--setup-commands', _get_fs_rw_command(target) + cmd.strip()]
125 return []
126
127
128+def _get_snap_channel_upgrade_command(series, arch, snap_name):
129+ for channel in ['beta', 'edge']:
130+ if _is_snap_published_for_channel(series, arch, snap_name, channel):
131+ return _get_snap_upgrade_command(snap_name, channel)
132+ raise RuntimeError('No channel found for {}.'.format(snap_name))
133+
134+
135+def _get_snap_upgrade_command(app, channel):
136+ return 'snap refresh --devmode --{c} {a}; '.format(c=channel, a=app)
137+
138+
139+def _get_deb_upgrade_command(app):
140+ return 'apt-get -y --no-install-recommends install {}; '.format(app)
141+
142+
143+def _is_snap_published_for_channel(series, arch, snap, channel):
144+ headers = {'X-Ubuntu-Series': series, 'X-Ubuntu-Architecture': arch}
145+ url = SNAP_STORE_QUERY_URL.format(n=snap, c=channel)
146+ response = requests.get(url, headers=headers)
147+ return response.ok
148+
149+
150 def _get_fs_ro_setup_command(target):
151 """Return setup command to set file system read-only."""
152 if target.config.get('mount_fs_ro', False):
153
154=== modified file 'ubuntu_system_tests/host/target_setup.py'
155--- ubuntu_system_tests/host/target_setup.py 2017-03-13 14:28:26 +0000
156+++ ubuntu_system_tests/host/target_setup.py 2017-03-30 18:35:16 +0000
157@@ -24,7 +24,9 @@
158 import dbus
159 import json
160 import os
161+import platform
162 import pwd
163+import requests
164 import shutil
165 import subprocess
166 import sys
167@@ -48,6 +50,10 @@
168 XENIAL = 16.04
169 YAKKETY = 16.10
170
171+SNAP_STORE_QUERY_URL = (
172+ 'https://search.apps.ubuntu.com/api/v1/snaps/details/{n}?channel={c}'
173+)
174+
175
176 class SetupRunner:
177
178@@ -57,6 +63,8 @@
179 self.config = config
180 self.series = None
181 self.release = None
182+ self.core_series = None
183+ self.arch = None
184 self._set_cache_dir()
185
186 def _set_cache_dir(self):
187@@ -95,6 +103,23 @@
188 ['lsb_release', '-cs']).decode().strip()
189 return self.series
190
191+ def get_core_series(self):
192+ """Return the Ubuntu Core series."""
193+ if self.core_series is None:
194+ self.core_series = subprocess.check_output(
195+ 'snap version | grep series | tr -s " " | cut -d" " -f2',
196+ shell=True).decode().strip()
197+ return self.core_series
198+
199+ def get_architecture(self):
200+ """Return the current architecture."""
201+ if self.arch is None:
202+ if platform.architecture()[0] == '64bit':
203+ self.arch = 'amd64'
204+ else:
205+ self.arch = 'i386'
206+ return self.arch
207+
208 def update_apt(self):
209 """Return command to apt-get update."""
210 if self.config['update_apt']:
211@@ -182,9 +207,26 @@
212 if snaps:
213 self.mount_fs_rw()
214 for snap in snaps:
215+ installed = False
216 cmd = 'refresh' if self.is_snap_installed(snap) else 'install'
217- subprocess.check_call(
218- ['snap', cmd, '--devmode', '--edge', snap])
219+ for channel in ['beta', 'edge']:
220+ if self.is_snap_published_for_channel(snap, channel):
221+ channel_arg = '--{}'.format(channel)
222+ subprocess.check_call(
223+ ['snap', cmd, '--devmode', channel_arg, snap])
224+ installed = True
225+ break
226+ if not installed:
227+ raise RuntimeError(
228+ 'Could not find channel for snap {}'.format(snap))
229+
230+ def is_snap_published_for_channel(self, snap, channel):
231+ arch = self.get_architecture()
232+ series = self.get_core_series()
233+ headers = {'X-Ubuntu-Series': series, 'X-Ubuntu-Architecture': arch}
234+ url = SNAP_STORE_QUERY_URL.format(n=snap, c=channel)
235+ response = requests.get(url, headers=headers)
236+ return response.ok
237
238 def is_snap_installed(self, snap):
239 return os.path.isdir(os.path.join('/snap', snap))
240
241=== modified file 'ubuntu_system_tests/host/targets.py'
242--- ubuntu_system_tests/host/targets.py 2017-03-07 16:30:47 +0000
243+++ ubuntu_system_tests/host/targets.py 2017-03-30 18:35:16 +0000
244@@ -131,6 +131,17 @@
245 def release(self):
246 return self.run('lsb_release -rs', log_stdout=False).output.strip()
247
248+ def core_series(self):
249+ return self.run(
250+ 'snap version | grep series | tr -s " " | cut -d" " -f2 ',
251+ log_stdout=False).output.strip()
252+
253+ def architecture(self):
254+ arch = self.run('uname --machine', log_stdout=False).output.strip()
255+ if arch == 'x86_64':
256+ return 'amd64'
257+ return 'i386'
258+
259 def user_id(self, user):
260 """Return user id for specified user name."""
261 return self.run(
262
263=== modified file 'ubuntu_system_tests/selftests/test_commands.py'
264--- ubuntu_system_tests/selftests/test_commands.py 2017-02-24 14:22:54 +0000
265+++ ubuntu_system_tests/selftests/test_commands.py 2017-03-30 18:35:16 +0000
266@@ -228,12 +228,20 @@
267 class MockTarget:
268
269 def __init__(self, **kwargs):
270+ self._core_series = '16'
271+ self._architecture = 'amd64'
272 for arg in kwargs.keys():
273 setattr(self, arg, kwargs[arg])
274
275 def snap_session(self):
276 return self.snap
277
278+ def core_series(self):
279+ return self._core_series
280+
281+ def architecture(self):
282+ return self._architecture
283+
284
285 class TestUpgradeSetupCommands(ConfigBaseTestCase):
286
287@@ -241,14 +249,20 @@
288 super().setUp()
289 self.args = DummyArgs(upgrade='package1,package2')
290
291- def test_get_upgrade_setup_command_snap(self):
292+ @mock.patch('ubuntu_system_tests.host.commands.'
293+ '_is_snap_published_for_channel',
294+ return_value=True)
295+ def test_get_upgrade_setup_command_snap(self, mock_is_snap_published):
296 target = MockTarget(snap=True, config={'mount_fs_rw': False})
297 cmd = commands._get_upgrade_setup_command(target, self.args.upgrade)
298+ mock_is_snap_published.assert_has_calls([
299+ mock.call('16', 'amd64', 'package1', 'beta'),
300+ mock.call('16', 'amd64', 'package2', 'beta')])
301 self.assertEqual(
302 cmd,
303 ['--setup-commands',
304- 'snap refresh --devmode --edge package1; '
305- 'snap refresh --devmode --edge package2;'])
306+ 'snap refresh --devmode --beta package1; '
307+ 'snap refresh --devmode --beta package2;'])
308
309 def test_get_upgrade_setup_command_deb(self):
310 target = MockTarget(snap=False, config={'mount_fs_rw': False})
311@@ -256,9 +270,51 @@
312 self.assertEqual(
313 cmd,
314 ['--setup-commands',
315- 'apt-get -y --no-install-recommends install package1 package2'])
316+ 'apt-get -y --no-install-recommends install package1; '
317+ 'apt-get -y --no-install-recommends install package2;'])
318
319 def test_get_upgrade_setup_includes_fs_rw(self):
320 target = MockTarget(snap=False, config={'mount_fs_rw': True})
321 cmd = commands._get_upgrade_setup_command(target, self.args.upgrade)
322 self.assertIn('mount -o remount,rw /', cmd[1])
323+
324+
325+class TestSnapUpgradeChannelSelection(ConfigBaseTestCase):
326+
327+ @mock.patch('ubuntu_system_tests.host.commands.'
328+ '_is_snap_published_for_channel',
329+ return_value=True)
330+ def test_beta_channel_first_priority(self, mock_is_snap_published):
331+ target = MockTarget(snap=True, config={'mount_fs_rw': False})
332+ cmd = commands._get_upgrade_setup_command(target, 'pkg1')
333+ self.assertEqual(
334+ cmd,
335+ ['--setup-commands',
336+ 'snap refresh --devmode --beta pkg1;'])
337+ mock_is_snap_published.assert_has_calls([
338+ mock.call('16', 'amd64', 'pkg1', 'beta')])
339+
340+ @mock.patch('ubuntu_system_tests.host.commands.'
341+ '_is_snap_published_for_channel',
342+ side_effect=[False, True])
343+ def test_edge_channel_second_priority(self, mock_is_snap_published):
344+ target = MockTarget(snap=True, config={'mount_fs_rw': False})
345+ cmd = commands._get_upgrade_setup_command(target, 'pkg1')
346+ self.assertEqual(
347+ cmd,
348+ ['--setup-commands',
349+ 'snap refresh --devmode --edge pkg1;'])
350+ mock_is_snap_published.assert_has_calls([
351+ mock.call('16', 'amd64', 'pkg1', 'beta'),
352+ mock.call('16', 'amd64', 'pkg1', 'edge')])
353+
354+ @mock.patch('ubuntu_system_tests.host.commands.'
355+ '_is_snap_published_for_channel',
356+ side_effect=[False, False])
357+ def test_raises_on_no_channel_found(self, mock_is_snap_published):
358+ target = MockTarget(snap=True, config={'mount_fs_rw': False})
359+ with self.assertRaises(RuntimeError):
360+ commands._get_upgrade_setup_command(target, 'pkg1')
361+ mock_is_snap_published.assert_has_calls([
362+ mock.call('16', 'amd64', 'pkg1', 'beta'),
363+ mock.call('16', 'amd64', 'pkg1', 'edge')])
364
365=== modified file 'ubuntu_system_tests/tests/test_launch_apps.py'
366--- ubuntu_system_tests/tests/test_launch_apps.py 2017-03-14 17:05:13 +0000
367+++ ubuntu_system_tests/tests/test_launch_apps.py 2017-03-30 18:35:16 +0000
368@@ -31,7 +31,9 @@
369 from ubuntu_system_tests.helpers.clock.app import Clock
370 from ubuntu_system_tests.helpers.filemanager.app import FileManager
371 from ubuntu_system_tests.helpers.gallery.app import Gallery
372-from ubuntu_system_tests.helpers.terminal.app import Terminal
373+from ubuntu_system_tests.helpers.terminal.app import (
374+ get_terminal_app,
375+)
376 from ubuntu_system_tests.helpers.system_settings.app import (
377 get_system_settings_app,
378 )
379@@ -96,7 +98,8 @@
380 self.check_app_launch(FileManager, 1)
381
382 def test_launch_terminal_app(self):
383- self.check_app_launch(Terminal, 1)
384+ app = get_terminal_app(self.config_stack.get('unity8_mode'))
385+ self.check_app_launch(app, 1)
386
387 def test_launch_webbrowser_app(self):
388 self.check_app_launch(WebBrowser, 1)
389@@ -131,7 +134,8 @@
390 self.check_app_launch(FileManager, 2)
391
392 def test_launch_terminal_app_twice(self):
393- self.check_app_launch(Terminal, 2)
394+ app = get_terminal_app(self.config_stack.get('unity8_mode'))
395+ self.check_app_launch(app, 2)
396
397 def test_launch_webbrowser_app_twice(self):
398 self.check_app_launch(WebBrowser, 2)

Subscribers

People subscribed via source and target branches

to all changes: