Merge lp:~sil2100/ubuntu-release-upgrader/snap-size-estimation into lp:ubuntu-release-upgrader

Proposed by Łukasz Zemczak
Status: Merged
Merged at revision: 3280
Proposed branch: lp:~sil2100/ubuntu-release-upgrader/snap-size-estimation
Merge into: lp:ubuntu-release-upgrader
Diff against target: 440 lines (+321/-38)
3 files modified
DistUpgrade/DistUpgradeCache.py (+2/-0)
DistUpgrade/DistUpgradeQuirks.py (+115/-38)
tests/test_quirks.py (+204/-0)
To merge this branch: bzr merge lp:~sil2100/ubuntu-release-upgrader/snap-size-estimation
Reviewer Review Type Date Requested Status
Brian Murray Needs Fixing
Review via email: mp+371120@code.launchpad.net

Commit message

Modify DistUpgradeQuirks to calculate extra space needed for snaps that need to be installed during upgrade (replacing old debs) and add that to estimates done in checkFreeSpace().

Description of the change

Modify DistUpgradeQuirks to calculate extra space needed for snaps that need to be installed during upgrade (replacing old debs) and add that to estimates done in checkFreeSpace().

This branch is now ready for an initial review. I had a few design bikeshedding issues while working on this, so I'm still not entirely happy with the end result. For instance, I don't like how it looks with the consecutive quirks assuming the previous ones set up everything for them - but that seemed to be optimal.

I added all the estimation as a quirk, alongside of the existing ones for replacing debs with snaps. Seemed to be 'consistent' with the current approach. In cases where quirks are not ran, the extra_snap_space is 0 so nothing funny is happening. I also added some very basic unit testing for the new methods added. They're not very pretty, mostly because a lot of the functionality had to be mocked as it required network access, which we don't want to rely on in unit testing.
For fetching the size, I try to be as accurate as possible - which is why I am performing raw API calls to the snapstore as per their recommendation. I could not use snap_info as we need to get the size of the snap for the specific branch (stable/ubuntu-x.x) - and only a fake snap_refresh allows that (which needs the snap-id specified). At first I wanted to be consistent and just use `snap info`, but as said - the command didn't seem to allow specifying the branch that needed to be checked.

I'm open to any propositions. This is my first contribution to u-r-u so apologies if the coding style is not adequate. I still need to test it in practice, which is why I'd prefer if we didn't merge it yet before more testing.

To post a comment you must log in.
Revision history for this message
Łukasz Zemczak (sil2100) wrote :

From the stylistic nitpicks for instance, I really had a strong urge to just call _prepare_snap_replacement_data() in both _calculateSnapSizeRequirements() and _replaceDebsWithSnaps(), and simply make it to exit early if self._snap_list is already non-empty. But then it would be non-optimal, since we know _replaceDebsWithSnaps will never be called without _calculateSnapSizeRequirements running first. But eh, I somehow couldn't make this look pretty, sorry...

Revision history for this message
Brian Murray (brian-murray) wrote :

Looking at DistUpgradeQuirks.py's run function we can see that there are currently 6 different quirk handlers that are supported. Is there a reason you didn't use an existing one and created "PreCalcDistUpgrade"? If there is a good reason then I think PreCalcDistUpgrade should be documented with the other ones.

review: Needs Information
Revision history for this message
Brian Murray (brian-murray) wrote :

I think it'd also be good if DistUpgradeCache.py were to log that the not enough free space error in /var was due to the fact that extra_snap_space was size X.

Revision history for this message
Brian Murray (brian-murray) wrote :

Thanks for working on this! You'll find some specific comments in line.

review: Needs Fixing
3280. By Łukasz Zemczak

Fixes as per Brian's review: do not create a PreCalcDistUpgrade quirk (no needed), fix typos.

Revision history for this message
Łukasz Zemczak (sil2100) wrote :

