From 5bc6343b23c030f70e80a86d2f563e09c2a9d674 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Thu, 14 Sep 2023 17:51:35 +0800 Subject: [PATCH 01/18] Revamp the kernel_snap_test.py script into something more generic This commit builds upon Patrick Liu's kernel_snap_test.py script (see commit 3b733018eebf77eba8885cbdb3b57b3c1cf96df9) and does the following in order to make it more generic, so it can be used with not only the kernel snap, but also snapd and gadget snaps: - rename the script `snap_update_test.py` and replace references to "kernel" with something more generic - extract some utility functions from the SnapRefreshRevert class (these functions can be executed without being part of that class) - use revision numbers instead of channels to refresh the snap to; this is because we want to take another use case into account: refreshing to/reverting from the base revision, which is the revision of the snap that is present on the image at install time - add the `guess_snaps()` function to guess the names of the kernel, gadget and snapd snaps - add the `get_snap_base_rev()` function to retrieve the base revision of a given snap from the system Fix CHECKBOX-717 --- providers/base/bin/kernel_snap_test.py | 254 ------------------- providers/base/bin/snap_update_test.py | 336 +++++++++++++++++++++++++ 2 files changed, 336 insertions(+), 254 deletions(-) delete mode 100755 providers/base/bin/kernel_snap_test.py create mode 100755 providers/base/bin/snap_update_test.py diff --git a/providers/base/bin/kernel_snap_test.py b/providers/base/bin/kernel_snap_test.py deleted file mode 100755 index e3e54b7f5..000000000 --- a/providers/base/bin/kernel_snap_test.py +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2023 Canonical Ltd. -# All rights reserved. -# -# Written by: -# Patrick Liu -# -# Checkbox is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3, -# as published by the Free Software Foundation. -# -# Checkbox is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Checkbox. If not, see . - -import argparse -import json -import logging -import sys -import time - -from checkbox_support.snap_utils.snapd import Snapd - - -class KernelSnapTest: - def __init__(self, info_path): - self.snapd = Snapd() - self.kernel_info = self.get_kernel_info() - self.path = info_path - - def get_kernel_info(self): - kernel_info = {} - installed_snaps = self.snapd.list() - for item in installed_snaps: - if item["type"] == "kernel": - kernel_info["name"] = item["name"] - kernel_info["tracking_channel"] = item["tracking-channel"] - kernel_info["installed_revision"] = item["revision"] - tracking = kernel_info["tracking_channel"] - prefix = (tracking.split("/")[0] + "/") if "/" in tracking else "" - kernel_info["tracking_prefix"] = prefix - - snap_info = self.snapd.find(kernel_info["name"], exact=True) - kernel_info["revisions"] = {} - for item in snap_info: - for channel, info in item["channels"].items(): - kernel_info["revisions"][channel] = info["revision"] - return kernel_info - - def kernel_refresh(self): - data = {} - original_revision = self.kernel_info["installed_revision"] - data["original_revision"] = original_revision - channel = "{}stable".format(self.kernel_info["tracking_prefix"]) - stable_rev = self.kernel_info["revisions"].get(channel, "") - logging.info( - "Refreshing kernel snap to stable (from rev %s to rev %s)", - original_revision, - stable_rev, - ) - r = self.snapd.refresh( - self.kernel_info["name"], channel=channel, reboot=True - ) - logging.info("Refreshing requested") - with open(self.path, "w") as file: - data["refresh_id"] = r["change"] - json.dump(data, file) - logging.info("Waiting for reboot...") - - def verify_refresh(self): - with open(self.path, "r") as file: - data = json.load(file) - id = data["refresh_id"] - - logging.info("Checking kernel refresh status") - start_time = time.time() - timeout = 300 # 5 minutes timeout - while True: - result = self.snapd.change(str(id)) - if result == "Done": - logging.info("Kernel refresh is complete") - break - - if time.time() - start_time >= timeout: - logging.error( - "Kernel refresh did not complete within 5 minutes" - ) - return False - logging.info( - "Waiting for kernel refreshing to be done..." - "trying again in 10 seconds" - ) - time.sleep(10) - - current_rev = self.snapd.list(self.kernel_info["name"])["revision"] - channel = "{}stable".format(self.kernel_info["tracking_prefix"]) - stable_rev = self.kernel_info["revisions"][channel] - if current_rev != stable_rev: - logging.error( - "Current revision %s is NOT equal to stable revision %s", - current_rev, - stable_rev, - ) - return False - else: - logging.info( - "PASS: current revision matches the stable channel revision" - ) - return True - - def kernel_revert(self): - with open(self.path, "r") as file: - data = json.load(file) - original_rev = data["original_revision"] - channel = "{}stable".format(self.kernel_info["tracking_prefix"]) - stable_rev = self.kernel_info["revisions"].get(channel, "") - logging.info( - "Reverting kernel snap (from rev %s to rev %s)", - stable_rev, - original_rev, - ) - r = self.snapd.revert(self.kernel_info["name"], reboot=True) - logging.info("Reverting requested") - with open(self.path, "w") as file: - data["revert_id"] = r["change"] - json.dump(data, file) - logging.info("Waiting for reboot...") - - def verify_revert(self): - with open(self.path, "r") as file: - data = json.load(file) - id = data["revert_id"] - original_rev = data["original_revision"] - - logging.info("Checking kernel revert status") - start_time = time.time() - timeout = 300 # 5 minutes timeout - while True: - result = self.snapd.change(str(id)) - if result == "Done": - logging.info("Kernel revert is complete") - break - - if time.time() - start_time >= timeout: - logging.error( - "Kernel revert did not complete within 5 minutes" - ) - return False - logging.info( - "Waiting for kernel reverting to be done..." - "trying again in 10 seconds" - ) - time.sleep(10) - - current_rev = self.snapd.list(self.kernel_info["name"])["revision"] - if current_rev != original_rev: - logging.error( - "Current revision %s is NOT equal to original revision %s", - current_rev, - original_rev, - ) - return False - else: - logging.info( - "PASS: current revision matches the original revision" - ) - return True - - def print_resource_info(self): - info = self.get_kernel_info() - tracking = info["tracking_channel"] - - prefix = self.kernel_info["tracking_prefix"] - stable_rev = info["revisions"].get("{}stable".format(prefix), "") - cand_rev = info["revisions"].get("{}candidate".format(prefix), "") - beta_rev = info["revisions"].get("{}beta".format(prefix), "") - edge_rev = info["revisions"].get("{}edge".format(prefix), "") - installed_rev = info.get("installed_revision", "") - - print("kernel_name: {}".format(info["name"])) - print("tracking: {}".format(tracking)) - print("stable_rev: {}".format(stable_rev)) - print("candidate_rev: {}".format(cand_rev)) - print("beta_rev: {}".format(beta_rev)) - print("edge_rev: {}".format(edge_rev)) - print("original_installed_rev: {}".format(installed_rev)) - - -def main(): - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(levelname)-8s %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - parser = argparse.ArgumentParser() - parser.add_argument( - "--resource", - action="store_true", - help="Refresh kernel snap", - ) - parser.add_argument( - "--refresh", - action="store_true", - help="Refresh kernel snap", - ) - parser.add_argument( - "--verify-refresh", - action="store_true", - help="Verify revision after refreshing kernel", - ) - parser.add_argument( - "--revert", - action="store_true", - help="Revert kernel snap", - ) - parser.add_argument( - "--verify-revert", - action="store_true", - help="Verify revision after reverting kernel", - ) - parser.add_argument( - "--info-path", - help="Path to the information file", - ) - - args = parser.parse_args() - info_path = args.info_path - test = KernelSnapTest(info_path) - - exit_code = 0 - if args.resource: - test.print_resource_info() - if args.refresh: - if not test.kernel_refresh(): - exit_code = 1 - if args.verify_refresh: - if not test.verify_refresh(): - exit_code = 1 - if args.revert: - if not test.kernel_revert(): - exit_code = 1 - if args.verify_revert: - if not test.verify_revert(): - exit_code = 1 - - return exit_code - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/providers/base/bin/snap_update_test.py b/providers/base/bin/snap_update_test.py new file mode 100755 index 000000000..22ec90f41 --- /dev/null +++ b/providers/base/bin/snap_update_test.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# All rights reserved. +# +# Written by: +# Patrick Liu +# Pierre Equoy +# +# Checkbox is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, +# as published by the Free Software Foundation. +# +# Checkbox is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Checkbox. If not, see . + +import argparse +from glob import glob +import json +import logging +import os.path +import sys +import time + +from checkbox_support.snap_utils.snapd import Snapd + + +def guess_snaps() -> dict: + """ + Guess the names of the kernel, snapd and gadget snaps from installed snaps + on the system. + + :return: a dict with the snap names for each snap found + :rtype: dict + """ + snapd = Snapd() + installed_snaps = snapd.list() + snaps = {} + for snap in installed_snaps: + if snap["type"] == "kernel": + snaps["kernel"] = snap["name"] + elif snap["type"] == "gadget": + snaps["gadget"] = snap["name"] + elif snap["type"] == "snapd": + snaps["snapd"] = snap["name"] + return snaps + + +def get_snap_base_rev() -> dict: + """ + Retrieve the name and the base revision of each snap originally installed + on the system. + + :return: a dict containing the snap names and their base revisions + :rtype: dict + """ + base_snaps = glob("/var/lib/snapd/seed/snaps/*.snap") + base_rev_info = {} + for snap_path in base_snaps: + snap_basename = os.path.basename(snap_path) + snap_name = os.path.splitext(snap_basename)[0] + snap, rev = snap_name.rsplit("_", maxsplit=1) + base_rev_info[snap] = rev + return base_rev_info + + +def get_snap_info(name) -> dict: + """ + Retrieve information such as name, type, available revisions, etc. about + a given snap. + + :return: a dict with the available information + :rtype: dict + """ + snapd = Snapd() + snap_info = {} + snap = snapd.list(name) + base_revs = get_snap_base_rev() + snap_info["name"] = snap["name"] + snap_info["type"] = snap["type"] + snap_info["tracking_channel"] = snap["tracking-channel"] + snap_info["installed_revision"] = snap["revision"] + snap_info["base_revision"] = base_revs.get(name, "") + tracking = snap_info["tracking_channel"] + prefix = (tracking.split("/")[0] + "/") if "/" in tracking else "" + snap_info["tracking_prefix"] = prefix + + snap_additional_info = snapd.find(name, exact=True) + snap_info["revisions"] = {} + for item in snap_additional_info: + for channel, info in item["channels"].items(): + snap_info["revisions"][channel] = info["revision"] + return snap_info + + +def print_resource_info(): + snaps = guess_snaps().values() + for snap in snaps: + info = get_snap_info(snap) + tracking = info["tracking_channel"] + prefix = info["tracking_prefix"] + base_rev = info.get("base_revision", "") + stable_rev = info["revisions"].get("{}stable".format(prefix), "") + cand_rev = info["revisions"].get("{}candidate".format(prefix), "") + beta_rev = info["revisions"].get("{}beta".format(prefix), "") + edge_rev = info["revisions"].get("{}edge".format(prefix), "") + installed_rev = info.get("installed_revision", "") + + print("name: {}".format(info["name"])) + print("type: {}".format(info["type"])) + print("tracking: {}".format(tracking)) + print("base_rev: {}".format(base_rev)) + print("stable_rev: {}".format(stable_rev)) + print("candidate_rev: {}".format(cand_rev)) + print("beta_rev: {}".format(beta_rev)) + print("edge_rev: {}".format(edge_rev)) + print("original_installed_rev: {}".format(installed_rev)) + print() + + +class SnapRefreshRevert: + def __init__(self, name, rev, info_path): + self.snapd = Snapd() + self.snap_info = get_snap_info(name) + self.path = info_path + self.rev = rev + self.name = name + + def snap_refresh(self): + data = {} + original_revision = self.snap_info["installed_revision"] + if original_revision == self.rev: + logging.error( + "Trying to refresh to the same revision (%s)!", self.rev + ) + return 1 + data["name"] = self.name + data["original_revision"] = original_revision + data["destination_revision"] = self.rev + logging.info( + "Refreshing %s snap from rev %s to rev %s", + self.name, + original_revision, + self.rev, + ) + r = self.snapd.refresh( + self.name, + channel=self.snap_info["tracking_channel"], + revision=self.rev, + reboot=True, + ) + logging.info( + "Refreshing requested (channel %s, rev %s)", + self.snap_info["tracking_channel"], + self.rev, + ) + with open(self.path, "w") as file: + data["refresh_id"] = r["change"] + json.dump(data, file) + logging.info("Waiting for reboot...") + return 0 + + def verify_refresh(self): + try: + with open(self.path, "r") as file: + data = json.load(file) + except FileNotFoundError: + logging.error("File not found: %s", self.path) + logging.error("Did the previous job run as expected?") + return 1 + id = data["refresh_id"] + name = data["name"] + + logging.info("Checking refresh status for snap %s...", name) + start_time = time.time() + timeout = 300 # 5 minutes timeout + while True: + result = self.snapd.change(str(id)) + if result == "Done": + logging.info("%s snap refresh complete", name) + break + + if time.time() - start_time >= timeout: + logging.error( + "%s snap refresh did not complete within 5 minutes", name + ) + return False + logging.info("Waiting for %s snap refreshing to be done...", name) + logging.info("Trying again in 10 seconds...") + time.sleep(10) + + current_rev = self.snapd.list(self.snap_info["name"])["revision"] + destination_rev = data["destination_revision"] + if current_rev != destination_rev: + logging.error( + "Current revision %s is NOT equal to expected revision %s", + current_rev, + destination_rev, + ) + return 1 + else: + logging.info( + "PASS: current revision (%s) matches the expected revision", + current_rev, + ) + return 0 + + def snap_revert(self): + with open(self.path, "r") as file: + data = json.load(file) + original_rev = data["original_revision"] + destination_rev = data["destination_revision"] + logging.info( + "Reverting %s snap (from rev %s to rev %s)", + self.name, + destination_rev, + original_rev, + ) + r = self.snapd.revert(self.snap_info["name"], reboot=True) + logging.info("Reverting requested") + with open(self.path, "w") as file: + data["revert_id"] = r["change"] + json.dump(data, file) + logging.info("Waiting for reboot...") + + def verify_revert(self): + with open(self.path, "r") as file: + data = json.load(file) + id = data["revert_id"] + original_rev = data["original_revision"] + + logging.info("Checking %s snap revert status", self.name) + start_time = time.time() + timeout = 300 # 5 minutes timeout + while True: + result = self.snapd.change(str(id)) + if result == "Done": + logging.info("%s snap revert complete", self.name) + break + + if time.time() - start_time >= timeout: + logging.error( + "%s snap revert did not complete within 5 minutes", + self.name, + ) + return False + logging.info( + "Waiting for %s snap reverting to be done...", self.name + ) + logging.info("Trying again in 10 seconds.") + time.sleep(10) + + current_rev = self.snapd.list(self.snap_info["name"])["revision"] + if current_rev != original_rev: + logging.error( + "Current revision (%s) is NOT equal to original revision (%s)", + current_rev, + original_rev, + ) + return 1 + else: + logging.info( + "PASS: current revision (%s) matches the original revision", + current_rev, + ) + return 0 + + +def main(): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + parser = argparse.ArgumentParser() + parser.add_argument( + "name", nargs="?", default="", help="Name of the snap to act upon" + ) + parser.add_argument( + "--resource", + action="store_true", + help="Gather information about kernel, snapd and gadget snaps", + ) + parser.add_argument( + "--refresh", + action="store_true", + help="Refresh the given snap", + ) + parser.add_argument( + "--verify-refresh", + action="store_true", + help="Verify revision after refreshing the given snap", + ) + parser.add_argument( + "--revert", + action="store_true", + help="Revert the given snap", + ) + parser.add_argument( + "--verify-revert", + action="store_true", + help="Verify revision after reverting the given snap", + ) + parser.add_argument( + "--info-path", + help="Path to the information file", + ) + parser.add_argument( + "--rev", + help="Revision to refresh to", + ) + + args = parser.parse_args() + + if args.resource: + print_resource_info() + else: + test = SnapRefreshRevert( + name=args.name, info_path=args.info_path, rev=args.rev + ) + if args.refresh: + return test.snap_refresh() + if args.verify_refresh: + return test.verify_refresh() + if args.revert: + return test.snap_revert() + if args.verify_revert: + return test.verify_revert() + + +if __name__ == "__main__": + sys.exit(main()) From c823e2140c23205ca4467e8322815d0430dd005c Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Thu, 14 Sep 2023 17:52:01 +0800 Subject: [PATCH 02/18] Add unit tests for the snap_update_test.py script --- providers/base/tests/test_snap_update_test.py | 713 ++++++++++++++++++ 1 file changed, 713 insertions(+) create mode 100644 providers/base/tests/test_snap_update_test.py diff --git a/providers/base/tests/test_snap_update_test.py b/providers/base/tests/test_snap_update_test.py new file mode 100644 index 000000000..ca8ba7489 --- /dev/null +++ b/providers/base/tests/test_snap_update_test.py @@ -0,0 +1,713 @@ +import io +import logging +import unittest +from unittest import mock + +import snap_update_test + + +snapd_list_sample = [ + { + "channel": "22/stable", + "confinement": "strict", + "contact": "", + "description": "The Ubuntu linux-raspi kernel package as a snap.\n" + "\n" + "This snap supports the Pi 2, 3 and 4. It is provided for " + "both armhf and arm64 architectures.", + "developer": "canonical", + "devmode": False, + "id": "jeIuP6tfFrvAdic8DMWqHmoaoukAPNbJ", + "ignore-validation": False, + "install-date": "2023-09-04T17:45:02.067900703Z", + "installed-size": 98791424, + "jailmode": False, + "links": None, + "mounted-from": "/var/lib/snapd/snaps/pi-kernel_663.snap", + "name": "pi-kernel", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "663", + "status": "active", + "summary": "The Ubuntu Raspberry Pi kernel", + "title": "pi-kernel", + "tracking-channel": "22/stable", + "type": "kernel", + "version": "5.15.0-1036.39", + }, + { + "channel": "latest/stable", + "confinement": "strict", + "contact": "https://github.com/snapcore/snapd/issues", + "description": "Install, configure, refresh and remove snap packages. Snaps " + "are\n" + "'universal' packages that work across many different Linux " + "systems,\n" + "enabling secure distribution of the latest apps and " + "utilities for\n" + "cloud, servers, desktops and the internet of things.\n" + "\n" + "Start with 'snap list' to see installed snaps.", + "developer": "canonical", + "devmode": False, + "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/snapd.png", + "id": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4", + "ignore-validation": False, + "install-date": "2023-09-06T18:37:40.283181077Z", + "installed-size": 37236736, + "jailmode": False, + "license": "GPL-3.0", + "links": { + "contact": ["https://github.com/snapcore/snapd/issues"], + "website": ["https://snapcraft.io"], + }, + "media": [ + { + "height": 460, + "type": "icon", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/snapd.png", + "width": 460, + }, + { + "height": 648, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_115756_hLcyetO.png", + "width": 956, + }, + { + "height": 648, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_115824_2v3y6l8.png", + "width": 956, + }, + { + "height": 834, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_115055_Uuq7KIb.png", + "width": 1023, + }, + { + "height": 648, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_125944.png", + "width": 956, + }, + ], + "mounted-from": "/var/lib/snapd/snaps/snapd_20102.snap", + "name": "snapd", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "20102", + "status": "active", + "summary": "Daemon and tooling that enable snap packages", + "title": "snapd", + "tracking-channel": "latest/stable", + "type": "snapd", + "version": "2.60.3", + "website": "https://snapcraft.io", + }, + { + "apps": [ + {"name": "checkbox-cli", "snap": "checkbox"}, + {"name": "client-cert-iot-ubuntucore", "snap": "checkbox"}, + {"name": "configure", "snap": "checkbox"}, + {"name": "odm-certification", "snap": "checkbox"}, + { + "active": True, + "daemon": "simple", + "daemon-scope": "system", + "enabled": True, + "name": "service", + "snap": "checkbox", + }, + {"name": "shell", "snap": "checkbox"}, + {"name": "sru", "snap": "checkbox"}, + {"name": "test-runner", "snap": "checkbox"}, + ], + "base": "core22", + "channel": "uc22/beta", + "confinement": "strict", + "contact": "", + "description": "Checkbox is a flexible test automation software.\n" + "It’s the main tool used in Ubuntu Certification program.\n", + "developer": "ce-certification-qa", + "devmode": True, + "id": "06zlGRiJvdhJMO5NFVNjIOcZ1g3m8yVb", + "ignore-validation": False, + "install-date": "2023-09-04T09:02:32.253731099Z", + "installed-size": 12288, + "jailmode": False, + "links": None, + "mounted-from": "/var/lib/snapd/snaps/checkbox_2784.snap", + "name": "checkbox", + "private": False, + "publisher": { + "display-name": "Canonical Certification Team", + "id": "Euf8YO6waprpTXVrREuDw8ODHNIACTwi", + "username": "ce-certification-qa", + "validation": "unproven", + }, + "revision": "2784", + "status": "active", + "summary": "Checkbox test runner", + "title": "checkbox", + "tracking-channel": "uc22/beta", + "type": "app", + "version": "2.9.1", + }, + { + "base": "core22", + "channel": "latest/edge", + "confinement": "strict", + "contact": "", + "description": "Checkbox runtime and public providers", + "developer": "ce-certification-qa", + "devmode": False, + "id": "jUzbHhPAz1sQPzVD24Nky8UTbuKZ0gpR", + "ignore-validation": False, + "install-date": "2023-09-07T01:08:22.885438702Z", + "installed-size": 168214528, + "jailmode": False, + "links": None, + "mounted-from": "/var/lib/snapd/snaps/checkbox22_518.snap", + "name": "checkbox22", + "private": False, + "publisher": { + "display-name": "Canonical Certification Team", + "id": "Euf8YO6waprpTXVrREuDw8ODHNIACTwi", + "username": "ce-certification-qa", + "validation": "unproven", + }, + "revision": "518", + "status": "active", + "summary": "Checkbox runtime and public providers", + "title": "checkbox22", + "tracking-channel": "latest/edge", + "type": "app", + "version": "2.9.2.dev18+gd294291ef", + }, + { + "channel": "latest/stable", + "confinement": "strict", + "contact": "https://github.com/snapcore/core-base/issues", + "description": "The base snap based on the Ubuntu 22.04 release.", + "developer": "canonical", + "devmode": False, + "id": "amcUKQILKXHHTlmSa7NMdnXSx02dNeeT", + "ignore-validation": False, + "install-date": "2023-08-31T06:06:59.522300877Z", + "installed-size": 71860224, + "jailmode": False, + "links": {"contact": ["https://github.com/snapcore/core-base/issues"]}, + "mounted-from": "/var/lib/snapd/snaps/core22_867.snap", + "name": "core22", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "867", + "status": "active", + "summary": "Runtime environment based on Ubuntu 22.04", + "title": "core22", + "tracking-channel": "latest/stable", + "type": "base", + "version": "20230801", + }, + { + "base": "core22", + "channel": "", + "confinement": "strict", + "contact": "", + "description": "Support files for booting Raspberry Pi.\n" + "This gadget snap supports the Raspberry Pi 2B, 3B, 3A+, 3B+, " + "4B, Compute\n" + "Module 3, and the Compute Module 3+ universally.\n", + "developer": "canonical", + "devmode": False, + "icon": "/v2/icons/pi/icon", + "id": "YbGa9O3dAXl88YLI6Y1bGG74pwBxZyKg", + "ignore-validation": False, + "install-date": "2023-08-31T06:05:21.054039971Z", + "installed-size": 11612160, + "jailmode": False, + "links": None, + "mounted-from": "/var/lib/snapd/snaps/pi_132.snap", + "name": "pi", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "132", + "status": "active", + "summary": "Raspberry Pi gadget", + "tracking-channel": "22/stable", + "type": "gadget", + "version": "22-2", + }, +] + + +class SnapUpdateTests(unittest.TestCase): + @mock.patch("snap_update_test.Snapd.list") + def test_guess_snaps(self, mock_snapd_list): + mock_snapd_list.return_value = snapd_list_sample + snaps = snap_update_test.guess_snaps() + expected_snaps = {"kernel": "pi-kernel", "snapd": "snapd", "gadget": "pi"} + self.assertEqual(snaps, expected_snaps) + + @mock.patch("snap_update_test.Snapd.list") + def test_guess_snaps_nothing(self, mock_snapd_list): + mock_snapd_list.return_value = [ + { + "channel": "latest/stable", + "confinement": "strict", + "contact": "https://github.com/snapcore/core-base/issues", + "description": "The base snap based on the Ubuntu 22.04 release.", + "developer": "canonical", + "devmode": False, + "id": "amcUKQILKXHHTlmSa7NMdnXSx02dNeeT", + "ignore-validation": False, + "install-date": "2023-08-30T08:38:36.555233022Z", + "installed-size": 77492224, + "jailmode": False, + "links": {"contact": ["https://github.com/snapcore/core-base/issues"]}, + "mounted-from": "/var/lib/snapd/snaps/core22_864.snap", + "name": "core22", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "864", + "status": "active", + "summary": "Runtime environment based on Ubuntu 22.04", + "title": "core22", + "tracking-channel": "latest/stable", + "type": "base", + "version": "20230801", + }, + ] + snaps = snap_update_test.guess_snaps() + self.assertEqual(snaps, {}) + + @mock.patch("snap_update_test.glob") + def test_get_snap_base_rev(self, mock_glob): + mock_glob.return_value = [ + "/var/lib/snapd/seed/snaps/firefox_2605.snap", + "/var/lib/snapd/seed/snaps/snapd_19267.snap", + "/var/lib/snapd/seed/snaps/pc-kernel_1289.snap", + "/var/lib/snapd/seed/snaps/core22_617.snap", + ] + snap_rev = snap_update_test.get_snap_base_rev() + self.assertEqual(len(snap_rev), 4) + self.assertEqual(snap_rev["pc-kernel"], "1289") + + @mock.patch("snap_update_test.get_snap_base_rev") + @mock.patch("snap_update_test.Snapd.list") + @mock.patch("snap_update_test.Snapd.find") + def test_get_snap_info(self, mock_snapd_find, mock_snapd_list, mock_base_revs): + mock_base_revs.return_value = {"firefox": "2605"} + mock_snapd_list.return_value = { + "apps": [ + { + "desktop-file": "/var/lib/snapd/desktop/applications/firefox_firefox.desktop", + "name": "firefox", + "snap": "firefox", + }, + {"name": "geckodriver", "snap": "firefox"}, + ], + "base": "core20", + "channel": "latest/stable", + "confinement": "strict", + "contact": "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla", + "description": "Firefox is a powerful, extensible web browser with support " + "for modern web application technologies.", + "developer": "mozilla", + "devmode": False, + "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", + "id": "3wdHCAVyZEmYsCMFDE9qt92UV8rC8Wdk", + "ignore-validation": False, + "install-date": "2023-08-25T08:35:06.453124352+08:00", + "installed-size": 248733696, + "jailmode": False, + "links": { + "contact": [ + "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla" + ], + "website": ["https://www.mozilla.org/firefox/"], + }, + "media": [ + { + "height": 196, + "type": "icon", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", + "width": 196, + }, + { + "height": 1415, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/09/Screenshot_from_2021-09-30_08-01-50.png", + "width": 1850, + }, + ], + "mounted-from": "/var/lib/snapd/snaps/firefox_3026.snap", + "name": "firefox", + "private": False, + "publisher": { + "display-name": "Mozilla", + "id": "OgeoZuqQpVvSr9eGKJzNCrFGSaKXpkey", + "username": "mozilla", + "validation": "verified", + }, + "revision": "3026", + "status": "active", + "summary": "Mozilla Firefox web browser", + "title": "firefox", + "tracking-channel": "latest/stable", + "type": "app", + "version": "116.0.3-2", + "website": "https://www.mozilla.org/firefox/", + } + + mock_snapd_find.return_value = [ + { + "base": "core22", + "categories": [{"featured": True, "name": "productivity"}], + "channel": "stable", + "channels": { + "esr/candidate": { + "channel": "esr/candidate", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-08-21T18:15:17.435529Z", + "revision": "3052", + "size": 253628416, + "version": "115.2.0esr-1", + }, + "esr/stable": { + "channel": "esr/stable", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-08-29T12:37:31.563045Z", + "revision": "3052", + "size": 253628416, + "version": "115.2.0esr-1", + }, + "latest/beta": { + "channel": "latest/beta", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-09-04T01:41:28.490375Z", + "revision": "3099", + "size": 251957248, + "version": "118.0b4-1", + }, + "latest/candidate": { + "channel": "latest/candidate", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-08-24T23:00:15.702917Z", + "revision": "3068", + "size": 248352768, + "version": "117.0-2", + }, + "latest/edge": { + "channel": "latest/edge", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-09-04T03:53:25.23937Z", + "revision": "3102", + "size": 269561856, + "version": "119.0a1", + }, + "latest/stable": { + "channel": "latest/stable", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-08-29T12:37:04.958128Z", + "revision": "3068", + "size": 248352768, + "version": "117.0-2", + }, + }, + "confinement": "strict", + "contact": "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla", + "description": "Firefox is a powerful, extensible web browser with support " + "for modern web application technologies.", + "developer": "mozilla", + "devmode": False, + "download-size": 248352768, + "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", + "id": "3wdHCAVyZEmYsCMFDE9qt92UV8rC8Wdk", + "ignore-validation": False, + "jailmode": False, + "license": "MPL-2.0", + "links": { + "contact": [ + "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla" + ], + "website": ["https://www.mozilla.org/firefox/"], + }, + "media": [ + { + "height": 196, + "type": "icon", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", + "width": 196, + }, + { + "height": 1415, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/09/Screenshot_from_2021-09-30_08-01-50.png", + "width": 1850, + }, + ], + "name": "firefox", + "private": False, + "publisher": { + "display-name": "Mozilla", + "id": "OgeoZuqQpVvSr9eGKJzNCrFGSaKXpkey", + "username": "mozilla", + "validation": "verified", + }, + "revision": "3068", + "status": "available", + "store-url": "https://snapcraft.io/firefox", + "summary": "Mozilla Firefox web browser", + "title": "firefox", + "tracks": ["latest", "esr"], + "type": "app", + "version": "117.0-2", + "website": "https://www.mozilla.org/firefox/", + } + ] + + expected_snap_info = { + "installed_revision": "3026", + "base_revision": "2605", + "name": "firefox", + "type": "app", + "revisions": { + "esr/candidate": "3052", + "esr/stable": "3052", + "latest/beta": "3099", + "latest/candidate": "3068", + "latest/edge": "3102", + "latest/stable": "3068", + }, + "tracking_channel": "latest/stable", + "tracking_prefix": "latest/", + } + + snap_info = snap_update_test.get_snap_info("firefox") + self.assertEqual(snap_info, expected_snap_info) + + @mock.patch("snap_update_test.get_snap_info") + @mock.patch("sys.stdout", new_callable=io.StringIO) + def test_print_resource_info(self, mock_stdout, mock_snap_info): + mock_snap_info.return_value = { + "installed_revision": "567", + "base_revision": "567", + "name": "pi-kernel", + "type": "kernel", + "revisions": { + "18-cm3/beta": "649", + "18-cm3/edge": "649", + "18-pi/beta": "667", + "18-pi/candidate": "667", + "18-pi/edge": "667", + "18-pi/stable": "662", + "18-pi2/beta": "649", + "18-pi2/edge": "649", + "18-pi3/beta": "649", + "18-pi3/candidate": "649", + "18-pi3/edge": "649", + "18-pi3/stable": "649", + "18-pi4/beta": "77", + "18-pi4/candidate": "77", + "18-pi4/edge": "77", + "20/beta": "666", + "20/candidate": "661", + "20/edge": "666", + "20/stable": "661", + "22/beta": "663", + "22/candidate": "663", + "22/edge": "663", + "22/stable": "658", + "latest/candidate": "542", + }, + "tracking_channel": "22/stable", + "tracking_prefix": "22/", + } + expected_output = "name: pi-kernel\ntype: kernel\ntracking: 22/stable\nbase_rev: 567\nstable_rev: 658\ncandidate_rev: 663\nbeta_rev: 663\nedge_rev: 663\noriginal_installed_rev: 567\n\n" + snap_update_test.print_resource_info() + self.assertEqual(mock_stdout.getvalue(), expected_output) + + +class SnapRefreshRevertTests(unittest.TestCase): + @mock.patch("snap_update_test.Snapd") + @mock.patch("snap_update_test.get_snap_info") + def test_snap_refresh_same_revision(self, mock_snap_info, mock_snapd): + mock_snap_info.return_value = {"installed_revision": "132"} + srr = snap_update_test.SnapRefreshRevert( + name="test", rev="132", info_path="/test/info" + ) + logging.disable(logging.CRITICAL) + self.assertEqual(srr.snap_refresh(), 1) + + @mock.patch("builtins.open", new_callable=mock.mock_open()) + @mock.patch("snap_update_test.Snapd.refresh") + @mock.patch("snap_update_test.get_snap_info") + def test_snap_refresh_different_revision( + self, mock_snap_info, mock_snapd_refresh, mock_file + ): + mock_snap_info.return_value = { + "installed_revision": "132", + "tracking_channel": "22/beta", + } + mock_snapd_refresh.return_value = {"change": "1"} + srr = snap_update_test.SnapRefreshRevert( + name="test", rev="137", info_path="/test/info" + ) + self.assertEqual(srr.snap_refresh(), 0) + + @mock.patch("builtins.open", new_callable=mock.mock_open()) + @mock.patch("snap_update_test.Snapd.list") + @mock.patch("snap_update_test.Snapd.change") + @mock.patch("snap_update_test.json.load") + @mock.patch("snap_update_test.get_snap_info") + def test_verify_refresh_ok( + self, + mock_snap_info, + mock_json_load, + mock_snapd_change, + mock_snapd_list, + mock_file, + ): + mock_snap_info.return_value = { + "name": "test-snap", + "installed_revision": "132", + "tracking_channel": "22/beta", + } + mock_json_load.return_value = { + "refresh_id": "1", + "name": "test-snap", + "destination_revision": "2", + } + mock_snapd_change.return_value = "Done" + mock_snapd_list.return_value = {"revision": "2"} + srr = snap_update_test.SnapRefreshRevert( + name="test-snap", rev="2", info_path="/test/info" + ) + self.assertEqual(srr.verify_refresh(), 0) + + @mock.patch("builtins.open", new_callable=mock.mock_open()) + @mock.patch("snap_update_test.Snapd.list") + @mock.patch("snap_update_test.Snapd.change") + @mock.patch("snap_update_test.json.load") + @mock.patch("snap_update_test.get_snap_info") + def test_verify_refresh_nok( + self, + mock_snap_info, + mock_json_load, + mock_snapd_change, + mock_snapd_list, + mock_file, + ): + mock_snap_info.return_value = { + "name": "test-snap", + "installed_revision": "132", + "tracking_channel": "22/beta", + } + mock_json_load.return_value = { + "refresh_id": "1", + "name": "test-snap", + "destination_revision": "2", + } + mock_snapd_change.return_value = "Done" + mock_snapd_list.return_value = {"revision": "1"} + srr = snap_update_test.SnapRefreshRevert( + name="test-snap", rev="2", info_path="/test/info" + ) + + logging.disable(logging.CRITICAL) + self.assertEqual(srr.verify_refresh(), 1) + + @mock.patch("builtins.open", new_callable=mock.mock_open()) + @mock.patch("snap_update_test.Snapd.list") + @mock.patch("snap_update_test.Snapd.change") + @mock.patch("snap_update_test.json.load") + @mock.patch("snap_update_test.get_snap_info") + def test_verify_revert_ok( + self, + mock_snap_info, + mock_json_load, + mock_snapd_change, + mock_snapd_list, + mock_file, + ): + mock_snap_info.return_value = { + "name": "test-snap", + "installed_revision": "132", + "tracking_channel": "22/beta", + } + mock_json_load.return_value = { + "revert_id": "1", + "name": "test-snap", + "original_revision": "2", + } + mock_snapd_change.return_value = "Done" + mock_snapd_list.return_value = {"revision": "2"} + srr = snap_update_test.SnapRefreshRevert( + name="test-snap", rev="2", info_path="/test/info" + ) + self.assertEqual(srr.verify_revert(), 0) + + @mock.patch("builtins.open", new_callable=mock.mock_open()) + @mock.patch("snap_update_test.Snapd.list") + @mock.patch("snap_update_test.Snapd.change") + @mock.patch("snap_update_test.json.load") + @mock.patch("snap_update_test.get_snap_info") + def test_verify_revert_nok( + self, + mock_snap_info, + mock_json_load, + mock_snapd_change, + mock_snapd_list, + mock_file, + ): + mock_snap_info.return_value = { + "name": "test-snap", + "installed_revision": "132", + "tracking_channel": "22/beta", + } + mock_json_load.return_value = { + "revert_id": "1", + "name": "test-snap", + "original_revision": "2", + } + mock_snapd_change.return_value = "Done" + mock_snapd_list.return_value = {"revision": "1"} + srr = snap_update_test.SnapRefreshRevert( + name="test-snap", rev="2", info_path="/test/info" + ) + logging.disable(logging.CRITICAL) + self.assertEqual(srr.verify_revert(), 1) From 5a9a73f407633af6792a8c9758d810044e4e5e54 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Thu, 14 Sep 2023 18:20:23 +0800 Subject: [PATCH 03/18] Revamp kernel/gadget/snapd refresh/revert tests - Turn existing kernel-only jobs into generic ones, using the snap_revision_info resource job as a template for the naming - Add jobs to refresh to/revert from base revision - Separate the reboot section into its own job: because the noreturn/autorestart flags have a big impact on Checkbox, it's better to isolate these jobs to the smallest possible action (in this case, a reboot). This also helps with keeping stdout/stderr to avoid issue [1] https://github.com/canonical/checkbox/issues/694 --- providers/base/units/ubuntucore/jobs.pxu | 183 +++++++++++++++++++---- 1 file changed, 152 insertions(+), 31 deletions(-) diff --git a/providers/base/units/ubuntucore/jobs.pxu b/providers/base/units/ubuntucore/jobs.pxu index fc17cecc1..0ce14aad1 100644 --- a/providers/base/units/ubuntucore/jobs.pxu +++ b/providers/base/units/ubuntucore/jobs.pxu @@ -153,80 +153,201 @@ _verification: "failboot" kernel category_id: ubuntucore -id: kernel_revision_info +id: snap_revision_info plugin: resource -_summary: Gather the name, tracking, and revision info of the kernel snap +_summary: + Gather name, tracking, and revision info of the kernel, snapd and gadget snaps estimated_duration: 3s command: - kernel_snap_test.py --resource + snap_update_test.py --resource unit: template -template-resource: kernel_revision_info +template-resource: snap_revision_info template-unit: job -id: ubuntucore/kernel-refresh-{kernel_name} -_summary: Test refreshing kernel snap to the stable channel +id: ubuntucore/snap-refresh-{type}-{name}-to-stable-rev +_summary: Refresh {name} snap to latest revision in stable channel _description: - This test is currently for SUV process, the kernel to be tested is on + This test is currently for SUV process, the snap to be tested is on the beta channel. Test the availability to refresh to the older version on the stable channel. This test will be excuted only when the current installed revision (on beta channel) is different from the revsion in stable channel. plugin: shell +estimated_duration: 1m +category_id: ubuntucore +user: root +imports: from com.canonical.certification import snap_revision_info +command: + path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" + snap_update_test.py --refresh --rev {stable_rev} --info-path "$path" {name} + +unit: template +template-resource: snap_revision_info +template-unit: job +id: ubuntucore/reboot-after-snap-refresh-{type}-{name}-to-stable-rev +_summary: Reboot after {name} snap refresh to latest revision in stable channel +plugin: shell +flags: noreturn autorestart estimated_duration: 3m category_id: ubuntucore user: root -imports: from com.canonical.certification import kernel_revision_info -requires: - kernel_revision_info.stable_rev != "" - kernel_revision_info.original_installed_rev != kernel_revision_info.stable_rev +depends: ubuntucore/snap-refresh-{type}-{name}-to-stable-rev +command: + echo "Waiting 90s for any snap operation to finish before rebooting..." + sleep 90 + reboot + +unit: template +template-resource: snap_revision_info +template-unit: job +id: ubuntucore/snap-verify-after-refresh-{type}-{name}-to-stable-rev +_summary: Verify {name} snap revision after refreshing to latest revision in stable channel +plugin: shell +estimated_duration: 30s +category_id: ubuntucore +user: root +depends: ubuntucore/reboot-after-snap-refresh-{type}-{name}-to-stable-rev +command: + path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" + snap_update_test.py --verify-refresh --info-path "$path" {name} + +unit: template +template-resource: snap_revision_info +template-unit: job +id: ubuntucore/snap-revert-{type}-{name}-from-stable-rev +_summary: Revert {name} snap to original revision from stable channel +plugin: shell +estimated_duration: 3m +category_id: ubuntucore +user: root +depends: ubuntucore/snap-verify-after-refresh-{type}-{name}-to-stable-rev +command: + path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" + snap_update_test.py --revert --info-path "$path" {name} + +unit: template +template-resource: snap_revision_info +template-unit: job +id: ubuntucore/reboot-after-snap-revert-{type}-{name}-from-stable-rev +_summary: Reboot after {name} snap reverting to latest revision in stable channel +plugin: shell flags: noreturn autorestart +estimated_duration: 3m +category_id: ubuntucore +user: root +depends: ubuntucore/snap-revert-{type}-{name}-from-stable-rev command: - path="$PLAINBOX_SESSION_SHARE/kernel_revision_info" - kernel_snap_test.py --refresh --info-path "$path" + echo "Waiting 90s for any snap operation to finish before rebooting..." sleep 90 reboot unit: template -template-resource: kernel_revision_info +template-resource: snap_revision_info template-unit: job -id: ubuntucore/kernel-verify-after-refresh-{kernel_name} -_summary: Verify the kernel revision after refreshing to stable channel +id: ubuntucore/snap-verify-after-revert-{type}-{name}-from-stable-rev +_summary: Verify {name} snap revision after reverting from stable revision plugin: shell estimated_duration: 3s category_id: ubuntucore user: root -depends: ubuntucore/kernel-refresh-{kernel_name} +depends: ubuntucore/reboot-after-snap-revert-{type}-{name}-from-stable-rev command: - path="$PLAINBOX_SESSION_SHARE/kernel_revision_info" - kernel_snap_test.py --verify-refresh --info-path "$path" + path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" + snap_update_test.py --verify-revert --info-path "$path" {name} unit: template -template-resource: kernel_revision_info +template-resource: snap_revision_info template-unit: job -id: ubuntucore/kernel-revert-{kernel_name} -_summary: Revert kernel snap to the original revision +id: ubuntucore/snap-refresh-{type}-{name}-to-base-rev +_summary: Refresh {name} snap to its base revision +_description: + This test is currently for SUV process, the snap to be tested is on + the beta channel. Test the availability to refresh to the base revision + (the revision that came with the image). This test will be excuted only + when the current installed revision (on beta channel) is different from the + base revision. + This job (and the jobs depending on it) are useful when the device cannot + be reprovisioned easily, but you still want to simulate a fresh install + that would bump to the latest revision available. plugin: shell estimated_duration: 3m category_id: ubuntucore user: root -depends: ubuntucore/kernel-verify-after-refresh-{kernel_name} +imports: from com.canonical.certification import snap_revision_info +command: + path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" + snap_update_test.py --refresh --rev {base_rev} --info-path "$path" {name} + +unit: template +template-resource: snap_revision_info +template-unit: job +id: ubuntucore/reboot-after-snap-refresh-{type}-{name}-to-base-rev +_summary: Reboot after {name} snap refresh to base revision +plugin: shell +flags: noreturn autorestart +estimated_duration: 3m +category_id: ubuntucore +user: root +depends: ubuntucore/snap-refresh-{type}-{name}-to-base-rev +command: + echo "Waiting 90s for any snap operation to finish before rebooting..." + sleep 90 + reboot + +unit: template +template-resource: snap_revision_info +template-unit: job +id: ubuntucore/snap-verify-after-refresh-{type}-{name}-to-base-rev +_summary: Verify {name} snap revision after refreshing to base revision +plugin: shell +estimated_duration: 30s +category_id: ubuntucore +user: root +depends: ubuntucore/reboot-after-snap-refresh-{type}-{name}-to-base-rev +command: + path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" + snap_update_test.py --verify-refresh --info-path "$path" {name} + +unit: template +template-resource: snap_revision_info +template-unit: job +id: ubuntucore/snap-revert-{type}-{name}-from-base-rev +_summary: Revert {name} snap from base revision to original revision +plugin: shell +estimated_duration: 3m +category_id: ubuntucore +user: root +depends: ubuntucore/snap-verify-after-refresh-{type}-{name}-to-base-rev +command: + path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" + snap_update_test.py --revert --info-path "$path" {name} + +unit: template +template-resource: snap_revision_info +template-unit: job +id: ubuntucore/reboot-after-snap-revert-{type}-{name}-from-base-rev +_summary: Reboot after {name} snap revert to base revision +plugin: shell flags: noreturn autorestart +estimated_duration: 3m +category_id: ubuntucore +user: root +depends: ubuntucore/snap-revert-{type}-{name}-from-base-rev command: - path="$PLAINBOX_SESSION_SHARE/kernel_revision_info" - kernel_snap_test.py --revert --info-path "$path" + echo "Waiting 90s for any snap operation to finish before rebooting..." sleep 90 reboot unit: template -template-resource: kernel_revision_info +template-resource: snap_revision_info template-unit: job -id: ubuntucore/kernel-verify-after-revert-{kernel_name} -_summary: Verify the kernel revision after reverting +id: ubuntucore/snap-verify-after-revert-{type}-{name}-from-base-rev +_summary: Verify {name} snap revision after reverting from base revision plugin: shell estimated_duration: 3s category_id: ubuntucore user: root -depends: ubuntucore/kernel-revert-{kernel_name} +depends: ubuntucore/reboot-after-snap-revert-{type}-{name}-from-base-rev command: - path="$PLAINBOX_SESSION_SHARE/kernel_revision_info" - kernel_snap_test.py --verify-revert --info-path "$path" + path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" + snap_update_test.py --verify-revert --info-path "$path" {name} From 8a50b2e3d745d04368c247f27e2eb45120cbfe49 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Thu, 14 Sep 2023 18:21:43 +0800 Subject: [PATCH 04/18] Rework ubuntucore-automated Keep the kernel tests, but use the new jobs. --- providers/base/units/ubuntucore/test-plan.pxu | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/providers/base/units/ubuntucore/test-plan.pxu b/providers/base/units/ubuntucore/test-plan.pxu index 3af016db7..23847a7ec 100644 --- a/providers/base/units/ubuntucore/test-plan.pxu +++ b/providers/base/units/ubuntucore/test-plan.pxu @@ -12,12 +12,14 @@ unit: test plan _name: Automated Ubuntu Core OS feature tests _description: Automated OS feature tests for Ubuntu Core devices bootstrap_include: - kernel_revision_info + snap_revision_info include: - ubuntucore/kernel-refresh-.* - ubuntucore/kernel-verify-after-refresh-.* - ubuntucore/kernel-revert-.* - ubuntucore/kernel-verify-after-revert-.* + ubuntucore/snap-refresh-kernel-.*-stable-rev + ubuntucore/reboot-after-snap-refresh-kernel-.*-stable-rev + ubuntucore/snap-verify-after-refresh-kernel-.*-stable-rev + ubuntucore/snap-revert-kernel-.*-stable-rev + ubuntucore/reboot-after-snap-revert-kernel-.*-stable-rev + ubuntucore/snap-verify-after-revert-kernel-.*-stable-rev id: ubuntucore-manual unit: test plan From 5d413cf8b7d646cd488d0e42c91ff8a85ec0e7a6 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Thu, 14 Sep 2023 18:26:17 +0800 Subject: [PATCH 05/18] Add 3 test plans to test refresh/revert for each snaps (kernel/snapd/gadget) These 3 test plans are aimed at being used during the snap-update-verification process, where typically: - a new snap (kernel, gadget, snapd...) is released and put into the beta channel - this triggers a Checkbox run using a defined test plan on a set of defined devices Depending on the snap being updated, operator can select the right test plan for the job. I initially wanted to put everything into the same test plan and use `requires:` section of the jobs to skip the jobs that were not required (for instance, if testing a new gadget snap, the kernel snap will not have a new revision available, so related jobs should be skipped). However, I could not find a proper way to deal with this using the current requirement mechanism in Checkbox jobs. --- providers/base/units/ubuntucore/test-plan.pxu | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/providers/base/units/ubuntucore/test-plan.pxu b/providers/base/units/ubuntucore/test-plan.pxu index 23847a7ec..6452792be 100644 --- a/providers/base/units/ubuntucore/test-plan.pxu +++ b/providers/base/units/ubuntucore/test-plan.pxu @@ -32,3 +32,90 @@ include: ubuntucore/os-recovery-mode ubuntucore/os-fail-boot-(?!with-refresh-control).* ubuntucore/sshd + +id: ubuntucore-kernel-snap-refresh-revert +unit: test plan +_name: Kernel snap refresh and revert automated tests +_description: + This test plan will: + - simulate fresh install snap refresh by first refreshing to the base revision + of the kernel snap + - revert to the version used (usually revision from beta channel if used in + the snap-update-verification process) + - refresh to the previous stable version + - revert to version used + This test plan is useful for devices that cannot be provisioned easily in the + lab. +bootstrap_include: + snap_revision_info +include: + ubuntucore/snap-refresh-kernel-.*-base-rev + ubuntucore/reboot-after-snap-refresh-kernel-.*-base-rev + ubuntucore/snap-verify-after-refresh-kernel-.*-base-rev + ubuntucore/snap-revert-kernel-.*-base-rev + ubuntucore/reboot-after-snap-revert-kernel-.*-base-rev + ubuntucore/snap-verify-after-revert-kernel-.*-base-rev + ubuntucore/snap-refresh-kernel-.*-stable-rev + ubuntucore/reboot-after-snap-refresh-kernel-.*-stable-rev + ubuntucore/snap-verify-after-refresh-kernel-.*-stable-rev + ubuntucore/snap-revert-kernel-.*-stable-rev + ubuntucore/reboot-after-snap-revert-kernel-.*-stable-rev + ubuntucore/snap-verify-after-revert-kernel-.*-stable-rev + +id: ubuntucore-snapd-snap-refresh-revert +unit: test plan +_name: Snapd snap refresh and revert automated tests +_description: + This test plan will: + - simulate fresh install snap refresh by first refreshing to the base revision + of the snapd snap + - revert to the version used (usually revision from beta channel if used in + the snap-update-verification process) + - refresh to the previous stable version + - revert to version used + This test plan is useful for devices that cannot be provisioned easily in the + lab. +bootstrap_include: + snap_revision_info +include: + ubuntucore/snap-refresh-snapd-.*-base-rev + ubuntucore/reboot-after-snap-refresh-snapd-.*-base-rev + ubuntucore/snap-verify-after-refresh-snapd-.*-base-rev + ubuntucore/snap-revert-snapd-.*-base-rev + ubuntucore/reboot-after-snap-revert-snapd-.*-base-rev + ubuntucore/snap-verify-after-revert-snapd-.*-base-rev + ubuntucore/snap-refresh-snapd-.*-stable-rev + ubuntucore/reboot-after-snap-refresh-snapd-.*-stable-rev + ubuntucore/snap-verify-after-refresh-snapd-.*-stable-rev + ubuntucore/snap-revert-snapd-.*-stable-rev + ubuntucore/reboot-after-snap-revert-snapd-.*-stable-rev + ubuntucore/snap-verify-after-revert-snapd-.*-stable-rev + +id: ubuntucore-gadget-snap-refresh-revert +unit: test plan +_name: Gadget snap refresh and revert automated tests +_description: + This test plan will: + - simulate fresh install snap refresh by first refreshing to the base revision + of the gadget snap + - revert to the version used (usually revision from beta channel if used in + the snap-update-verification process) + - refresh to the previous stable version + - revert to version used + This test plan is useful for devices that cannot be provisioned easily in the + lab. +bootstrap_include: + snap_revision_info +include: + ubuntucore/snap-refresh-gadget-.*-base-rev + ubuntucore/reboot-after-snap-refresh-gadget-.*-base-rev + ubuntucore/snap-verify-after-refresh-gadget-.*-base-rev + ubuntucore/snap-revert-gadget-.*-base-rev + ubuntucore/reboot-after-snap-revert-gadget-.*-base-rev + ubuntucore/snap-verify-after-revert-gadget-.*-base-rev + ubuntucore/snap-refresh-gadget-.*-stable-rev + ubuntucore/reboot-after-snap-refresh-gadget-.*-stable-rev + ubuntucore/snap-verify-after-refresh-gadget-.*-stable-rev + ubuntucore/snap-revert-gadget-.*-stable-rev + ubuntucore/reboot-after-snap-revert-gadget-.*-stable-rev + ubuntucore/snap-verify-after-revert-gadget-.*-stable-rev From 77f78cef01627950fb29c0ea6604717ad4cb7902 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Fri, 15 Sep 2023 18:04:40 +0800 Subject: [PATCH 06/18] Add requirements for the snap name and revisions to allow jobs to be skipped This requirement trick allows to specify the stable_rev/base_rev and original_installed_rev to check for the snap being tested. This allows to efficiently skip the job if said snap has the same source and destination revision. Thanks to this, these jobs are not marked as failed anymore, but instead skipped. --- providers/base/units/ubuntucore/jobs.pxu | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/providers/base/units/ubuntucore/jobs.pxu b/providers/base/units/ubuntucore/jobs.pxu index 0ce14aad1..a229c8ae7 100644 --- a/providers/base/units/ubuntucore/jobs.pxu +++ b/providers/base/units/ubuntucore/jobs.pxu @@ -177,6 +177,8 @@ estimated_duration: 1m category_id: ubuntucore user: root imports: from com.canonical.certification import snap_revision_info +requires: + (snap_revision_info.name == "{name}") and snap_revision_info.stable_rev != snap_revision_info.original_installed_rev command: path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" snap_update_test.py --refresh --rev {stable_rev} --info-path "$path" {name} @@ -274,6 +276,8 @@ estimated_duration: 3m category_id: ubuntucore user: root imports: from com.canonical.certification import snap_revision_info +requires: + (snap_revision_info.name == "{name}") and snap_revision_info.base_rev != snap_revision_info.original_installed_rev command: path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" snap_update_test.py --refresh --rev {base_rev} --info-path "$path" {name} From 55b4a2cf2931acb5552aee4892231f661b27a87d Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Fri, 15 Sep 2023 18:07:28 +0800 Subject: [PATCH 07/18] Use one test plan to test kernel/snapd/gadget snap refresh/revert Thanks to the previous commit, only one test plan can be used instead of 3 separate test plans. This way, if the test plan is called by the snap-update-verification process because there is a new gadget snap in beta channel, the jobs for the two other snaps (kernel and snapd) will be skipped instead of failing. --- providers/base/units/ubuntucore/test-plan.pxu | 97 +++++++------------ 1 file changed, 36 insertions(+), 61 deletions(-) diff --git a/providers/base/units/ubuntucore/test-plan.pxu b/providers/base/units/ubuntucore/test-plan.pxu index 6452792be..f288a3795 100644 --- a/providers/base/units/ubuntucore/test-plan.pxu +++ b/providers/base/units/ubuntucore/test-plan.pxu @@ -33,11 +33,11 @@ include: ubuntucore/os-fail-boot-(?!with-refresh-control).* ubuntucore/sshd -id: ubuntucore-kernel-snap-refresh-revert +id: ubuntucore-snap-refresh-revert unit: test plan -_name: Kernel snap refresh and revert automated tests +_name: Snaps refresh and revert automated tests _description: - This test plan will: + For each of the kernel/snapd/gadget snap, this test plan will: - simulate fresh install snap refresh by first refreshing to the base revision of the kernel snap - revert to the version used (usually revision from beta channel if used in @@ -49,64 +49,9 @@ _description: bootstrap_include: snap_revision_info include: - ubuntucore/snap-refresh-kernel-.*-base-rev - ubuntucore/reboot-after-snap-refresh-kernel-.*-base-rev - ubuntucore/snap-verify-after-refresh-kernel-.*-base-rev - ubuntucore/snap-revert-kernel-.*-base-rev - ubuntucore/reboot-after-snap-revert-kernel-.*-base-rev - ubuntucore/snap-verify-after-revert-kernel-.*-base-rev - ubuntucore/snap-refresh-kernel-.*-stable-rev - ubuntucore/reboot-after-snap-refresh-kernel-.*-stable-rev - ubuntucore/snap-verify-after-refresh-kernel-.*-stable-rev - ubuntucore/snap-revert-kernel-.*-stable-rev - ubuntucore/reboot-after-snap-revert-kernel-.*-stable-rev - ubuntucore/snap-verify-after-revert-kernel-.*-stable-rev - -id: ubuntucore-snapd-snap-refresh-revert -unit: test plan -_name: Snapd snap refresh and revert automated tests -_description: - This test plan will: - - simulate fresh install snap refresh by first refreshing to the base revision - of the snapd snap - - revert to the version used (usually revision from beta channel if used in - the snap-update-verification process) - - refresh to the previous stable version - - revert to version used - This test plan is useful for devices that cannot be provisioned easily in the - lab. -bootstrap_include: - snap_revision_info -include: - ubuntucore/snap-refresh-snapd-.*-base-rev - ubuntucore/reboot-after-snap-refresh-snapd-.*-base-rev - ubuntucore/snap-verify-after-refresh-snapd-.*-base-rev - ubuntucore/snap-revert-snapd-.*-base-rev - ubuntucore/reboot-after-snap-revert-snapd-.*-base-rev - ubuntucore/snap-verify-after-revert-snapd-.*-base-rev - ubuntucore/snap-refresh-snapd-.*-stable-rev - ubuntucore/reboot-after-snap-refresh-snapd-.*-stable-rev - ubuntucore/snap-verify-after-refresh-snapd-.*-stable-rev - ubuntucore/snap-revert-snapd-.*-stable-rev - ubuntucore/reboot-after-snap-revert-snapd-.*-stable-rev - ubuntucore/snap-verify-after-revert-snapd-.*-stable-rev - -id: ubuntucore-gadget-snap-refresh-revert -unit: test plan -_name: Gadget snap refresh and revert automated tests -_description: - This test plan will: - - simulate fresh install snap refresh by first refreshing to the base revision - of the gadget snap - - revert to the version used (usually revision from beta channel if used in - the snap-update-verification process) - - refresh to the previous stable version - - revert to version used - This test plan is useful for devices that cannot be provisioned easily in the - lab. -bootstrap_include: - snap_revision_info -include: + ###################### + # Gadget-related tests + ###################### ubuntucore/snap-refresh-gadget-.*-base-rev ubuntucore/reboot-after-snap-refresh-gadget-.*-base-rev ubuntucore/snap-verify-after-refresh-gadget-.*-base-rev @@ -119,3 +64,33 @@ include: ubuntucore/snap-revert-gadget-.*-stable-rev ubuntucore/reboot-after-snap-revert-gadget-.*-stable-rev ubuntucore/snap-verify-after-revert-gadget-.*-stable-rev + ##################### + # Snapd-related tests + ##################### + ubuntucore/snap-refresh-snapd-.*-base-rev + ubuntucore/reboot-after-snap-refresh-snapd-.*-base-rev + ubuntucore/snap-verify-after-refresh-snapd-.*-base-rev + ubuntucore/snap-revert-snapd-.*-base-rev + ubuntucore/reboot-after-snap-revert-snapd-.*-base-rev + ubuntucore/snap-verify-after-revert-snapd-.*-base-rev + ubuntucore/snap-refresh-snapd-.*-stable-rev + ubuntucore/reboot-after-snap-refresh-snapd-.*-stable-rev + ubuntucore/snap-verify-after-refresh-snapd-.*-stable-rev + ubuntucore/snap-revert-snapd-.*-stable-rev + ubuntucore/reboot-after-snap-revert-snapd-.*-stable-rev + ubuntucore/snap-verify-after-revert-snapd-.*-stable-rev + ###################### + # Kernel-related tests + ###################### + ubuntucore/snap-refresh-kernel-.*-base-rev + ubuntucore/reboot-after-snap-refresh-kernel-.*-base-rev + ubuntucore/snap-verify-after-refresh-kernel-.*-base-rev + ubuntucore/snap-revert-kernel-.*-base-rev + ubuntucore/reboot-after-snap-revert-kernel-.*-base-rev + ubuntucore/snap-verify-after-revert-kernel-.*-base-rev + ubuntucore/snap-refresh-kernel-.*-stable-rev + ubuntucore/reboot-after-snap-refresh-kernel-.*-stable-rev + ubuntucore/snap-verify-after-refresh-kernel-.*-stable-rev + ubuntucore/snap-revert-kernel-.*-stable-rev + ubuntucore/reboot-after-snap-revert-kernel-.*-stable-rev + ubuntucore/snap-verify-after-revert-kernel-.*-stable-rev From 5ba9b2f13cf5009a433e1dfc804c10faf0c034f9 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Mon, 25 Sep 2023 10:18:24 +0800 Subject: [PATCH 08/18] Move test data to its own directory --- .../tests/test_data/snap_update_test_data.py | 513 +++++++++++++++++ providers/base/tests/test_snap_update_test.py | 523 +----------------- 2 files changed, 532 insertions(+), 504 deletions(-) create mode 100644 providers/base/tests/test_data/snap_update_test_data.py diff --git a/providers/base/tests/test_data/snap_update_test_data.py b/providers/base/tests/test_data/snap_update_test_data.py new file mode 100644 index 000000000..94bd777b4 --- /dev/null +++ b/providers/base/tests/test_data/snap_update_test_data.py @@ -0,0 +1,513 @@ +snapd_list_sample = [ + { + "channel": "22/stable", + "confinement": "strict", + "contact": "", + "description": ( + "The Ubuntu linux-raspi kernel package as a snap.\n" + "\n" + "This snap supports the Pi 2, 3 and 4. It is provided for " + "both armhf and arm64 architectures." + ), + "developer": "canonical", + "devmode": False, + "id": "jeIuP6tfFrvAdic8DMWqHmoaoukAPNbJ", + "ignore-validation": False, + "install-date": "2023-09-04T17:45:02.067900703Z", + "installed-size": 98791424, + "jailmode": False, + "links": None, + "mounted-from": "/var/lib/snapd/snaps/pi-kernel_663.snap", + "name": "pi-kernel", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "663", + "status": "active", + "summary": "The Ubuntu Raspberry Pi kernel", + "title": "pi-kernel", + "tracking-channel": "22/stable", + "type": "kernel", + "version": "5.15.0-1036.39", + }, + { + "channel": "latest/stable", + "confinement": "strict", + "contact": "https://github.com/snapcore/snapd/issues", + "description": ( + "Install, configure, refresh and remove snap packages. Snaps " + "are\n" + "'universal' packages that work across many different Linux " + "systems,\n" + "enabling secure distribution of the latest apps and " + "utilities for\n" + "cloud, servers, desktops and the internet of things.\n" + "\n" + "Start with 'snap list' to see installed snaps." + ), + "developer": "canonical", + "devmode": False, + "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/snapd.png", + "id": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4", + "ignore-validation": False, + "install-date": "2023-09-06T18:37:40.283181077Z", + "installed-size": 37236736, + "jailmode": False, + "license": "GPL-3.0", + "links": { + "contact": ["https://github.com/snapcore/snapd/issues"], + "website": ["https://snapcraft.io"], + }, + "media": [ + { + "height": 460, + "type": "icon", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/snapd.png", + "width": 460, + }, + { + "height": 648, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_115756_hLcyetO.png", + "width": 956, + }, + { + "height": 648, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_115824_2v3y6l8.png", + "width": 956, + }, + { + "height": 834, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_115055_Uuq7KIb.png", + "width": 1023, + }, + { + "height": 648, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_125944.png", + "width": 956, + }, + ], + "mounted-from": "/var/lib/snapd/snaps/snapd_20102.snap", + "name": "snapd", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "20102", + "status": "active", + "summary": "Daemon and tooling that enable snap packages", + "title": "snapd", + "tracking-channel": "latest/stable", + "type": "snapd", + "version": "2.60.3", + "website": "https://snapcraft.io", + }, + { + "apps": [ + {"name": "checkbox-cli", "snap": "checkbox"}, + {"name": "client-cert-iot-ubuntucore", "snap": "checkbox"}, + {"name": "configure", "snap": "checkbox"}, + {"name": "odm-certification", "snap": "checkbox"}, + { + "active": True, + "daemon": "simple", + "daemon-scope": "system", + "enabled": True, + "name": "service", + "snap": "checkbox", + }, + {"name": "shell", "snap": "checkbox"}, + {"name": "sru", "snap": "checkbox"}, + {"name": "test-runner", "snap": "checkbox"}, + ], + "base": "core22", + "channel": "uc22/beta", + "confinement": "strict", + "contact": "", + "description": ( + "Checkbox is a flexible test automation software.\n" + "It’s the main tool used in Ubuntu Certification program.\n" + ), + "developer": "ce-certification-qa", + "devmode": True, + "id": "06zlGRiJvdhJMO5NFVNjIOcZ1g3m8yVb", + "ignore-validation": False, + "install-date": "2023-09-04T09:02:32.253731099Z", + "installed-size": 12288, + "jailmode": False, + "links": None, + "mounted-from": "/var/lib/snapd/snaps/checkbox_2784.snap", + "name": "checkbox", + "private": False, + "publisher": { + "display-name": "Canonical Certification Team", + "id": "Euf8YO6waprpTXVrREuDw8ODHNIACTwi", + "username": "ce-certification-qa", + "validation": "unproven", + }, + "revision": "2784", + "status": "active", + "summary": "Checkbox test runner", + "title": "checkbox", + "tracking-channel": "uc22/beta", + "type": "app", + "version": "2.9.1", + }, + { + "base": "core22", + "channel": "latest/edge", + "confinement": "strict", + "contact": "", + "description": "Checkbox runtime and public providers", + "developer": "ce-certification-qa", + "devmode": False, + "id": "jUzbHhPAz1sQPzVD24Nky8UTbuKZ0gpR", + "ignore-validation": False, + "install-date": "2023-09-07T01:08:22.885438702Z", + "installed-size": 168214528, + "jailmode": False, + "links": None, + "mounted-from": "/var/lib/snapd/snaps/checkbox22_518.snap", + "name": "checkbox22", + "private": False, + "publisher": { + "display-name": "Canonical Certification Team", + "id": "Euf8YO6waprpTXVrREuDw8ODHNIACTwi", + "username": "ce-certification-qa", + "validation": "unproven", + }, + "revision": "518", + "status": "active", + "summary": "Checkbox runtime and public providers", + "title": "checkbox22", + "tracking-channel": "latest/edge", + "type": "app", + "version": "2.9.2.dev18+gd294291ef", + }, + { + "channel": "latest/stable", + "confinement": "strict", + "contact": "https://github.com/snapcore/core-base/issues", + "description": "The base snap based on the Ubuntu 22.04 release.", + "developer": "canonical", + "devmode": False, + "id": "amcUKQILKXHHTlmSa7NMdnXSx02dNeeT", + "ignore-validation": False, + "install-date": "2023-08-31T06:06:59.522300877Z", + "installed-size": 71860224, + "jailmode": False, + "links": {"contact": ["https://github.com/snapcore/core-base/issues"]}, + "mounted-from": "/var/lib/snapd/snaps/core22_867.snap", + "name": "core22", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "867", + "status": "active", + "summary": "Runtime environment based on Ubuntu 22.04", + "title": "core22", + "tracking-channel": "latest/stable", + "type": "base", + "version": "20230801", + }, + { + "base": "core22", + "channel": "", + "confinement": "strict", + "contact": "", + "description": ( + "Support files for booting Raspberry Pi.\n" + "This gadget snap supports the Raspberry Pi 2B, 3B, 3A+, 3B+, " + "4B, Compute\n" + "Module 3, and the Compute Module 3+ universally.\n" + ), + "developer": "canonical", + "devmode": False, + "icon": "/v2/icons/pi/icon", + "id": "YbGa9O3dAXl88YLI6Y1bGG74pwBxZyKg", + "ignore-validation": False, + "install-date": "2023-08-31T06:05:21.054039971Z", + "installed-size": 11612160, + "jailmode": False, + "links": None, + "mounted-from": "/var/lib/snapd/snaps/pi_132.snap", + "name": "pi", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "132", + "status": "active", + "summary": "Raspberry Pi gadget", + "tracking-channel": "22/stable", + "type": "gadget", + "version": "22-2", + }, +] + +snapd_list_no_kernel_snapd_gadget_snap = [ + { + "channel": "latest/stable", + "confinement": "strict", + "contact": "https://github.com/snapcore/core-base/issues", + "description": "The base snap based on the Ubuntu 22.04 release.", + "developer": "canonical", + "devmode": False, + "id": "amcUKQILKXHHTlmSa7NMdnXSx02dNeeT", + "ignore-validation": False, + "install-date": "2023-08-30T08:38:36.555233022Z", + "installed-size": 77492224, + "jailmode": False, + "links": {"contact": ["https://github.com/snapcore/core-base/issues"]}, + "mounted-from": "/var/lib/snapd/snaps/core22_864.snap", + "name": "core22", + "private": False, + "publisher": { + "display-name": "Canonical", + "id": "canonical", + "username": "canonical", + "validation": "verified", + }, + "revision": "864", + "status": "active", + "summary": "Runtime environment based on Ubuntu 22.04", + "title": "core22", + "tracking-channel": "latest/stable", + "type": "base", + "version": "20230801", + }, +] + +snapd_seed_glob_data = [ + "/var/lib/snapd/seed/snaps/firefox_2605.snap", + "/var/lib/snapd/seed/snaps/snapd_19267.snap", + "/var/lib/snapd/seed/snaps/pc-kernel_1289.snap", + "/var/lib/snapd/seed/snaps/core22_617.snap", +] + +snapd_list_firefox_snap = { + "apps": [ + { + "desktop-file": "/var/lib/snapd/desktop/applications/firefox_firefox.desktop", + "name": "firefox", + "snap": "firefox", + }, + {"name": "geckodriver", "snap": "firefox"}, + ], + "base": "core20", + "channel": "latest/stable", + "confinement": "strict", + "contact": "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla", + "description": "Firefox is a powerful, extensible web browser with support " + "for modern web application technologies.", + "developer": "mozilla", + "devmode": False, + "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", + "id": "3wdHCAVyZEmYsCMFDE9qt92UV8rC8Wdk", + "ignore-validation": False, + "install-date": "2023-08-25T08:35:06.453124352+08:00", + "installed-size": 248733696, + "jailmode": False, + "links": { + "contact": [ + "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla" + ], + "website": ["https://www.mozilla.org/firefox/"], + }, + "media": [ + { + "height": 196, + "type": "icon", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", + "width": 196, + }, + { + "height": 1415, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/09/Screenshot_from_2021-09-30_08-01-50.png", + "width": 1850, + }, + ], + "mounted-from": "/var/lib/snapd/snaps/firefox_3026.snap", + "name": "firefox", + "private": False, + "publisher": { + "display-name": "Mozilla", + "id": "OgeoZuqQpVvSr9eGKJzNCrFGSaKXpkey", + "username": "mozilla", + "validation": "verified", + }, + "revision": "3026", + "status": "active", + "summary": "Mozilla Firefox web browser", + "title": "firefox", + "tracking-channel": "latest/stable", + "type": "app", + "version": "116.0.3-2", + "website": "https://www.mozilla.org/firefox/", +} + +snapd_find_firefox_snap = [ + { + "base": "core22", + "categories": [{"featured": True, "name": "productivity"}], + "channel": "stable", + "channels": { + "esr/candidate": { + "channel": "esr/candidate", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-08-21T18:15:17.435529Z", + "revision": "3052", + "size": 253628416, + "version": "115.2.0esr-1", + }, + "esr/stable": { + "channel": "esr/stable", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-08-29T12:37:31.563045Z", + "revision": "3052", + "size": 253628416, + "version": "115.2.0esr-1", + }, + "latest/beta": { + "channel": "latest/beta", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-09-04T01:41:28.490375Z", + "revision": "3099", + "size": 251957248, + "version": "118.0b4-1", + }, + "latest/candidate": { + "channel": "latest/candidate", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-08-24T23:00:15.702917Z", + "revision": "3068", + "size": 248352768, + "version": "117.0-2", + }, + "latest/edge": { + "channel": "latest/edge", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-09-04T03:53:25.23937Z", + "revision": "3102", + "size": 269561856, + "version": "119.0a1", + }, + "latest/stable": { + "channel": "latest/stable", + "confinement": "strict", + "epoch": {"read": [0], "write": [0]}, + "released-at": "2023-08-29T12:37:04.958128Z", + "revision": "3068", + "size": 248352768, + "version": "117.0-2", + }, + }, + "confinement": "strict", + "contact": "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla", + "description": "Firefox is a powerful, extensible web browser with support " + "for modern web application technologies.", + "developer": "mozilla", + "devmode": False, + "download-size": 248352768, + "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", + "id": "3wdHCAVyZEmYsCMFDE9qt92UV8rC8Wdk", + "ignore-validation": False, + "jailmode": False, + "license": "MPL-2.0", + "links": { + "contact": [ + "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla" + ], + "website": ["https://www.mozilla.org/firefox/"], + }, + "media": [ + { + "height": 196, + "type": "icon", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", + "width": 196, + }, + { + "height": 1415, + "type": "screenshot", + "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/09/Screenshot_from_2021-09-30_08-01-50.png", + "width": 1850, + }, + ], + "name": "firefox", + "private": False, + "publisher": { + "display-name": "Mozilla", + "id": "OgeoZuqQpVvSr9eGKJzNCrFGSaKXpkey", + "username": "mozilla", + "validation": "verified", + }, + "revision": "3068", + "status": "available", + "store-url": "https://snapcraft.io/firefox", + "summary": "Mozilla Firefox web browser", + "title": "firefox", + "tracks": ["latest", "esr"], + "type": "app", + "version": "117.0-2", + "website": "https://www.mozilla.org/firefox/", + } +] + +snap_info_pi_kernel = { + "installed_revision": "567", + "base_revision": "567", + "name": "pi-kernel", + "type": "kernel", + "revisions": { + "18-cm3/beta": "649", + "18-cm3/edge": "649", + "18-pi/beta": "667", + "18-pi/candidate": "667", + "18-pi/edge": "667", + "18-pi/stable": "662", + "18-pi2/beta": "649", + "18-pi2/edge": "649", + "18-pi3/beta": "649", + "18-pi3/candidate": "649", + "18-pi3/edge": "649", + "18-pi3/stable": "649", + "18-pi4/beta": "77", + "18-pi4/candidate": "77", + "18-pi4/edge": "77", + "20/beta": "666", + "20/candidate": "661", + "20/edge": "666", + "20/stable": "661", + "22/beta": "663", + "22/candidate": "663", + "22/edge": "663", + "22/stable": "658", + "latest/candidate": "542", + }, + "tracking_channel": "22/stable", + "tracking_prefix": "22/", +} diff --git a/providers/base/tests/test_snap_update_test.py b/providers/base/tests/test_snap_update_test.py index ca8ba7489..94682ea48 100644 --- a/providers/base/tests/test_snap_update_test.py +++ b/providers/base/tests/test_snap_update_test.py @@ -5,262 +5,14 @@ import snap_update_test - -snapd_list_sample = [ - { - "channel": "22/stable", - "confinement": "strict", - "contact": "", - "description": "The Ubuntu linux-raspi kernel package as a snap.\n" - "\n" - "This snap supports the Pi 2, 3 and 4. It is provided for " - "both armhf and arm64 architectures.", - "developer": "canonical", - "devmode": False, - "id": "jeIuP6tfFrvAdic8DMWqHmoaoukAPNbJ", - "ignore-validation": False, - "install-date": "2023-09-04T17:45:02.067900703Z", - "installed-size": 98791424, - "jailmode": False, - "links": None, - "mounted-from": "/var/lib/snapd/snaps/pi-kernel_663.snap", - "name": "pi-kernel", - "private": False, - "publisher": { - "display-name": "Canonical", - "id": "canonical", - "username": "canonical", - "validation": "verified", - }, - "revision": "663", - "status": "active", - "summary": "The Ubuntu Raspberry Pi kernel", - "title": "pi-kernel", - "tracking-channel": "22/stable", - "type": "kernel", - "version": "5.15.0-1036.39", - }, - { - "channel": "latest/stable", - "confinement": "strict", - "contact": "https://github.com/snapcore/snapd/issues", - "description": "Install, configure, refresh and remove snap packages. Snaps " - "are\n" - "'universal' packages that work across many different Linux " - "systems,\n" - "enabling secure distribution of the latest apps and " - "utilities for\n" - "cloud, servers, desktops and the internet of things.\n" - "\n" - "Start with 'snap list' to see installed snaps.", - "developer": "canonical", - "devmode": False, - "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/snapd.png", - "id": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4", - "ignore-validation": False, - "install-date": "2023-09-06T18:37:40.283181077Z", - "installed-size": 37236736, - "jailmode": False, - "license": "GPL-3.0", - "links": { - "contact": ["https://github.com/snapcore/snapd/issues"], - "website": ["https://snapcraft.io"], - }, - "media": [ - { - "height": 460, - "type": "icon", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/snapd.png", - "width": 460, - }, - { - "height": 648, - "type": "screenshot", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_115756_hLcyetO.png", - "width": 956, - }, - { - "height": 648, - "type": "screenshot", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_115824_2v3y6l8.png", - "width": 956, - }, - { - "height": 834, - "type": "screenshot", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_115055_Uuq7KIb.png", - "width": 1023, - }, - { - "height": 648, - "type": "screenshot", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/09/Screenshot_20190924_125944.png", - "width": 956, - }, - ], - "mounted-from": "/var/lib/snapd/snaps/snapd_20102.snap", - "name": "snapd", - "private": False, - "publisher": { - "display-name": "Canonical", - "id": "canonical", - "username": "canonical", - "validation": "verified", - }, - "revision": "20102", - "status": "active", - "summary": "Daemon and tooling that enable snap packages", - "title": "snapd", - "tracking-channel": "latest/stable", - "type": "snapd", - "version": "2.60.3", - "website": "https://snapcraft.io", - }, - { - "apps": [ - {"name": "checkbox-cli", "snap": "checkbox"}, - {"name": "client-cert-iot-ubuntucore", "snap": "checkbox"}, - {"name": "configure", "snap": "checkbox"}, - {"name": "odm-certification", "snap": "checkbox"}, - { - "active": True, - "daemon": "simple", - "daemon-scope": "system", - "enabled": True, - "name": "service", - "snap": "checkbox", - }, - {"name": "shell", "snap": "checkbox"}, - {"name": "sru", "snap": "checkbox"}, - {"name": "test-runner", "snap": "checkbox"}, - ], - "base": "core22", - "channel": "uc22/beta", - "confinement": "strict", - "contact": "", - "description": "Checkbox is a flexible test automation software.\n" - "It’s the main tool used in Ubuntu Certification program.\n", - "developer": "ce-certification-qa", - "devmode": True, - "id": "06zlGRiJvdhJMO5NFVNjIOcZ1g3m8yVb", - "ignore-validation": False, - "install-date": "2023-09-04T09:02:32.253731099Z", - "installed-size": 12288, - "jailmode": False, - "links": None, - "mounted-from": "/var/lib/snapd/snaps/checkbox_2784.snap", - "name": "checkbox", - "private": False, - "publisher": { - "display-name": "Canonical Certification Team", - "id": "Euf8YO6waprpTXVrREuDw8ODHNIACTwi", - "username": "ce-certification-qa", - "validation": "unproven", - }, - "revision": "2784", - "status": "active", - "summary": "Checkbox test runner", - "title": "checkbox", - "tracking-channel": "uc22/beta", - "type": "app", - "version": "2.9.1", - }, - { - "base": "core22", - "channel": "latest/edge", - "confinement": "strict", - "contact": "", - "description": "Checkbox runtime and public providers", - "developer": "ce-certification-qa", - "devmode": False, - "id": "jUzbHhPAz1sQPzVD24Nky8UTbuKZ0gpR", - "ignore-validation": False, - "install-date": "2023-09-07T01:08:22.885438702Z", - "installed-size": 168214528, - "jailmode": False, - "links": None, - "mounted-from": "/var/lib/snapd/snaps/checkbox22_518.snap", - "name": "checkbox22", - "private": False, - "publisher": { - "display-name": "Canonical Certification Team", - "id": "Euf8YO6waprpTXVrREuDw8ODHNIACTwi", - "username": "ce-certification-qa", - "validation": "unproven", - }, - "revision": "518", - "status": "active", - "summary": "Checkbox runtime and public providers", - "title": "checkbox22", - "tracking-channel": "latest/edge", - "type": "app", - "version": "2.9.2.dev18+gd294291ef", - }, - { - "channel": "latest/stable", - "confinement": "strict", - "contact": "https://github.com/snapcore/core-base/issues", - "description": "The base snap based on the Ubuntu 22.04 release.", - "developer": "canonical", - "devmode": False, - "id": "amcUKQILKXHHTlmSa7NMdnXSx02dNeeT", - "ignore-validation": False, - "install-date": "2023-08-31T06:06:59.522300877Z", - "installed-size": 71860224, - "jailmode": False, - "links": {"contact": ["https://github.com/snapcore/core-base/issues"]}, - "mounted-from": "/var/lib/snapd/snaps/core22_867.snap", - "name": "core22", - "private": False, - "publisher": { - "display-name": "Canonical", - "id": "canonical", - "username": "canonical", - "validation": "verified", - }, - "revision": "867", - "status": "active", - "summary": "Runtime environment based on Ubuntu 22.04", - "title": "core22", - "tracking-channel": "latest/stable", - "type": "base", - "version": "20230801", - }, - { - "base": "core22", - "channel": "", - "confinement": "strict", - "contact": "", - "description": "Support files for booting Raspberry Pi.\n" - "This gadget snap supports the Raspberry Pi 2B, 3B, 3A+, 3B+, " - "4B, Compute\n" - "Module 3, and the Compute Module 3+ universally.\n", - "developer": "canonical", - "devmode": False, - "icon": "/v2/icons/pi/icon", - "id": "YbGa9O3dAXl88YLI6Y1bGG74pwBxZyKg", - "ignore-validation": False, - "install-date": "2023-08-31T06:05:21.054039971Z", - "installed-size": 11612160, - "jailmode": False, - "links": None, - "mounted-from": "/var/lib/snapd/snaps/pi_132.snap", - "name": "pi", - "private": False, - "publisher": { - "display-name": "Canonical", - "id": "canonical", - "username": "canonical", - "validation": "verified", - }, - "revision": "132", - "status": "active", - "summary": "Raspberry Pi gadget", - "tracking-channel": "22/stable", - "type": "gadget", - "version": "22-2", - }, -] +from test_data.snap_update_test_data import ( + snapd_list_sample, + snapd_list_no_kernel_snapd_gadget_snap, + snapd_seed_glob_data, + snapd_list_firefox_snap, + snapd_find_firefox_snap, + snap_info_pi_kernel, +) class SnapUpdateTests(unittest.TestCase): @@ -273,49 +25,13 @@ def test_guess_snaps(self, mock_snapd_list): @mock.patch("snap_update_test.Snapd.list") def test_guess_snaps_nothing(self, mock_snapd_list): - mock_snapd_list.return_value = [ - { - "channel": "latest/stable", - "confinement": "strict", - "contact": "https://github.com/snapcore/core-base/issues", - "description": "The base snap based on the Ubuntu 22.04 release.", - "developer": "canonical", - "devmode": False, - "id": "amcUKQILKXHHTlmSa7NMdnXSx02dNeeT", - "ignore-validation": False, - "install-date": "2023-08-30T08:38:36.555233022Z", - "installed-size": 77492224, - "jailmode": False, - "links": {"contact": ["https://github.com/snapcore/core-base/issues"]}, - "mounted-from": "/var/lib/snapd/snaps/core22_864.snap", - "name": "core22", - "private": False, - "publisher": { - "display-name": "Canonical", - "id": "canonical", - "username": "canonical", - "validation": "verified", - }, - "revision": "864", - "status": "active", - "summary": "Runtime environment based on Ubuntu 22.04", - "title": "core22", - "tracking-channel": "latest/stable", - "type": "base", - "version": "20230801", - }, - ] + mock_snapd_list.return_value = snapd_list_no_kernel_snapd_gadget_snap snaps = snap_update_test.guess_snaps() self.assertEqual(snaps, {}) @mock.patch("snap_update_test.glob") def test_get_snap_base_rev(self, mock_glob): - mock_glob.return_value = [ - "/var/lib/snapd/seed/snaps/firefox_2605.snap", - "/var/lib/snapd/seed/snaps/snapd_19267.snap", - "/var/lib/snapd/seed/snaps/pc-kernel_1289.snap", - "/var/lib/snapd/seed/snaps/core22_617.snap", - ] + mock_glob.return_value = snapd_seed_glob_data snap_rev = snap_update_test.get_snap_base_rev() self.assertEqual(len(snap_rev), 4) self.assertEqual(snap_rev["pc-kernel"], "1289") @@ -325,181 +41,8 @@ def test_get_snap_base_rev(self, mock_glob): @mock.patch("snap_update_test.Snapd.find") def test_get_snap_info(self, mock_snapd_find, mock_snapd_list, mock_base_revs): mock_base_revs.return_value = {"firefox": "2605"} - mock_snapd_list.return_value = { - "apps": [ - { - "desktop-file": "/var/lib/snapd/desktop/applications/firefox_firefox.desktop", - "name": "firefox", - "snap": "firefox", - }, - {"name": "geckodriver", "snap": "firefox"}, - ], - "base": "core20", - "channel": "latest/stable", - "confinement": "strict", - "contact": "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla", - "description": "Firefox is a powerful, extensible web browser with support " - "for modern web application technologies.", - "developer": "mozilla", - "devmode": False, - "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", - "id": "3wdHCAVyZEmYsCMFDE9qt92UV8rC8Wdk", - "ignore-validation": False, - "install-date": "2023-08-25T08:35:06.453124352+08:00", - "installed-size": 248733696, - "jailmode": False, - "links": { - "contact": [ - "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla" - ], - "website": ["https://www.mozilla.org/firefox/"], - }, - "media": [ - { - "height": 196, - "type": "icon", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", - "width": 196, - }, - { - "height": 1415, - "type": "screenshot", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/09/Screenshot_from_2021-09-30_08-01-50.png", - "width": 1850, - }, - ], - "mounted-from": "/var/lib/snapd/snaps/firefox_3026.snap", - "name": "firefox", - "private": False, - "publisher": { - "display-name": "Mozilla", - "id": "OgeoZuqQpVvSr9eGKJzNCrFGSaKXpkey", - "username": "mozilla", - "validation": "verified", - }, - "revision": "3026", - "status": "active", - "summary": "Mozilla Firefox web browser", - "title": "firefox", - "tracking-channel": "latest/stable", - "type": "app", - "version": "116.0.3-2", - "website": "https://www.mozilla.org/firefox/", - } - - mock_snapd_find.return_value = [ - { - "base": "core22", - "categories": [{"featured": True, "name": "productivity"}], - "channel": "stable", - "channels": { - "esr/candidate": { - "channel": "esr/candidate", - "confinement": "strict", - "epoch": {"read": [0], "write": [0]}, - "released-at": "2023-08-21T18:15:17.435529Z", - "revision": "3052", - "size": 253628416, - "version": "115.2.0esr-1", - }, - "esr/stable": { - "channel": "esr/stable", - "confinement": "strict", - "epoch": {"read": [0], "write": [0]}, - "released-at": "2023-08-29T12:37:31.563045Z", - "revision": "3052", - "size": 253628416, - "version": "115.2.0esr-1", - }, - "latest/beta": { - "channel": "latest/beta", - "confinement": "strict", - "epoch": {"read": [0], "write": [0]}, - "released-at": "2023-09-04T01:41:28.490375Z", - "revision": "3099", - "size": 251957248, - "version": "118.0b4-1", - }, - "latest/candidate": { - "channel": "latest/candidate", - "confinement": "strict", - "epoch": {"read": [0], "write": [0]}, - "released-at": "2023-08-24T23:00:15.702917Z", - "revision": "3068", - "size": 248352768, - "version": "117.0-2", - }, - "latest/edge": { - "channel": "latest/edge", - "confinement": "strict", - "epoch": {"read": [0], "write": [0]}, - "released-at": "2023-09-04T03:53:25.23937Z", - "revision": "3102", - "size": 269561856, - "version": "119.0a1", - }, - "latest/stable": { - "channel": "latest/stable", - "confinement": "strict", - "epoch": {"read": [0], "write": [0]}, - "released-at": "2023-08-29T12:37:04.958128Z", - "revision": "3068", - "size": 248352768, - "version": "117.0-2", - }, - }, - "confinement": "strict", - "contact": "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla", - "description": "Firefox is a powerful, extensible web browser with support " - "for modern web application technologies.", - "developer": "mozilla", - "devmode": False, - "download-size": 248352768, - "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", - "id": "3wdHCAVyZEmYsCMFDE9qt92UV8rC8Wdk", - "ignore-validation": False, - "jailmode": False, - "license": "MPL-2.0", - "links": { - "contact": [ - "https://support.mozilla.org/kb/file-bug-report-or-feature-request-mozilla" - ], - "website": ["https://www.mozilla.org/firefox/"], - }, - "media": [ - { - "height": 196, - "type": "icon", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/12/firefox_logo.png", - "width": 196, - }, - { - "height": 1415, - "type": "screenshot", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2021/09/Screenshot_from_2021-09-30_08-01-50.png", - "width": 1850, - }, - ], - "name": "firefox", - "private": False, - "publisher": { - "display-name": "Mozilla", - "id": "OgeoZuqQpVvSr9eGKJzNCrFGSaKXpkey", - "username": "mozilla", - "validation": "verified", - }, - "revision": "3068", - "status": "available", - "store-url": "https://snapcraft.io/firefox", - "summary": "Mozilla Firefox web browser", - "title": "firefox", - "tracks": ["latest", "esr"], - "type": "app", - "version": "117.0-2", - "website": "https://www.mozilla.org/firefox/", - } - ] - + mock_snapd_list.return_value = snapd_list_firefox_snap + mock_snapd_find.return_value = snapd_find_firefox_snap expected_snap_info = { "installed_revision": "3026", "base_revision": "2605", @@ -523,41 +66,13 @@ def test_get_snap_info(self, mock_snapd_find, mock_snapd_list, mock_base_revs): @mock.patch("snap_update_test.get_snap_info") @mock.patch("sys.stdout", new_callable=io.StringIO) def test_print_resource_info(self, mock_stdout, mock_snap_info): - mock_snap_info.return_value = { - "installed_revision": "567", - "base_revision": "567", - "name": "pi-kernel", - "type": "kernel", - "revisions": { - "18-cm3/beta": "649", - "18-cm3/edge": "649", - "18-pi/beta": "667", - "18-pi/candidate": "667", - "18-pi/edge": "667", - "18-pi/stable": "662", - "18-pi2/beta": "649", - "18-pi2/edge": "649", - "18-pi3/beta": "649", - "18-pi3/candidate": "649", - "18-pi3/edge": "649", - "18-pi3/stable": "649", - "18-pi4/beta": "77", - "18-pi4/candidate": "77", - "18-pi4/edge": "77", - "20/beta": "666", - "20/candidate": "661", - "20/edge": "666", - "20/stable": "661", - "22/beta": "663", - "22/candidate": "663", - "22/edge": "663", - "22/stable": "658", - "latest/candidate": "542", - }, - "tracking_channel": "22/stable", - "tracking_prefix": "22/", - } - expected_output = "name: pi-kernel\ntype: kernel\ntracking: 22/stable\nbase_rev: 567\nstable_rev: 658\ncandidate_rev: 663\nbeta_rev: 663\nedge_rev: 663\noriginal_installed_rev: 567\n\n" + mock_snap_info.return_value = snap_info_pi_kernel + expected_output = ( + "name: pi-kernel\ntype: kernel\n" + "tracking: 22/stable\nbase_rev: 567\nstable_rev: 658\n" + "candidate_rev: 663\nbeta_rev: 663\nedge_rev: 663\n" + "original_installed_rev: 567\n\n" + ) snap_update_test.print_resource_info() self.assertEqual(mock_stdout.getvalue(), expected_output) From b0fd08e69a74ac1a81df01392023021b06c688c6 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Mon, 25 Sep 2023 12:22:44 +0800 Subject: [PATCH 09/18] Add test for the snap_revert function --- providers/base/tests/test_snap_update_test.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/providers/base/tests/test_snap_update_test.py b/providers/base/tests/test_snap_update_test.py index 94682ea48..0aa8c6ab1 100644 --- a/providers/base/tests/test_snap_update_test.py +++ b/providers/base/tests/test_snap_update_test.py @@ -196,7 +196,7 @@ def test_verify_revert_ok( ) self.assertEqual(srr.verify_revert(), 0) - @mock.patch("builtins.open", new_callable=mock.mock_open()) + @mock.patch("builtins.open", new_callable=mock.mock_open) @mock.patch("snap_update_test.Snapd.list") @mock.patch("snap_update_test.Snapd.change") @mock.patch("snap_update_test.json.load") @@ -226,3 +226,24 @@ def test_verify_revert_nok( ) logging.disable(logging.CRITICAL) self.assertEqual(srr.verify_revert(), 1) + + @mock.patch("snap_update_test.Snapd.revert") + @mock.patch("snap_update_test.get_snap_info") + def test_snap_revert(self, mock_snap_info, mock_snapd_revert): + mock_file_data = ( + '{"name": "test-snap", "original_revision": "10", ' + '"destination_revision": "20", "refresh_id": "80"}' + ) + mock_snapd_revert.return_value = {"change": 99} + srr = snap_update_test.SnapRefreshRevert( + name="test-snap", rev="2", info_path="/test/info" + ) + mock_snap_info.return_value = { + "name": "test-snap", + "installed_revision": "132", + "tracking_channel": "22/beta", + } + with mock.patch("builtins.open", mock.mock_open(read_data=mock_file_data)) as m: + srr.snap_revert() + mock_snapd_revert.assert_called() + m.assert_called_with("/test/info", "w") From 3683a6edfcc6cb1c0cdeaf85326644ae5ff10d42 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Mon, 25 Sep 2023 12:27:15 +0800 Subject: [PATCH 10/18] Import patch and mock_open directly --- providers/base/tests/test_snap_update_test.py | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/providers/base/tests/test_snap_update_test.py b/providers/base/tests/test_snap_update_test.py index 0aa8c6ab1..ca849bacc 100644 --- a/providers/base/tests/test_snap_update_test.py +++ b/providers/base/tests/test_snap_update_test.py @@ -1,7 +1,7 @@ import io import logging import unittest -from unittest import mock +from unittest.mock import patch, mock_open import snap_update_test @@ -16,29 +16,29 @@ class SnapUpdateTests(unittest.TestCase): - @mock.patch("snap_update_test.Snapd.list") + @patch("snap_update_test.Snapd.list") def test_guess_snaps(self, mock_snapd_list): mock_snapd_list.return_value = snapd_list_sample snaps = snap_update_test.guess_snaps() expected_snaps = {"kernel": "pi-kernel", "snapd": "snapd", "gadget": "pi"} self.assertEqual(snaps, expected_snaps) - @mock.patch("snap_update_test.Snapd.list") + @patch("snap_update_test.Snapd.list") def test_guess_snaps_nothing(self, mock_snapd_list): mock_snapd_list.return_value = snapd_list_no_kernel_snapd_gadget_snap snaps = snap_update_test.guess_snaps() self.assertEqual(snaps, {}) - @mock.patch("snap_update_test.glob") + @patch("snap_update_test.glob") def test_get_snap_base_rev(self, mock_glob): mock_glob.return_value = snapd_seed_glob_data snap_rev = snap_update_test.get_snap_base_rev() self.assertEqual(len(snap_rev), 4) self.assertEqual(snap_rev["pc-kernel"], "1289") - @mock.patch("snap_update_test.get_snap_base_rev") - @mock.patch("snap_update_test.Snapd.list") - @mock.patch("snap_update_test.Snapd.find") + @patch("snap_update_test.get_snap_base_rev") + @patch("snap_update_test.Snapd.list") + @patch("snap_update_test.Snapd.find") def test_get_snap_info(self, mock_snapd_find, mock_snapd_list, mock_base_revs): mock_base_revs.return_value = {"firefox": "2605"} mock_snapd_list.return_value = snapd_list_firefox_snap @@ -63,8 +63,8 @@ def test_get_snap_info(self, mock_snapd_find, mock_snapd_list, mock_base_revs): snap_info = snap_update_test.get_snap_info("firefox") self.assertEqual(snap_info, expected_snap_info) - @mock.patch("snap_update_test.get_snap_info") - @mock.patch("sys.stdout", new_callable=io.StringIO) + @patch("snap_update_test.get_snap_info") + @patch("sys.stdout", new_callable=io.StringIO) def test_print_resource_info(self, mock_stdout, mock_snap_info): mock_snap_info.return_value = snap_info_pi_kernel expected_output = ( @@ -78,8 +78,8 @@ def test_print_resource_info(self, mock_stdout, mock_snap_info): class SnapRefreshRevertTests(unittest.TestCase): - @mock.patch("snap_update_test.Snapd") - @mock.patch("snap_update_test.get_snap_info") + @patch("snap_update_test.Snapd") + @patch("snap_update_test.get_snap_info") def test_snap_refresh_same_revision(self, mock_snap_info, mock_snapd): mock_snap_info.return_value = {"installed_revision": "132"} srr = snap_update_test.SnapRefreshRevert( @@ -88,9 +88,9 @@ def test_snap_refresh_same_revision(self, mock_snap_info, mock_snapd): logging.disable(logging.CRITICAL) self.assertEqual(srr.snap_refresh(), 1) - @mock.patch("builtins.open", new_callable=mock.mock_open()) - @mock.patch("snap_update_test.Snapd.refresh") - @mock.patch("snap_update_test.get_snap_info") + @patch("builtins.open", new_callable=mock_open) + @patch("snap_update_test.Snapd.refresh") + @patch("snap_update_test.get_snap_info") def test_snap_refresh_different_revision( self, mock_snap_info, mock_snapd_refresh, mock_file ): @@ -104,11 +104,11 @@ def test_snap_refresh_different_revision( ) self.assertEqual(srr.snap_refresh(), 0) - @mock.patch("builtins.open", new_callable=mock.mock_open()) - @mock.patch("snap_update_test.Snapd.list") - @mock.patch("snap_update_test.Snapd.change") - @mock.patch("snap_update_test.json.load") - @mock.patch("snap_update_test.get_snap_info") + @patch("builtins.open", new_callable=mock_open) + @patch("snap_update_test.Snapd.list") + @patch("snap_update_test.Snapd.change") + @patch("snap_update_test.json.load") + @patch("snap_update_test.get_snap_info") def test_verify_refresh_ok( self, mock_snap_info, @@ -134,11 +134,11 @@ def test_verify_refresh_ok( ) self.assertEqual(srr.verify_refresh(), 0) - @mock.patch("builtins.open", new_callable=mock.mock_open()) - @mock.patch("snap_update_test.Snapd.list") - @mock.patch("snap_update_test.Snapd.change") - @mock.patch("snap_update_test.json.load") - @mock.patch("snap_update_test.get_snap_info") + @patch("builtins.open", new_callable=mock_open) + @patch("snap_update_test.Snapd.list") + @patch("snap_update_test.Snapd.change") + @patch("snap_update_test.json.load") + @patch("snap_update_test.get_snap_info") def test_verify_refresh_nok( self, mock_snap_info, @@ -166,11 +166,11 @@ def test_verify_refresh_nok( logging.disable(logging.CRITICAL) self.assertEqual(srr.verify_refresh(), 1) - @mock.patch("builtins.open", new_callable=mock.mock_open()) - @mock.patch("snap_update_test.Snapd.list") - @mock.patch("snap_update_test.Snapd.change") - @mock.patch("snap_update_test.json.load") - @mock.patch("snap_update_test.get_snap_info") + @patch("builtins.open", new_callable=mock_open) + @patch("snap_update_test.Snapd.list") + @patch("snap_update_test.Snapd.change") + @patch("snap_update_test.json.load") + @patch("snap_update_test.get_snap_info") def test_verify_revert_ok( self, mock_snap_info, @@ -196,11 +196,11 @@ def test_verify_revert_ok( ) self.assertEqual(srr.verify_revert(), 0) - @mock.patch("builtins.open", new_callable=mock.mock_open) - @mock.patch("snap_update_test.Snapd.list") - @mock.patch("snap_update_test.Snapd.change") - @mock.patch("snap_update_test.json.load") - @mock.patch("snap_update_test.get_snap_info") + @patch("builtins.open", new_callable=mock_open) + @patch("snap_update_test.Snapd.list") + @patch("snap_update_test.Snapd.change") + @patch("snap_update_test.json.load") + @patch("snap_update_test.get_snap_info") def test_verify_revert_nok( self, mock_snap_info, @@ -227,8 +227,8 @@ def test_verify_revert_nok( logging.disable(logging.CRITICAL) self.assertEqual(srr.verify_revert(), 1) - @mock.patch("snap_update_test.Snapd.revert") - @mock.patch("snap_update_test.get_snap_info") + @patch("snap_update_test.Snapd.revert") + @patch("snap_update_test.get_snap_info") def test_snap_revert(self, mock_snap_info, mock_snapd_revert): mock_file_data = ( '{"name": "test-snap", "original_revision": "10", ' @@ -243,7 +243,7 @@ def test_snap_revert(self, mock_snap_info, mock_snapd_revert): "installed_revision": "132", "tracking_channel": "22/beta", } - with mock.patch("builtins.open", mock.mock_open(read_data=mock_file_data)) as m: + with patch("builtins.open", mock_open(read_data=mock_file_data)) as m: srr.snap_revert() mock_snapd_revert.assert_called() m.assert_called_with("/test/info", "w") From 0fde55a4ae2338bcf2e6624f19d8d90f98e0a756 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Mon, 25 Sep 2023 14:12:58 +0800 Subject: [PATCH 11/18] Fix test_snap_revert to work with Python 3.5 --- providers/base/tests/test_snap_update_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/base/tests/test_snap_update_test.py b/providers/base/tests/test_snap_update_test.py index ca849bacc..14c938789 100644 --- a/providers/base/tests/test_snap_update_test.py +++ b/providers/base/tests/test_snap_update_test.py @@ -245,5 +245,5 @@ def test_snap_revert(self, mock_snap_info, mock_snapd_revert): } with patch("builtins.open", mock_open(read_data=mock_file_data)) as m: srr.snap_revert() - mock_snapd_revert.assert_called() + self.assertTrue(mock_snapd_revert.called) m.assert_called_with("/test/info", "w") From cf1302d8d2238fe2651884e565cf972c7339d575 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Wed, 18 Oct 2023 19:48:18 +0800 Subject: [PATCH 12/18] Fix snap_update_test.py after PR feedback --- providers/base/bin/snap_update_test.py | 281 +++++++++++-------------- 1 file changed, 119 insertions(+), 162 deletions(-) diff --git a/providers/base/bin/snap_update_test.py b/providers/base/bin/snap_update_test.py index 22ec90f41..8015979fc 100755 --- a/providers/base/bin/snap_update_test.py +++ b/providers/base/bin/snap_update_test.py @@ -19,38 +19,32 @@ # along with Checkbox. If not, see . import argparse -from glob import glob +from pathlib import Path import json import logging -import os.path import sys import time from checkbox_support.snap_utils.snapd import Snapd -def guess_snaps() -> dict: +def guess_snaps() -> list: """ Guess the names of the kernel, snapd and gadget snaps from installed snaps on the system. - :return: a dict with the snap names for each snap found - :rtype: dict + :return: a list of snap names that are either kernel, snapd or gadget snaps + :rtype: list """ - snapd = Snapd() - installed_snaps = snapd.list() - snaps = {} - for snap in installed_snaps: - if snap["type"] == "kernel": - snaps["kernel"] = snap["name"] - elif snap["type"] == "gadget": - snaps["gadget"] = snap["name"] - elif snap["type"] == "snapd": - snaps["snapd"] = snap["name"] + snaps = [ + snap["name"] + for snap in Snapd().list() + if snap["type"] in ("kernel", "gadget", "snapd") + ] return snaps -def get_snap_base_rev() -> dict: +def get_snaps_base_rev() -> dict: """ Retrieve the name and the base revision of each snap originally installed on the system. @@ -58,216 +52,179 @@ def get_snap_base_rev() -> dict: :return: a dict containing the snap names and their base revisions :rtype: dict """ - base_snaps = glob("/var/lib/snapd/seed/snaps/*.snap") + seed_snaps_dir = Path("/var/lib/snapd/seed/snaps/") + base_snaps = seed_snaps_dir.glob("*.snap") base_rev_info = {} for snap_path in base_snaps: - snap_basename = os.path.basename(snap_path) - snap_name = os.path.splitext(snap_basename)[0] - snap, rev = snap_name.rsplit("_", maxsplit=1) + snap, rev = snap_path.stem.rsplit("_", maxsplit=1) base_rev_info[snap] = rev return base_rev_info -def get_snap_info(name) -> dict: - """ - Retrieve information such as name, type, available revisions, etc. about - a given snap. +class SnapInfo: + def __init__(self, name): + snap = Snapd().list(name) + self.name = snap["name"] + self.type = snap["type"] + self.tracking_channel = snap["tracking-channel"] + self.installed_revision = snap["revision"] + self.tracking_prefix = (self.tracking_channel.split("/")[0] + "/") if "/" in self.tracking_channel else "" + self.base_revision = get_snaps_base_rev().get(name, "") - :return: a dict with the available information - :rtype: dict - """ - snapd = Snapd() - snap_info = {} - snap = snapd.list(name) - base_revs = get_snap_base_rev() - snap_info["name"] = snap["name"] - snap_info["type"] = snap["type"] - snap_info["tracking_channel"] = snap["tracking-channel"] - snap_info["installed_revision"] = snap["revision"] - snap_info["base_revision"] = base_revs.get(name, "") - tracking = snap_info["tracking_channel"] - prefix = (tracking.split("/")[0] + "/") if "/" in tracking else "" - snap_info["tracking_prefix"] = prefix + revisions = {} + for item in Snapd().find(name, exact=True): + for channel, info in item["channels"].items(): + revisions[channel] = info["revision"] - snap_additional_info = snapd.find(name, exact=True) - snap_info["revisions"] = {} - for item in snap_additional_info: - for channel, info in item["channels"].items(): - snap_info["revisions"][channel] = info["revision"] - return snap_info + self.stable_revision = revisions.get( + "{}stable".format(self.tracking_prefix), "" + ) + self.candidate_revision = revisions.get( + "{}candidate".format(self.tracking_prefix), "" + ) + self.beta_revision = revisions.get( + "{}beta".format(self.tracking_prefix), "" + ) + self.edge_revision = revisions.get( + "{}edge".format(self.tracking_prefix), "" + ) + + def print_as_resource(self): + print("name: {}".format(self.name)) + print("type: {}".format(self.type)) + print("tracking: {}".format(self.tracking_channel)) + print("base_rev: {}".format(self.base_revision)) + print("stable_rev: {}".format(self.stable_revision)) + print("candidate_rev: {}".format(self.candidate_revision)) + print("beta_rev: {}".format(self.beta_revision)) + print("edge_rev: {}".format(self.edge_revision)) + print("original_installed_rev: {}".format(self.installed_revision)) + print() def print_resource_info(): - snaps = guess_snaps().values() - for snap in snaps: - info = get_snap_info(snap) - tracking = info["tracking_channel"] - prefix = info["tracking_prefix"] - base_rev = info.get("base_revision", "") - stable_rev = info["revisions"].get("{}stable".format(prefix), "") - cand_rev = info["revisions"].get("{}candidate".format(prefix), "") - beta_rev = info["revisions"].get("{}beta".format(prefix), "") - edge_rev = info["revisions"].get("{}edge".format(prefix), "") - installed_rev = info.get("installed_revision", "") + for snap in guess_snaps(): + SnapInfo(snap).print_as_resource() - print("name: {}".format(info["name"])) - print("type: {}".format(info["type"])) - print("tracking: {}".format(tracking)) - print("base_rev: {}".format(base_rev)) - print("stable_rev: {}".format(stable_rev)) - print("candidate_rev: {}".format(cand_rev)) - print("beta_rev: {}".format(beta_rev)) - print("edge_rev: {}".format(edge_rev)) - print("original_installed_rev: {}".format(installed_rev)) - print() +def save_change_info(path, data): + with open(path, "w") as file: + json.dump(data, file) +def load_change_info(path): + try: + with open(path, "r") as file: + data = json.load(file) + except FileNotFoundError: + logging.error("File not found: %s", path) + logging.error("Did the previous job run as expected?") + raise SystemExit(1) + return data class SnapRefreshRevert: - def __init__(self, name, rev, info_path): + def __init__(self, name, revision, info_path): self.snapd = Snapd() - self.snap_info = get_snap_info(name) + self.snap_info = SnapInfo(name) self.path = info_path - self.rev = rev + self.revision = revision self.name = name def snap_refresh(self): data = {} - original_revision = self.snap_info["installed_revision"] - if original_revision == self.rev: + original_revision = self.snap_info.installed_revision + if original_revision == self.revision: logging.error( - "Trying to refresh to the same revision (%s)!", self.rev + "Trying to refresh to the same revision (%s)!", self.revision ) - return 1 + raise SystemExit(1) data["name"] = self.name data["original_revision"] = original_revision - data["destination_revision"] = self.rev + data["destination_revision"] = self.revision logging.info( - "Refreshing %s snap from rev %s to rev %s", + "Refreshing %s snap from revision %s to revision %s", self.name, original_revision, - self.rev, + self.revision, ) - r = self.snapd.refresh( + response = self.snapd.refresh( self.name, - channel=self.snap_info["tracking_channel"], - revision=self.rev, + channel=self.snap_info.tracking_channel, + revision=self.revision, reboot=True, ) logging.info( - "Refreshing requested (channel %s, rev %s)", - self.snap_info["tracking_channel"], - self.rev, + "Refreshing requested (channel %s, revision %s)", + self.snap_info.tracking_channel, + self.revision, ) - with open(self.path, "w") as file: - data["refresh_id"] = r["change"] - json.dump(data, file) + data["change_id"] = response["change"] + save_change_info(self.path, data) logging.info("Waiting for reboot...") - return 0 - - def verify_refresh(self): - try: - with open(self.path, "r") as file: - data = json.load(file) - except FileNotFoundError: - logging.error("File not found: %s", self.path) - logging.error("Did the previous job run as expected?") - return 1 - id = data["refresh_id"] - name = data["name"] - - logging.info("Checking refresh status for snap %s...", name) - start_time = time.time() - timeout = 300 # 5 minutes timeout - while True: - result = self.snapd.change(str(id)) - if result == "Done": - logging.info("%s snap refresh complete", name) - break - - if time.time() - start_time >= timeout: - logging.error( - "%s snap refresh did not complete within 5 minutes", name - ) - return False - logging.info("Waiting for %s snap refreshing to be done...", name) - logging.info("Trying again in 10 seconds...") - time.sleep(10) - - current_rev = self.snapd.list(self.snap_info["name"])["revision"] - destination_rev = data["destination_revision"] - if current_rev != destination_rev: - logging.error( - "Current revision %s is NOT equal to expected revision %s", - current_rev, - destination_rev, - ) - return 1 - else: - logging.info( - "PASS: current revision (%s) matches the expected revision", - current_rev, - ) - return 0 def snap_revert(self): - with open(self.path, "r") as file: - data = json.load(file) + data = load_change_info(self.path) original_rev = data["original_revision"] destination_rev = data["destination_revision"] logging.info( - "Reverting %s snap (from rev %s to rev %s)", + "Reverting %s snap (from revision %s to revision %s)", self.name, destination_rev, original_rev, ) - r = self.snapd.revert(self.snap_info["name"], reboot=True) + response = self.snapd.revert(self.name, reboot=True) logging.info("Reverting requested") - with open(self.path, "w") as file: - data["revert_id"] = r["change"] - json.dump(data, file) + data["change_id"] = response["change"] + save_change_info(self.path, data) logging.info("Waiting for reboot...") - def verify_revert(self): - with open(self.path, "r") as file: - data = json.load(file) - id = data["revert_id"] - original_rev = data["original_revision"] + def verify(self, type): + if type not in ("refresh", "revert"): + raise SystemExit( + "'{}' verification unknown. Can be either 'refresh' or 'revert'.".format( + type + ) + ) + data = load_change_info(self.path) + id = data["change_id"] - logging.info("Checking %s snap revert status", self.name) + logging.info("Checking %s status for snap %s...", type, self.name) start_time = time.time() timeout = 300 # 5 minutes timeout while True: result = self.snapd.change(str(id)) if result == "Done": - logging.info("%s snap revert complete", self.name) + logging.info("%s snap %s complete", self.name, type) break if time.time() - start_time >= timeout: logging.error( - "%s snap revert did not complete within 5 minutes", + "%s snap %s did not complete within 5 minutes", self.name, + type, ) - return False + raise SystemExit(1) logging.info( - "Waiting for %s snap reverting to be done...", self.name + "Waiting for %s snap %s to be done...", self.name, type ) - logging.info("Trying again in 10 seconds.") + logging.info("Trying again in 10 seconds...") time.sleep(10) - current_rev = self.snapd.list(self.snap_info["name"])["revision"] - if current_rev != original_rev: + current_rev = self.snapd.list(self.name)["revision"] + if type == "refresh": + tested_rev = data["destination_revision"] + else: + tested_rev = data["original_revision"] + if current_rev != tested_rev: logging.error( - "Current revision (%s) is NOT equal to original revision (%s)", + "Current revision (%s) is different from expected revision (%s)", current_rev, - original_rev, + tested_rev, ) - return 1 + raise SystemExit(1) else: logging.info( - "PASS: current revision (%s) matches the original revision", + "PASS: current revision (%s) matches the expected revision", current_rev, ) - return 0 def main(): @@ -310,7 +267,7 @@ def main(): help="Path to the information file", ) parser.add_argument( - "--rev", + "--revision", help="Revision to refresh to", ) @@ -320,16 +277,16 @@ def main(): print_resource_info() else: test = SnapRefreshRevert( - name=args.name, info_path=args.info_path, rev=args.rev + name=args.name, info_path=args.info_path, revision=args.revision ) if args.refresh: - return test.snap_refresh() + test.snap_refresh() if args.verify_refresh: - return test.verify_refresh() + test.verify("refresh") if args.revert: - return test.snap_revert() + test.snap_revert() if args.verify_revert: - return test.verify_revert() + test.verify("revert") if __name__ == "__main__": From d34fcf2cf5f692685bbfdef8dd9a576e1fb7ec0f Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Wed, 18 Oct 2023 19:48:45 +0800 Subject: [PATCH 13/18] Update related jobs according to feedback --- providers/base/units/ubuntucore/jobs.pxu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/base/units/ubuntucore/jobs.pxu b/providers/base/units/ubuntucore/jobs.pxu index a229c8ae7..12cbdfcb5 100644 --- a/providers/base/units/ubuntucore/jobs.pxu +++ b/providers/base/units/ubuntucore/jobs.pxu @@ -181,7 +181,7 @@ requires: (snap_revision_info.name == "{name}") and snap_revision_info.stable_rev != snap_revision_info.original_installed_rev command: path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" - snap_update_test.py --refresh --rev {stable_rev} --info-path "$path" {name} + snap_update_test.py --refresh --revision {stable_rev} --info-path "$path" {name} unit: template template-resource: snap_revision_info @@ -280,7 +280,7 @@ requires: (snap_revision_info.name == "{name}") and snap_revision_info.base_rev != snap_revision_info.original_installed_rev command: path="$PLAINBOX_SESSION_SHARE/{name}_snap_revision_info" - snap_update_test.py --refresh --rev {base_rev} --info-path "$path" {name} + snap_update_test.py --refresh --revision {base_rev} --info-path "$path" {name} unit: template template-resource: snap_revision_info From f2fef9ed4f7bf80f5d460c10dd7b0bcfaf812aa0 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Fri, 20 Oct 2023 12:33:23 +0800 Subject: [PATCH 14/18] Update unit tests --- .../tests/test_data/snap_update_test_data.py | 10 +- providers/base/tests/test_snap_update_test.py | 380 ++++++++---------- 2 files changed, 182 insertions(+), 208 deletions(-) diff --git a/providers/base/tests/test_data/snap_update_test_data.py b/providers/base/tests/test_data/snap_update_test_data.py index 94bd777b4..9be308740 100644 --- a/providers/base/tests/test_data/snap_update_test_data.py +++ b/providers/base/tests/test_data/snap_update_test_data.py @@ -1,3 +1,5 @@ +from pathlib import Path + snapd_list_sample = [ { "channel": "22/stable", @@ -296,10 +298,10 @@ ] snapd_seed_glob_data = [ - "/var/lib/snapd/seed/snaps/firefox_2605.snap", - "/var/lib/snapd/seed/snaps/snapd_19267.snap", - "/var/lib/snapd/seed/snaps/pc-kernel_1289.snap", - "/var/lib/snapd/seed/snaps/core22_617.snap", + Path("/var/lib/snapd/seed/snaps/firefox_2605.snap"), + Path("/var/lib/snapd/seed/snaps/snapd_19267.snap"), + Path("/var/lib/snapd/seed/snaps/pc-kernel_1289.snap"), + Path("/var/lib/snapd/seed/snaps/core22_617.snap"), ] snapd_list_firefox_snap = { diff --git a/providers/base/tests/test_snap_update_test.py b/providers/base/tests/test_snap_update_test.py index 14c938789..17e1a9483 100644 --- a/providers/base/tests/test_snap_update_test.py +++ b/providers/base/tests/test_snap_update_test.py @@ -1,7 +1,7 @@ import io import logging import unittest -from unittest.mock import patch, mock_open +from unittest.mock import patch, mock_open, MagicMock import snap_update_test @@ -20,230 +20,202 @@ class SnapUpdateTests(unittest.TestCase): def test_guess_snaps(self, mock_snapd_list): mock_snapd_list.return_value = snapd_list_sample snaps = snap_update_test.guess_snaps() - expected_snaps = {"kernel": "pi-kernel", "snapd": "snapd", "gadget": "pi"} + expected_snaps = ["pi-kernel", "snapd", "pi"] self.assertEqual(snaps, expected_snaps) @patch("snap_update_test.Snapd.list") def test_guess_snaps_nothing(self, mock_snapd_list): mock_snapd_list.return_value = snapd_list_no_kernel_snapd_gadget_snap snaps = snap_update_test.guess_snaps() - self.assertEqual(snaps, {}) + self.assertEqual(snaps, []) - @patch("snap_update_test.glob") - def test_get_snap_base_rev(self, mock_glob): + @patch("snap_update_test.Path.glob") + def test_get_snaps_base_rev(self, mock_glob): mock_glob.return_value = snapd_seed_glob_data - snap_rev = snap_update_test.get_snap_base_rev() + snap_rev = snap_update_test.get_snaps_base_rev() self.assertEqual(len(snap_rev), 4) self.assertEqual(snap_rev["pc-kernel"], "1289") - @patch("snap_update_test.get_snap_base_rev") - @patch("snap_update_test.Snapd.list") - @patch("snap_update_test.Snapd.find") - def test_get_snap_info(self, mock_snapd_find, mock_snapd_list, mock_base_revs): + @patch("builtins.open", new_callable=mock_open) + def test_load_change_info_file_not_found(self, mock_open): + mock_open.side_effect = FileNotFoundError() + logging.disable(logging.CRITICAL) + with self.assertRaises(SystemExit): + snap_update_test.load_change_info("/file/not/found") + + @patch("builtins.open", new_callable=mock_open) + @patch("snap_update_test.json") + def test_load_change_info(self, mock_json, mock_open): + snap_update_test.load_change_info("test") + mock_json.load.assert_called() + + @patch("snap_update_test.print_resource_info") + def test_main_print_resource(self, mock_print_resource_info): + args = ["--resource"] + snap_update_test.main(args) + mock_print_resource_info.assert_called() + + @patch("snap_update_test.SnapRefreshRevert") + def test_main_refresh(self, mock_srr): + args = ["--refresh", "--info-path", "/tmp/change.json", "chromium"] + snap_update_test.main(args) + instance = mock_srr.return_value + instance.snap_refresh.assert_called() + + @patch("snap_update_test.SnapRefreshRevert") + def test_main_verify_refresh(self, mock_srr): + args = ["--verify-refresh", "--info-path", "/tmp/change.json", "chromium"] + snap_update_test.main(args) + instance = mock_srr.return_value + instance.verify.assert_called_with("refresh") + + @patch("snap_update_test.SnapRefreshRevert") + def test_main_revert(self, mock_srr): + args = ["--revert", "--info-path", "/tmp/change.json", "chromium"] + snap_update_test.main(args) + instance = mock_srr.return_value + instance.snap_revert.assert_called() + + @patch("snap_update_test.SnapRefreshRevert") + def test_main_verify_revert(self, mock_srr): + args = ["--verify-revert", "--info-path", "/tmp/change.json", "chromium"] + snap_update_test.main(args) + instance = mock_srr.return_value + instance.verify.assert_called_with("revert") + + +class SnapInfoTests(unittest.TestCase): + @patch("snap_update_test.get_snaps_base_rev") + @patch("snap_update_test.Snapd") + def test_init(self, mock_snapd, mock_base_revs): mock_base_revs.return_value = {"firefox": "2605"} - mock_snapd_list.return_value = snapd_list_firefox_snap - mock_snapd_find.return_value = snapd_find_firefox_snap - expected_snap_info = { - "installed_revision": "3026", - "base_revision": "2605", - "name": "firefox", - "type": "app", - "revisions": { - "esr/candidate": "3052", - "esr/stable": "3052", - "latest/beta": "3099", - "latest/candidate": "3068", - "latest/edge": "3102", - "latest/stable": "3068", - }, - "tracking_channel": "latest/stable", - "tracking_prefix": "latest/", - } - - snap_info = snap_update_test.get_snap_info("firefox") - self.assertEqual(snap_info, expected_snap_info) - - @patch("snap_update_test.get_snap_info") + mock_snapd.return_value.list.return_value = snapd_list_firefox_snap + mock_snapd.return_value.find.return_value = snapd_find_firefox_snap + + snap_info = snap_update_test.SnapInfo("firefox") + self.assertEqual(snap_info.installed_revision, "3026") + self.assertEqual(snap_info.base_revision, "2605") + self.assertEqual(snap_info.tracking_channel, "latest/stable") + self.assertEqual(snap_info.tracking_prefix, "latest/") + self.assertEqual(snap_info.stable_revision, "3068") + self.assertEqual(snap_info.candidate_revision, "3068") + self.assertEqual(snap_info.beta_revision, "3099") + self.assertEqual(snap_info.edge_revision, "3102") + @patch("sys.stdout", new_callable=io.StringIO) - def test_print_resource_info(self, mock_stdout, mock_snap_info): - mock_snap_info.return_value = snap_info_pi_kernel - expected_output = ( - "name: pi-kernel\ntype: kernel\n" - "tracking: 22/stable\nbase_rev: 567\nstable_rev: 658\n" - "candidate_rev: 663\nbeta_rev: 663\nedge_rev: 663\n" - "original_installed_rev: 567\n\n" - ) - snap_update_test.print_resource_info() - self.assertEqual(mock_stdout.getvalue(), expected_output) + def test_print_as_resource(self, mock_stdout): + mock_self = MagicMock() + snap_update_test.SnapInfo.print_as_resource(mock_self) + assert "name:" in mock_stdout.getvalue() + assert "type:" in mock_stdout.getvalue() + assert "tracking:" in mock_stdout.getvalue() + assert "base_rev:" in mock_stdout.getvalue() + assert "stable_rev:" in mock_stdout.getvalue() + assert "candidate_rev:" in mock_stdout.getvalue() + assert "beta_rev:" in mock_stdout.getvalue() + assert "edge_rev:" in mock_stdout.getvalue() + assert "original_installed_rev:" in mock_stdout.getvalue() + # Make sure there is a blank line at the end, as this is required by + # Checkbox resource jobs + assert mock_stdout.getvalue().endswith("\n\n") class SnapRefreshRevertTests(unittest.TestCase): - @patch("snap_update_test.Snapd") - @patch("snap_update_test.get_snap_info") - def test_snap_refresh_same_revision(self, mock_snap_info, mock_snapd): - mock_snap_info.return_value = {"installed_revision": "132"} - srr = snap_update_test.SnapRefreshRevert( - name="test", rev="132", info_path="/test/info" - ) + def test_snap_refresh_same_revision(self): + mock_self = MagicMock() + mock_self.revision = "1" + mock_snap_info = MagicMock() + mock_snap_info.installed_revision = "1" + mock_self.snap_info = mock_snap_info logging.disable(logging.CRITICAL) - self.assertEqual(srr.snap_refresh(), 1) + with self.assertRaises(SystemExit): + snap_update_test.SnapRefreshRevert.snap_refresh(mock_self) - @patch("builtins.open", new_callable=mock_open) @patch("snap_update_test.Snapd.refresh") - @patch("snap_update_test.get_snap_info") + @patch("snap_update_test.save_change_info") def test_snap_refresh_different_revision( - self, mock_snap_info, mock_snapd_refresh, mock_file - ): - mock_snap_info.return_value = { - "installed_revision": "132", - "tracking_channel": "22/beta", - } - mock_snapd_refresh.return_value = {"change": "1"} - srr = snap_update_test.SnapRefreshRevert( - name="test", rev="137", info_path="/test/info" - ) - self.assertEqual(srr.snap_refresh(), 0) - - @patch("builtins.open", new_callable=mock_open) - @patch("snap_update_test.Snapd.list") - @patch("snap_update_test.Snapd.change") - @patch("snap_update_test.json.load") - @patch("snap_update_test.get_snap_info") - def test_verify_refresh_ok( - self, - mock_snap_info, - mock_json_load, - mock_snapd_change, - mock_snapd_list, - mock_file, - ): - mock_snap_info.return_value = { - "name": "test-snap", - "installed_revision": "132", - "tracking_channel": "22/beta", - } - mock_json_load.return_value = { - "refresh_id": "1", - "name": "test-snap", - "destination_revision": "2", - } - mock_snapd_change.return_value = "Done" - mock_snapd_list.return_value = {"revision": "2"} - srr = snap_update_test.SnapRefreshRevert( - name="test-snap", rev="2", info_path="/test/info" - ) - self.assertEqual(srr.verify_refresh(), 0) - - @patch("builtins.open", new_callable=mock_open) - @patch("snap_update_test.Snapd.list") - @patch("snap_update_test.Snapd.change") - @patch("snap_update_test.json.load") - @patch("snap_update_test.get_snap_info") - def test_verify_refresh_nok( - self, - mock_snap_info, - mock_json_load, - mock_snapd_change, - mock_snapd_list, - mock_file, - ): - mock_snap_info.return_value = { - "name": "test-snap", - "installed_revision": "132", - "tracking_channel": "22/beta", - } - mock_json_load.return_value = { - "refresh_id": "1", - "name": "test-snap", - "destination_revision": "2", - } - mock_snapd_change.return_value = "Done" - mock_snapd_list.return_value = {"revision": "1"} - srr = snap_update_test.SnapRefreshRevert( - name="test-snap", rev="2", info_path="/test/info" - ) - - logging.disable(logging.CRITICAL) - self.assertEqual(srr.verify_refresh(), 1) - - @patch("builtins.open", new_callable=mock_open) - @patch("snap_update_test.Snapd.list") - @patch("snap_update_test.Snapd.change") - @patch("snap_update_test.json.load") - @patch("snap_update_test.get_snap_info") - def test_verify_revert_ok( - self, - mock_snap_info, - mock_json_load, - mock_snapd_change, - mock_snapd_list, - mock_file, + self, mock_save_change_info, mock_snapd_refresh ): - mock_snap_info.return_value = { - "name": "test-snap", - "installed_revision": "132", - "tracking_channel": "22/beta", - } - mock_json_load.return_value = { - "revert_id": "1", - "name": "test-snap", - "original_revision": "2", - } - mock_snapd_change.return_value = "Done" - mock_snapd_list.return_value = {"revision": "2"} - srr = snap_update_test.SnapRefreshRevert( - name="test-snap", rev="2", info_path="/test/info" - ) - self.assertEqual(srr.verify_revert(), 0) - - @patch("builtins.open", new_callable=mock_open) - @patch("snap_update_test.Snapd.list") - @patch("snap_update_test.Snapd.change") - @patch("snap_update_test.json.load") - @patch("snap_update_test.get_snap_info") - def test_verify_revert_nok( - self, - mock_snap_info, - mock_json_load, - mock_snapd_change, - mock_snapd_list, - mock_file, - ): - mock_snap_info.return_value = { - "name": "test-snap", - "installed_revision": "132", - "tracking_channel": "22/beta", - } - mock_json_load.return_value = { - "revert_id": "1", - "name": "test-snap", - "original_revision": "2", - } - mock_snapd_change.return_value = "Done" - mock_snapd_list.return_value = {"revision": "1"} - srr = snap_update_test.SnapRefreshRevert( - name="test-snap", rev="2", info_path="/test/info" - ) + mock_self = MagicMock() + mock_self.revision = "1" + mock_snap_info = MagicMock() + mock_snap_info.installed_revision = "2" + mock_self.snap_info = mock_snap_info logging.disable(logging.CRITICAL) - self.assertEqual(srr.verify_revert(), 1) + snap_update_test.SnapRefreshRevert.snap_refresh(mock_self) + snap_update_test.save_change_info.assert_called() @patch("snap_update_test.Snapd.revert") - @patch("snap_update_test.get_snap_info") - def test_snap_revert(self, mock_snap_info, mock_snapd_revert): - mock_file_data = ( - '{"name": "test-snap", "original_revision": "10", ' - '"destination_revision": "20", "refresh_id": "80"}' - ) - mock_snapd_revert.return_value = {"change": 99} - srr = snap_update_test.SnapRefreshRevert( - name="test-snap", rev="2", info_path="/test/info" - ) - mock_snap_info.return_value = { - "name": "test-snap", - "installed_revision": "132", - "tracking_channel": "22/beta", - } - with patch("builtins.open", mock_open(read_data=mock_file_data)) as m: - srr.snap_revert() - self.assertTrue(mock_snapd_revert.called) - m.assert_called_with("/test/info", "w") + @patch("snap_update_test.load_change_info") + @patch("snap_update_test.save_change_info") + def test_snap_revert( + self, mock_save_change_info, mock_load_change_info, mock_snapd_revert + ): + mock_self = MagicMock() + mock_snap_info = MagicMock() + mock_self.snap_info = mock_snap_info + snap_update_test.SnapRefreshRevert.snap_revert(mock_self) + snap_update_test.load_change_info.assert_called() + snap_update_test.save_change_info.assert_called() + + def test_verify_invalid(self): + mock_self = MagicMock() + mock_snap_info = MagicMock() + mock_self.snap_info = mock_snap_info + with self.assertRaises(SystemExit): + snap_update_test.SnapRefreshRevert.verify(mock_self, type="invalid") + + @patch("snap_update_test.load_change_info") + def test_verify_refresh_wrong_revision(self, mock_load_change): + mock_self = MagicMock() + mock_snap_info = MagicMock() + mock_self.snap_info = mock_snap_info + mock_load_change.return_value = {"change_id": "1", "destination_revision": "1"} + mock_self.snapd.list.return_value = {"revision": "2"} + with self.assertRaises(SystemExit): + snap_update_test.SnapRefreshRevert.verify(mock_self, type="refresh") + + @patch("snap_update_test.load_change_info") + def test_verify_refresh_expected_revision(self, mock_load_change): + mock_self = MagicMock() + mock_snap_info = MagicMock() + mock_self.snap_info = mock_snap_info + mock_load_change.return_value = {"change_id": "1", "destination_revision": "1"} + mock_self.snapd.list.return_value = {"revision": "1"} + snap_update_test.SnapRefreshRevert.verify(mock_self, type="refresh") + + @patch("snap_update_test.load_change_info") + def test_verify_reverting_wrong_revision(self, mock_load_change): + mock_self = MagicMock() + mock_snap_info = MagicMock() + mock_self.snap_info = mock_snap_info + mock_load_change.return_value = {"change_id": "1", "original_revision": "1"} + mock_self.snapd.list.return_value = {"revision": "2"} + with self.assertRaises(SystemExit): + snap_update_test.SnapRefreshRevert.verify(mock_self, type="revert") + + def test_wait_for_snap_change(self): + mock_self = MagicMock() + mock_snap_info = MagicMock() + mock_self.snap_info = mock_snap_info + mock_self.snapd.change.return_value = "Done" + snap_update_test.SnapRefreshRevert.wait_for_snap_change(mock_self, change_id=1) + + def test_wait_for_snap_change_timeout(self): + mock_self = MagicMock() + mock_snap_info = MagicMock() + mock_self.snap_info = mock_snap_info + with self.assertRaises(SystemExit): + snap_update_test.SnapRefreshRevert.wait_for_snap_change( + mock_self, change_id=1, timeout=-1 + ) + + @patch("snap_update_test.time.time") + @patch("snap_update_test.time.sleep") + def test_wait_for_snap_change_ongoing(self, mock_sleep, mock_time): + mock_self = MagicMock() + mock_snap_info = MagicMock() + mock_self.snap_info = mock_snap_info + mock_self.snapd.change.side_effect = ["Doing", "Done"] + mock_time.return_value = 1 + snap_update_test.SnapRefreshRevert.wait_for_snap_change(mock_self, change_id=1) From 5455721fef1203c8114e5698208cda689da9e525 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Fri, 20 Oct 2023 12:36:52 +0800 Subject: [PATCH 15/18] Few changes to the snap_update_test.py script to make it easier to test Namely: - Add a logger and use it instead of calling logging.xxx() directly - Decouple the logic that waits for a snap change from the verification logic (create a new `wait_for_snap_change()` method and modify `verify()`) - When error happens, put the message as a parameter of SystemExit() instead of logging it first - Pass arguments from command line to the `main()` function --- providers/base/bin/snap_update_test.py | 107 +++++++++++++------------ 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/providers/base/bin/snap_update_test.py b/providers/base/bin/snap_update_test.py index 8015979fc..9a714efbe 100755 --- a/providers/base/bin/snap_update_test.py +++ b/providers/base/bin/snap_update_test.py @@ -27,6 +27,13 @@ from checkbox_support.snap_utils.snapd import Snapd +logger = logging.getLogger(__name__) +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) + def guess_snaps() -> list: """ @@ -68,7 +75,11 @@ def __init__(self, name): self.type = snap["type"] self.tracking_channel = snap["tracking-channel"] self.installed_revision = snap["revision"] - self.tracking_prefix = (self.tracking_channel.split("/")[0] + "/") if "/" in self.tracking_channel else "" + self.tracking_prefix = ( + (self.tracking_channel.split("/")[0] + "/") + if "/" in self.tracking_channel + else "" + ) self.base_revision = get_snaps_base_rev().get(name, "") revisions = {} @@ -106,20 +117,23 @@ def print_resource_info(): for snap in guess_snaps(): SnapInfo(snap).print_as_resource() + def save_change_info(path, data): with open(path, "w") as file: json.dump(data, file) + def load_change_info(path): try: with open(path, "r") as file: data = json.load(file) except FileNotFoundError: - logging.error("File not found: %s", path) - logging.error("Did the previous job run as expected?") + logger.error("File not found: %s", path) + logger.error("Did the previous job run as expected?") raise SystemExit(1) return data + class SnapRefreshRevert: def __init__(self, name, revision, info_path): self.snapd = Snapd() @@ -132,14 +146,14 @@ def snap_refresh(self): data = {} original_revision = self.snap_info.installed_revision if original_revision == self.revision: - logging.error( + logger.error( "Trying to refresh to the same revision (%s)!", self.revision ) raise SystemExit(1) data["name"] = self.name data["original_revision"] = original_revision data["destination_revision"] = self.revision - logging.info( + logger.info( "Refreshing %s snap from revision %s to revision %s", self.name, original_revision, @@ -151,88 +165,81 @@ def snap_refresh(self): revision=self.revision, reboot=True, ) - logging.info( + logger.info( "Refreshing requested (channel %s, revision %s)", self.snap_info.tracking_channel, self.revision, ) data["change_id"] = response["change"] save_change_info(self.path, data) - logging.info("Waiting for reboot...") + logger.info("Waiting for reboot...") def snap_revert(self): data = load_change_info(self.path) original_rev = data["original_revision"] destination_rev = data["destination_revision"] - logging.info( + logger.info( "Reverting %s snap (from revision %s to revision %s)", self.name, destination_rev, original_rev, ) response = self.snapd.revert(self.name, reboot=True) - logging.info("Reverting requested") + logger.info("Reverting requested") data["change_id"] = response["change"] save_change_info(self.path, data) - logging.info("Waiting for reboot...") - - def verify(self, type): - if type not in ("refresh", "revert"): - raise SystemExit( - "'{}' verification unknown. Can be either 'refresh' or 'revert'.".format( - type - ) - ) - data = load_change_info(self.path) - id = data["change_id"] + logger.info("Waiting for reboot...") - logging.info("Checking %s status for snap %s...", type, self.name) + def wait_for_snap_change(self, change_id, type, timeout=300): start_time = time.time() - timeout = 300 # 5 minutes timeout while True: - result = self.snapd.change(str(id)) + result = self.snapd.change(str(change_id)) if result == "Done": - logging.info("%s snap %s complete", self.name, type) - break - - if time.time() - start_time >= timeout: - logging.error( - "%s snap %s did not complete within 5 minutes", - self.name, - type, + logger.info("%s snap %s complete", self.name, type) + return + + current_time = time.time() + if current_time - start_time >= timeout: + raise SystemExit( + "{} snap {} did not complete within {} seconds".format( + self.name, type, timeout + ) ) - raise SystemExit(1) - logging.info( - "Waiting for %s snap %s to be done...", self.name, type - ) - logging.info("Trying again in 10 seconds...") + logger.info("Waiting for %s snap %s to be done...", + self.name, + type) + logger.info("Trying again in 10 seconds...") time.sleep(10) + def verify(self, type, timeout=300): + logger.info("Beginning verify...") + if type not in ("refresh", "revert"): + msg = ("'{}' verification unknown. Can be either 'refresh' " + "or 'revert'.").format(type) + raise SystemExit(msg) + data = load_change_info(self.path) + id = data["change_id"] + self.wait_for_snap_change(id, type, timeout) + logger.info("Checking %s status for snap %s...", type, self.name) + current_rev = self.snapd.list(self.name)["revision"] if type == "refresh": tested_rev = data["destination_revision"] else: tested_rev = data["original_revision"] if current_rev != tested_rev: - logging.error( - "Current revision (%s) is different from expected revision (%s)", - current_rev, - tested_rev, + msg = ("Current revision ({}) is different from expected revision " + "({})").format(current_rev, tested_rev) + raise SystemExit( ) - raise SystemExit(1) else: - logging.info( + logger.info( "PASS: current revision (%s) matches the expected revision", current_rev, ) -def main(): - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(levelname)-8s %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) +def main(args): parser = argparse.ArgumentParser() parser.add_argument( "name", nargs="?", default="", help="Name of the snap to act upon" @@ -271,7 +278,7 @@ def main(): help="Revision to refresh to", ) - args = parser.parse_args() + args = parser.parse_args(args) if args.resource: print_resource_info() @@ -290,4 +297,4 @@ def main(): if __name__ == "__main__": - sys.exit(main()) + sys.exit(main(sys.argv[1:])) From 6462d9b3fb544d6ff4db03ff34bee7981d999d32 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Fri, 20 Oct 2023 14:42:16 +0800 Subject: [PATCH 16/18] Add error handling when the snap change result is "Error" If there is an error for current snap change, no need to wait for timeout. We log the information from this change and exit with an error. Also add unit tests to test this. --- providers/base/bin/snap_update_test.py | 15 ++++++++++ providers/base/tests/test_snap_update_test.py | 30 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/providers/base/bin/snap_update_test.py b/providers/base/bin/snap_update_test.py index 9a714efbe..023d965e4 100755 --- a/providers/base/bin/snap_update_test.py +++ b/providers/base/bin/snap_update_test.py @@ -197,6 +197,21 @@ def wait_for_snap_change(self, change_id, type, timeout=300): if result == "Done": logger.info("%s snap %s complete", self.name, type) return + elif result == "Error": + tasks = self.snapd.tasks(str(change_id)) + for task in tasks: + logger.error("%s | %s | %s", + task["id"], + task["status"], + task["summary"] + ) + if task.get("log"): + for log in task["log"]: + logger.error("\t %s", log) + raise SystemExit("Error during snap {} {}.".format( + self.name, type) + ) + current_time = time.time() if current_time - start_time >= timeout: diff --git a/providers/base/tests/test_snap_update_test.py b/providers/base/tests/test_snap_update_test.py index 17e1a9483..6f8830434 100644 --- a/providers/base/tests/test_snap_update_test.py +++ b/providers/base/tests/test_snap_update_test.py @@ -199,7 +199,29 @@ def test_wait_for_snap_change(self): mock_snap_info = MagicMock() mock_self.snap_info = mock_snap_info mock_self.snapd.change.return_value = "Done" - snap_update_test.SnapRefreshRevert.wait_for_snap_change(mock_self, change_id=1) + snap_update_test.SnapRefreshRevert.wait_for_snap_change( + mock_self, change_id=1, type="refresh" + ) + + def test_wait_for_snap_change_error(self): + mock_self = MagicMock() + mock_snap_info = MagicMock() + mock_self.snap_info = mock_snap_info + mock_self.snapd.change.return_value = "Error" + mock_self.snapd.tasks.return_value = [{ + "id": "3285", + "kind": "auto-connect", + "log": ["ERROR cannot finish pi-kernel installation"], + "progress": {"done": 1, "label": "", "total": 1}, + "ready-time": "2023-10-20T04:36:29.493419161Z", + "spawn-time": "2023-10-20T04:34:44.614034129Z", + "status": "Error", + "summary": "Automatically connect eligible plugs and slots" + }] + with self.assertRaises(SystemExit): + snap_update_test.SnapRefreshRevert.wait_for_snap_change( + mock_self, change_id=1, type="refresh" + ) def test_wait_for_snap_change_timeout(self): mock_self = MagicMock() @@ -207,7 +229,7 @@ def test_wait_for_snap_change_timeout(self): mock_self.snap_info = mock_snap_info with self.assertRaises(SystemExit): snap_update_test.SnapRefreshRevert.wait_for_snap_change( - mock_self, change_id=1, timeout=-1 + mock_self, change_id=1, type="refresh", timeout=-1 ) @patch("snap_update_test.time.time") @@ -218,4 +240,6 @@ def test_wait_for_snap_change_ongoing(self, mock_sleep, mock_time): mock_self.snap_info = mock_snap_info mock_self.snapd.change.side_effect = ["Doing", "Done"] mock_time.return_value = 1 - snap_update_test.SnapRefreshRevert.wait_for_snap_change(mock_self, change_id=1) + snap_update_test.SnapRefreshRevert.wait_for_snap_change( + mock_self, change_id=1, type="refresh" + ) From ef66718b4706f43a534d41d26a6f0bcf5d176bad Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Fri, 20 Oct 2023 14:49:13 +0800 Subject: [PATCH 17/18] Flake 8 fixes --- providers/base/bin/snap_update_test.py | 32 +++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/providers/base/bin/snap_update_test.py b/providers/base/bin/snap_update_test.py index 023d965e4..b6c17bfe8 100755 --- a/providers/base/bin/snap_update_test.py +++ b/providers/base/bin/snap_update_test.py @@ -200,19 +200,19 @@ def wait_for_snap_change(self, change_id, type, timeout=300): elif result == "Error": tasks = self.snapd.tasks(str(change_id)) for task in tasks: - logger.error("%s | %s | %s", - task["id"], - task["status"], - task["summary"] + logger.error( + "%s | %s | %s", + task["id"], + task["status"], + task["summary"], ) if task.get("log"): for log in task["log"]: logger.error("\t %s", log) - raise SystemExit("Error during snap {} {}.".format( - self.name, type) + raise SystemExit( + "Error during snap {} {}.".format(self.name, type) ) - current_time = time.time() if current_time - start_time >= timeout: raise SystemExit( @@ -222,15 +222,18 @@ def wait_for_snap_change(self, change_id, type, timeout=300): ) logger.info("Waiting for %s snap %s to be done...", self.name, - type) + type + ) logger.info("Trying again in 10 seconds...") time.sleep(10) def verify(self, type, timeout=300): logger.info("Beginning verify...") if type not in ("refresh", "revert"): - msg = ("'{}' verification unknown. Can be either 'refresh' " - "or 'revert'.").format(type) + msg = ( + "'{}' verification unknown. Can be either 'refresh' " + "or 'revert'." + ).format(type) raise SystemExit(msg) data = load_change_info(self.path) id = data["change_id"] @@ -243,10 +246,11 @@ def verify(self, type, timeout=300): else: tested_rev = data["original_revision"] if current_rev != tested_rev: - msg = ("Current revision ({}) is different from expected revision " - "({})").format(current_rev, tested_rev) - raise SystemExit( - ) + msg = ( + "Current revision ({}) is different from expected revision " + "({})" + ).format(current_rev, tested_rev) + raise SystemExit() else: logger.info( "PASS: current revision (%s) matches the expected revision", From 1c5d39293e554ea930c1a28c3afa67d756e7a8b6 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Fri, 20 Oct 2023 15:04:13 +0800 Subject: [PATCH 18/18] Fix tests to support Python 3.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit assert_called() was introduced in 3.6... 😭 --- providers/base/tests/test_snap_update_test.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/providers/base/tests/test_snap_update_test.py b/providers/base/tests/test_snap_update_test.py index 6f8830434..033fb7554 100644 --- a/providers/base/tests/test_snap_update_test.py +++ b/providers/base/tests/test_snap_update_test.py @@ -47,20 +47,20 @@ def test_load_change_info_file_not_found(self, mock_open): @patch("snap_update_test.json") def test_load_change_info(self, mock_json, mock_open): snap_update_test.load_change_info("test") - mock_json.load.assert_called() + self.assertTrue(mock_json.load.called) @patch("snap_update_test.print_resource_info") def test_main_print_resource(self, mock_print_resource_info): args = ["--resource"] snap_update_test.main(args) - mock_print_resource_info.assert_called() + self.assertTrue(mock_print_resource_info.called) @patch("snap_update_test.SnapRefreshRevert") def test_main_refresh(self, mock_srr): args = ["--refresh", "--info-path", "/tmp/change.json", "chromium"] snap_update_test.main(args) instance = mock_srr.return_value - instance.snap_refresh.assert_called() + self.assertTrue(instance.snap_refresh.called) @patch("snap_update_test.SnapRefreshRevert") def test_main_verify_refresh(self, mock_srr): @@ -74,7 +74,7 @@ def test_main_revert(self, mock_srr): args = ["--revert", "--info-path", "/tmp/change.json", "chromium"] snap_update_test.main(args) instance = mock_srr.return_value - instance.snap_revert.assert_called() + self.assertTrue(instance.snap_revert.called) @patch("snap_update_test.SnapRefreshRevert") def test_main_verify_revert(self, mock_srr): @@ -143,7 +143,7 @@ def test_snap_refresh_different_revision( mock_self.snap_info = mock_snap_info logging.disable(logging.CRITICAL) snap_update_test.SnapRefreshRevert.snap_refresh(mock_self) - snap_update_test.save_change_info.assert_called() + self.assertTrue(snap_update_test.save_change_info.called) @patch("snap_update_test.Snapd.revert") @patch("snap_update_test.load_change_info") @@ -155,8 +155,8 @@ def test_snap_revert( mock_snap_info = MagicMock() mock_self.snap_info = mock_snap_info snap_update_test.SnapRefreshRevert.snap_revert(mock_self) - snap_update_test.load_change_info.assert_called() - snap_update_test.save_change_info.assert_called() + self.assertTrue(snap_update_test.load_change_info.called) + self.assertTrue(snap_update_test.save_change_info.called) def test_verify_invalid(self): mock_self = MagicMock()