Thanks for the review! I will address all the issues you have outlined, with comments to a few of them (not including those inline as the diff will change and it's always a bit more annoying to switch to the old revision to see the comments):

1) Regarding PreCalcDistUpgrade: actually I see that I can just do the same in PostInitialUpdate, not sure why those weeks ago I didn't find that one appropriate. I just know any of the later ones would not do, as it has to happen early in the upgrade process. One plus side of having a PreCalcDistUpgrade was that it all felt a bit more consistent, as the size calculation was done right before the dist-upgrade was 'calculated'. I changed it to be done as part of PostInitialUpdate now which is also fine, but the only downside is that now very early users might be seeing 'Calculating snap size requirements', which might seem a bit out-of-place. On the other hand, that doesn't actually matter much I'd say!

2) Regarding changing the 'installing ...' strings as you requested - won't that break translations? I mean, I'm usually very reluctant in changing translatable strings without a valid reason. Guess this particular change won't be backported to bionic though, so maybe it's actually safe. Anyway, I guess I'd recommend changing those in a separate MP/commit.

3) Regarding Snap-Device-Series: for now this is a constant that I think can just stay hard-coded. One might think it's related to the core base (16, 18 etc.) - that would be logical, right? But actually it's not. I was confused about that as well, but the snapd team told me this series is actually unrelated to the snap base but is instead like a snap-specific standard version which should be only bumped when the standard changes, which for now is still constant. So this argument is '16' for both core16, core18 and possibly the upcoming core20.

4) Regarding the count of 5 in the test for replacing debs with snaps: I guess it feels correct, because basically currently as well as previously when determining which debs need to be exchanged with snaps, at the stage of when the quirk is running there was never any logic to check if we're replacing a real deb or not. So for instance 'gtk-common-themes', which is not a real deb package, in the case when its snap was not installed on the system, would anyway mark it for installation and put it into forced_obsoletes. The logic here didn't change. What the unit test checks is only if the 'refresh' snaps are refreshed and if the 'install' snaps are installed (and, per the existing logic, added to the forced_obsoletes).
I can only assume that the upgrader actually checks if the packages in forced_obsoletes are actual packages that need acting on somewhere later on.

5) Regarding the new dependencies and adding those to debian/control: I think no new dependencies need to be added. Both the json and urllib modules are part of the python standard library, so they basically come from the libpython3.*-stdlib etc. packages, and those are explicit dependencies of python3.*. So I guess we should be safe here, right?

Revision history for this message
Łukasz Zemczak (sil2100) wrote :

Was also looking into making DistUpgradeCache mentioning the not enough space being because of extra_snap_space, but from what I see the way it's written right now feels very generic and not sure if I should change that. But I'll look into it a bit more.

Revision history for this message
Brian Murray (brian-murray) wrote :
Download full text (3.3 KiB)

> Thanks for the review! I will address all the issues you have outlined, with
> comments to a few of them (not including those inline as the diff will change
> and it's always a bit more annoying to switch to the old revision to see the
> comments):
>
> 1) Regarding PreCalcDistUpgrade: actually I see that I can just do the same in
> PostInitialUpdate, not sure why those weeks ago I didn't find that one
> appropriate. I just know any of the later ones would not do, as it has to
> happen early in the upgrade process. One plus side of having a
> PreCalcDistUpgrade was that it all felt a bit more consistent, as the size
> calculation was done right before the dist-upgrade was 'calculated'. I changed
> it to be done as part of PostInitialUpdate now which is also fine, but the
> only downside is that now very early users might be seeing 'Calculating snap
> size requirements', which might seem a bit out-of-place. On the other hand,
> that doesn't actually matter much I'd say!

Great, thanks.

> 2) Regarding changing the 'installing ...' strings as you requested - won't
> that break translations? I mean, I'm usually very reluctant in changing
> translatable strings without a valid reason. Guess this particular change
> won't be backported to bionic though, so maybe it's actually safe. Anyway, I
> guess I'd recommend changing those in a separate MP/commit.

That's fair and not a big deal really.

> 3) Regarding Snap-Device-Series: for now this is a constant that I think can
> just stay hard-coded. One might think it's related to the core base (16, 18
> etc.) - that would be logical, right? But actually it's not. I was confused
> about that as well, but the snapd team told me this series is actually
> unrelated to the snap base but is instead like a snap-specific standard
> version which should be only bumped when the standard changes, which for now
> is still constant. So this argument is '16' for both core16, core18 and
> possibly the upcoming core20.

Alright, thanks for explaining this!

> 4) Regarding the count of 5 in the test for replacing debs with snaps: I guess
> it feels correct, because basically currently as well as previously when
> determining which debs need to be exchanged with snaps, at the stage of when
> the quirk is running there was never any logic to check if we're replacing a
> real deb or not. So for instance 'gtk-common-themes', which is not a real deb
> package, in the case when its snap was not installed on the system, would
> anyway mark it for installation and put it into forced_obsoletes. The logic
> here didn't change. What the unit test checks is only if the 'refresh' snaps
> are refreshed and if the 'install' snaps are installed (and, per the existing
> logic, added to the forced_obsoletes).
> I can only assume that the upgrader actually checks if the packages in
> forced_obsoletes are actual packages that need acting on somewhere later on.

That makes sense, maybe its worth adding a comment about why it is 5 though?

> 5) Regarding the new dependencies and adding those to debian/control: I think
> no new dependencies need to be added. Both the json and urllib modules are
> part of the python standard library, so the...

Read more...

Revision history for this message
Brian Murray (brian-murray) wrote :

This look good to me now, but as I mentioned on irc I'd expect the size calculation to fail for the core18 snap because there is no stable/ubuntu-19.10 channel for core18. It'd be good to test that this is really failing before fixing bug 1841225.

3281. By Łukasz Zemczak

Add a better comment regarding the forced_obsoletes check in quirks.

Revision history for this message
Łukasz Zemczak (sil2100) wrote :

I did some testing on a VM and real upgrade scenarios and with this branch and the core18-stable follow up one, everything seems to work as expected. Let me merge both in this case.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'DistUpgrade/DistUpgradeCache.py'
2--- DistUpgrade/DistUpgradeCache.py 2019-06-21 17:46:27 +0000
3+++ DistUpgrade/DistUpgradeCache.py 2019-08-30 14:54:30 +0000
4@@ -1157,6 +1157,8 @@
5 # sum up space requirements
6 for (dir, size) in [(archivedir, self.required_download),
7 ("/usr", self.additional_required_space),
8+ # this is only >0 for the deb-to-snap quirks
9+ ("/var", self.quirks.extra_snap_space),
10 # plus 50M safety buffer in /usr
11 ("/usr", 50*1024*1024),
12 ("/boot", space_in_boot),
13
14=== modified file 'DistUpgrade/DistUpgradeQuirks.py'
15--- DistUpgrade/DistUpgradeQuirks.py 2019-06-24 18:16:09 +0000
16+++ DistUpgrade/DistUpgradeQuirks.py 2019-08-30 14:54:30 +0000
17@@ -49,8 +49,12 @@
18 self.uname = Popen(["uname", "-r"], stdout=PIPE,
19 universal_newlines=True).communicate()[0].strip()
20 self.arch = get_arch()
21+ self.extra_snap_space = 0
22 self._poke = None
23 self._snapstore_reachable = False
24+ self._snap_list = None
25+ self._from_version = None
26+ self._to_version = None
27
28 # the quirk function have the name:
29 # $Name (e.g. PostUpgrade)
30@@ -118,6 +122,11 @@
31 if cache['ubuntu-desktop'].is_installed and \
32 cache['snapd'].is_installed:
33 self._checkStoreConnectivity()
34+ # If the snap store is accessible, at the same time calculate the
35+ # extra size needed by to-be-installed snaps. This also prepares
36+ # the snaps-to-install list for the actual upgrade.
37+ if self._snapstore_reachable:
38+ self._calculateSnapSizeRequirements()
39
40 def eoanPostUpgrade(self):
41 logging.debug("running Quirks.eoanPostUpgrade")
42@@ -128,7 +137,7 @@
43 return
44 if cache['ubuntu-desktop'].is_installed and \
45 cache['snapd'].is_installed and \
46- self._snapstore_reachable:
47+ self._snap_list:
48 self._replaceDebsWithSnaps()
49
50 # individual quirks handler when the dpkg run is finished ---------
51@@ -439,51 +448,65 @@
52 if not res:
53 self.controller.abort()
54
55+ def _calculateSnapSizeRequirements(self):
56+ import json
57+ import urllib.request
58+ from urllib.error import URLError
59+
60+ # first fetch the list of snap-deb replacements that will be needed
61+ # and store them for future reference, along with other data we'll
62+ # need in the process
63+ self._prepare_snap_replacement_data()
64+ # now perform direct API calls to the store, requesting size
65+ # information for each of the snaps needing installation
66+ self._view.updateStatus(_("Calculating snap size requirements"))
67+ for snap, snap_object in self._snap_list.items():
68+ if snap_object['command'] != 'install':
69+ continue
70+ action = {
71+ "instance-key": "upgrade-size-check",
72+ "action": "download",
73+ "snap-id": snap_object['snap-id'],
74+ "channel": "stable/ubuntu-%s" % self._to_version,
75+ }
76+ data = {
77+ "context": [],
78+ "actions": [action],
79+ }
80+ req = urllib.request.Request(
81+ url='https://api.snapcraft.io/v2/snaps/refresh',
82+ data=bytes(json.dumps(data), encoding='utf-8'))
83+ req.add_header('Snap-Device-Series', '16')
84+ req.add_header('Content-type', 'application/json')
85+ req.add_header('Snap-Device-Architecture', self.arch)
86+ try:
87+ response = urllib.request.urlopen(req).read()
88+ info = json.loads(response)
89+ size = int(info['results'][0]['snap']['download']['size'])
90+ except (KeyError, URLError, ValueError):
91+ logging.debug("Failed fetching size of snap %s" % snap)
92+ continue
93+ self.extra_snap_space += size
94+
95 def _replaceDebsWithSnaps(self):
96- di = distro_info.UbuntuDistroInfo()
97- try:
98- fromVersion = \
99- di.version('%s' % self.controller.fromDist).split()[0]
100- toVersion = di.version('%s' % self.controller.toDist).split()[0]
101- # Ubuntu 18.04's python3-distro-info does not have version
102- except AttributeError:
103- fromVersion = next((r.version for r in di.get_all("object")
104- if r.series == self.controller.fromDist),
105- self.controller.fromDist).split()[0]
106- toVersion = next((r.version for r in di.get_all("object")
107- if r.series == self.controller.toDist),
108- self.controller.toDist).split()[0]
109 """ install a snap and mark its corresponding package for removal """
110 # gtk-common-themes isn't a package name but is this risky?
111- snaps = ['core18', 'gnome-3-28-1804', 'gtk-common-themes',
112- 'gnome-calculator', 'gnome-characters', 'gnome-logs',
113- 'gnome-system-monitor']
114- self._view.updateStatus(_("Checking for installed snaps"))
115- for snap in snaps:
116- # check to see if the snap is already installed
117- snap_info = subprocess.Popen(["snap", "info", snap],
118- universal_newlines=True,
119- stdout=subprocess.PIPE).communicate()
120- self._view.processEvents()
121- if re.search("^installed: ", snap_info[0], re.MULTILINE):
122- logging.debug("Snap %s is installed" % snap)
123- # its not tracking the release channel so don't refresh
124- if not re.search("^tracking:.*ubuntu-%s" % fromVersion,
125- snap_info[0], re.MULTILINE):
126- logging.debug("Snap %s is not tracking the release channel"
127- % snap)
128- continue
129- command = 'refresh'
130+ self._view.updateStatus(_("Processing snap replacements"))
131+ # _snap_list should be populated by the earlier
132+ # _calculateSnapSizeRequirements call.
133+ for snap, snap_object in self._snap_list.items():
134+ command = snap_object['command']
135+ if command == 'refresh':
136 self._view.updateStatus(_("refreshing snap %s" % snap))
137 else:
138- command = 'install'
139 self._view.updateStatus(_("installing snap %s" % snap))
140 try:
141 self._view.processEvents()
142- proc = subprocess.run(["snap", command, "--channel",
143- "stable/ubuntu-%s" % toVersion, snap],
144- stdout=subprocess.PIPE,
145- check=True)
146+ proc = subprocess.run(
147+ ["snap", command, "--channel",
148+ "stable/ubuntu-%s" % self._to_version, snap],
149+ stdout=subprocess.PIPE,
150+ check=True)
151 self._view.processEvents()
152 except subprocess.CalledProcessError:
153 logging.debug("%s of snap %s failed" % (command, snap))
154@@ -767,3 +790,57 @@
155 msg += " enabling it just for the upgrade"
156 logging.warning(msg)
157 apt.apt_pkg.config.set("Apt::Install-Recommends", "1")
158+
159+ def _prepare_snap_replacement_data(self):
160+ """ Helper function fetching all required info for the deb-to-snap
161+ migration: version strings for upgrade (from and to) and the list
162+ of snaps (with actions).
163+ """
164+ di = distro_info.UbuntuDistroInfo()
165+ try:
166+ self._from_version = \
167+ di.version('%s' % self.controller.fromDist).split()[0]
168+ self._to_version = \
169+ di.version('%s' % self.controller.toDist).split()[0]
170+ # Ubuntu 18.04's python3-distro-info does not have version
171+ except AttributeError:
172+ self._from_version = next(
173+ (r.version for r in di.get_all("object")
174+ if r.series == self.controller.fromDist),
175+ self.controller.fromDist).split()[0]
176+ self._to_version = next(
177+ (r.version for r in di.get_all("object")
178+ if r.series == self.controller.toDist),
179+ self.controller.toDist).split()[0]
180+ self._snap_list = {}
181+ # gtk-common-themes isn't a package name but is this risky?
182+ snaps = ['core18', 'gnome-3-28-1804', 'gtk-common-themes',
183+ 'gnome-calculator', 'gnome-characters', 'gnome-logs',
184+ 'gnome-system-monitor']
185+ self._view.updateStatus(_("Checking for installed snaps"))
186+ for snap in snaps:
187+ snap_object = {}
188+ # check to see if the snap is already installed
189+ snap_info = subprocess.Popen(["snap", "info", snap],
190+ universal_newlines=True,
191+ stdout=subprocess.PIPE).communicate()
192+ self._view.processEvents()
193+ if re.search("^installed: ", snap_info[0], re.MULTILINE):
194+ logging.debug("Snap %s is installed" % snap)
195+ # its not tracking the release channel so don't refresh
196+ if not re.search("^tracking:.*ubuntu-%s" % self._from_version,
197+ snap_info[0], re.MULTILINE):
198+ logging.debug("Snap %s is not tracking the release channel"
199+ % snap)
200+ continue
201+ snap_object['command'] = 'refresh'
202+ else:
203+ match = re.search("snap-id:\s*(\w*)", snap_info[0])
204+ if not match:
205+ logging.debug("Could not parse snap-id for the %s snap"
206+ % snap)
207+ continue
208+ snap_object['command'] = 'install'
209+ snap_object['snap-id'] = match[1]
210+ self._snap_list[snap] = snap_object
211+ return self._snap_list
212
213=== modified file 'tests/test_quirks.py'
214--- tests/test_quirks.py 2018-08-31 22:46:45 +0000
215+++ tests/test_quirks.py 2019-08-30 14:54:30 +0000
216@@ -9,6 +9,7 @@
217 import unittest
218 import shutil
219 import tempfile
220+import json
221
222 from DistUpgrade.DistUpgradeQuirks import DistUpgradeQuirks
223
224@@ -24,6 +25,104 @@
225 pass
226
227
228+class MockPopenSnap():
229+ def __init__(self, cmd, universal_newlines=True,
230+ stdout=None):
231+ self.command = cmd
232+
233+ def communicate(self):
234+ snap_name = self.command[2]
235+ if (snap_name == 'gnome-logs' or
236+ snap_name == 'gnome-system-monitor'):
237+ return ["""
238+name: test-snap
239+summary: Test Snap
240+publisher: Canonical
241+license: unset
242+description: Some description
243+commands:
244+ - gnome-calculator
245+snap-id: 1234
246+tracking: stable/ubuntu-19.04
247+refresh-date: 2019-04-11
248+channels:
249+ stable: 3.32.1 2019-04-10 (406) 4MB -
250+ candidate: 3.32.2 2019-06-26 (433) 4MB -
251+ beta: 3.33.89 2019-08-06 (459) 4MB -
252+ edge: 3.33.90 2019-08-06 (460) 4MB -
253+installed: 3.32.1 (406) 4MB -
254+"""]
255+ else:
256+ return ["""
257+name: test-snap
258+summary: Test Snap
259+publisher: Canonical
260+license: unset
261+description: Some description
262+commands:
263+ - gnome-calculator
264+snap-id: 1234
265+refresh-date: 2019-04-11
266+channels:
267+ stable: 3.32.1 2019-04-10 (406) 4MB -
268+ candidate: 3.32.2 2019-06-26 (433) 4MB -
269+ beta: 3.33.89 2019-08-06 (459) 4MB -
270+ edge: 3.33.90 2019-08-06 (460) 4MB -
271+"""]
272+
273+
274+def mock_urlopen_snap(req):
275+ result = """{{
276+ "error-list": [],
277+ "results": [
278+ {{
279+ "effective-channel": "stable",
280+ "instance-key": "test",
281+ "name": "{name}",
282+ "released-at": "2019-04-10T18:54:15.717357+00:00",
283+ "result": "download",
284+ "snap": {{
285+ "created-at": "2019-04-09T17:09:29.941588+00:00",
286+ "download": {{
287+ "deltas": [],
288+ "size": {size},
289+ "url": "SNAPURL"
290+ }},
291+ "license": "GPL-3.0+",
292+ "name": "{name}",
293+ "prices": {{ }},
294+ "publisher": {{
295+ "display-name": "Canonical",
296+ "id": "canonical",
297+ "username": "canonical",
298+ "validation": "verified"
299+ }},
300+ "revision": 406,
301+ "snap-id": "{snap_id}",
302+ "summary": "GNOME Calculator",
303+ "title": "GNOME Calculator",
304+ "type": "app",
305+ "version": "3.32.1"
306+ }},
307+ "snap-id": "{snap_id}"
308+ }}
309+ ]
310+}}
311+"""
312+ test_snaps = {
313+ '1': ("gnome-calculator", 4218880),
314+ '2': ("test-snap", 2000000)
315+ }
316+ json_data = json.loads(req.data)
317+ snap_id = json_data['actions'][0]['snap-id']
318+ name = test_snaps[snap_id][0]
319+ size = test_snaps[snap_id][1]
320+ response_mock = mock.Mock()
321+ response_mock.read.return_value = result.format(
322+ name=name, snap_id=snap_id, size=size)
323+ return response_mock
324+
325+
326 def make_mock_pkg(name, is_installed, candidate_rec):
327 mock_pkg = mock.Mock()
328 mock_pkg.name = name
329@@ -229,6 +328,111 @@
330 self.assertEqual(pkgname, "linux-generic-lts-quantal")
331
332
333+class TestSnapQuirks(unittest.TestCase):
334+
335+ @mock.patch("subprocess.Popen", MockPopenSnap)
336+ def test_prepare_snap_replacement_data(self):
337+ # Prepare the state for testing
338+ controller = mock.Mock()
339+ controller.fromDist = 'disco'
340+ controller.toDist = 'eoan'
341+ config = mock.Mock()
342+ q = DistUpgradeQuirks(controller, config)
343+ # Call method under test
344+ q._prepare_snap_replacement_data()
345+ self.assertEqual(q._from_version, '19.04')
346+ self.assertEqual(q._to_version, '19.10')
347+ # Check if the right snaps have been detected as installed and
348+ # needing refresh and which ones need installation
349+ self.assertDictEqual(
350+ q._snap_list,
351+ {'core18': {'command': 'install', 'snap-id': '1234'},
352+ 'gnome-3-28-1804': {'command': 'install', 'snap-id': '1234'},
353+ 'gtk-common-themes': {'command': 'install', 'snap-id': '1234'},
354+ 'gnome-calculator': {'command': 'install', 'snap-id': '1234'},
355+ 'gnome-characters': {'command': 'install', 'snap-id': '1234'},
356+ 'gnome-logs': {'command': 'refresh'},
357+ 'gnome-system-monitor': {'command': 'refresh'}})
358+
359+ @mock.patch("DistUpgrade.DistUpgradeQuirks.get_arch")
360+ @mock.patch("urllib.request.urlopen")
361+ def test_calculate_snap_size_requirements(self, urlopen, arch):
362+ # Prepare the state for testing
363+ arch.return_value = 'amd64'
364+ controller = mock.Mock()
365+ config = mock.Mock()
366+ q = DistUpgradeQuirks(controller, config)
367+ # We mock out _prepare_snap_replacement_data(), as this is tested
368+ # separately.
369+ q._prepare_snap_replacement_data = mock.Mock()
370+ q._snap_list = {
371+ 'test-snap': {'command': 'install', 'snap-id': '2'},
372+ 'gnome-calculator': {'command': 'install', 'snap-id': '1'},
373+ 'gnome-system-monitor': {'command': 'refresh'}
374+ }
375+ q._to_version = "19.10"
376+ # Mock out urlopen in such a way that we get a mocked response based
377+ # on the parameters given but also allow us to check call arguments
378+ # etc.
379+ urlopen.side_effect = mock_urlopen_snap
380+ # Call method under test
381+ q._calculateSnapSizeRequirements()
382+ # Check if the size was calculated correctly
383+ self.assertEqual(q.extra_snap_space, 6218880)
384+ # Check if we only sent queries for the two command: install snaps
385+ self.assertEqual(urlopen.call_count, 2)
386+ # Make sure each call had the right headers and parameters
387+ for call in urlopen.call_args_list:
388+ req = call[0][0]
389+ self.assertIn(b"stable/ubuntu-19.10", req.data)
390+ self.assertDictEqual(
391+ req.headers,
392+ {'Snap-device-series': '16',
393+ 'Content-type': 'application/json',
394+ 'Snap-device-architecture': 'amd64'})
395+
396+ @mock.patch("subprocess.run")
397+ def test_replace_debs_with_snaps(self, run_mock):
398+ controller = mock.Mock()
399+ config = mock.Mock()
400+ q = DistUpgradeQuirks(controller, config)
401+ q._snap_list = {
402+ 'core18': {'command': 'install', 'snap-id': '1234'},
403+ 'gnome-3-28-1804': {'command': 'install', 'snap-id': '1234'},
404+ 'gtk-common-themes': {'command': 'install', 'snap-id': '1234'},
405+ 'gnome-calculator': {'command': 'install', 'snap-id': '1234'},
406+ 'gnome-characters': {'command': 'install', 'snap-id': '1234'},
407+ 'gnome-logs': {'command': 'refresh'},
408+ 'gnome-system-monitor': {'command': 'refresh'}
409+ }
410+ q._to_version = "19.10"
411+ q._replaceDebsWithSnaps()
412+ # Make sure all snaps have been handled
413+ self.assertEqual(run_mock.call_count, 7)
414+ snaps_refreshed = set()
415+ snaps_installed = set()
416+ # Check if all the snaps that needed to be installed were installed
417+ # and those that needed a refresh - refreshed
418+ for call in run_mock.call_args_list:
419+ args = call[0][0]
420+ if args[1] == 'install':
421+ snaps_installed.add(args[4])
422+ else:
423+ snaps_refreshed.add(args[4])
424+ self.assertSetEqual(
425+ snaps_refreshed,
426+ {'gnome-logs', 'gnome-system-monitor'})
427+ self.assertSetEqual(
428+ snaps_installed,
429+ {'core18', 'gnome-3-28-1804', 'gtk-common-themes',
430+ 'gnome-calculator', 'gnome-characters'})
431+ # Make sure we marked the replaced ones for removal
432+ # Here we only check if the right number of 'packages' has been
433+ # added to the forced_obsoletes list - not all of those packages are
434+ # actual deb packages that will have to be removed during the upgrade
435+ self.assertEqual(controller.forced_obsoletes.append.call_count, 5)
436+
437+
438 if __name__ == "__main__":
439 import logging
440 logging.basicConfig(level=logging.DEBUG)

Subscribers

People subscribed via source and target branches