From ac771290414da337e1f800b23b01360c5471ece4 Mon Sep 17 00:00:00 2001 From: Justin Newberry Date: Wed, 6 Mar 2024 18:24:46 -0500 Subject: [PATCH] updated: basic e2e update tests (#31742) * e2e update test * that too * fix * fix * fix running in docker * don't think GHA will work * also test switching branches * it's a test * lets not delete that yet * comment * space --- pyproject.toml | 1 + selfdrive/updated/tests/test_updated.py | 172 ++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100755 selfdrive/updated/tests/test_updated.py diff --git a/pyproject.toml b/pyproject.toml index 99a8602460..0f7734b3b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ testpaths = [ "selfdrive/thermald", "selfdrive/test/longitudinal_maneuvers", "selfdrive/test/process_replay/test_fuzzy.py", + "selfdrive/updated", "system/camerad", "system/hardware/tici", "system/loggerd", diff --git a/selfdrive/updated/tests/test_updated.py b/selfdrive/updated/tests/test_updated.py new file mode 100755 index 0000000000..d8ce9f3394 --- /dev/null +++ b/selfdrive/updated/tests/test_updated.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +import os +import pathlib +import shutil +import signal +import subprocess +import tempfile +import time +import unittest +from unittest import mock + +import pytest +from openpilot.selfdrive.manager.process import ManagerProcess + + +from openpilot.selfdrive.test.helpers import processes_context +from openpilot.common.params import Params + + +def run(args, **kwargs): + return subprocess.run(args, **kwargs, check=True) + + +def update_release(directory, name, version, release_notes): + with open(directory / "RELEASES.md", "w") as f: + f.write(release_notes) + + (directory / "common").mkdir(exist_ok=True) + + with open(directory / "common" / "version.h", "w") as f: + f.write(f'#define COMMA_VERSION "{version}"') + + run(["git", "add", "."], cwd=directory) + run(["git", "commit", "-m", f"openpilot release {version}"], cwd=directory) + + +@pytest.mark.slow # TODO: can we test overlayfs in GHA? +class TestUpdateD(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + + run(["sudo", "mount", "-t", "tmpfs", "tmpfs", self.tmpdir]) # overlayfs doesn't work inside of docker unless this is a tmpfs + + self.mock_update_path = pathlib.Path(self.tmpdir) + + self.params = Params() + + self.basedir = self.mock_update_path / "openpilot" + self.basedir.mkdir() + + self.staging_root = self.mock_update_path / "safe_staging" + self.staging_root.mkdir() + + self.remote_dir = self.mock_update_path / "remote" + self.remote_dir.mkdir() + + mock.patch("openpilot.common.basedir.BASEDIR", self.basedir).start() + + os.environ["UPDATER_STAGING_ROOT"] = str(self.staging_root) + os.environ["UPDATER_LOCK_FILE"] = str(self.mock_update_path / "safe_staging_overlay.lock") + + self.MOCK_RELEASES = { + "release3": ("0.1.2", "0.1.2 release notes"), + "master": ("0.1.3", "0.1.3 release notes"), + } + + def set_target_branch(self, branch): + self.params.put("UpdaterTargetBranch", branch) + + def setup_basedir_release(self, release): + self.params = Params() + self.set_target_branch(release) + run(["git", "clone", "-b", release, self.remote_dir, self.basedir]) + + def update_remote_release(self, release): + update_release(self.remote_dir, release, *self.MOCK_RELEASES[release]) + + def setup_remote_release(self, release): + run(["git", "init"], cwd=self.remote_dir) + run(["git", "checkout", "-b", release], cwd=self.remote_dir) + self.update_remote_release(release) + + def tearDown(self): + mock.patch.stopall() + run(["sudo", "umount", "-l", str(self.staging_root / "merged")]) + run(["sudo", "umount", "-l", self.tmpdir]) + shutil.rmtree(self.tmpdir) + + def send_check_for_updates_signal(self, updated: ManagerProcess): + updated.signal(signal.SIGUSR1.value) + + def send_download_signal(self, updated: ManagerProcess): + updated.signal(signal.SIGHUP.value) + + def _test_params(self, branch, fetch_available, update_available): + self.assertEqual(self.params.get("UpdaterTargetBranch", encoding="utf-8"), branch) + self.assertEqual(self.params.get_bool("UpdaterFetchAvailable"), fetch_available) + self.assertEqual(self.params.get_bool("UpdateAvailable"), update_available) + + def _test_update_params(self, branch, version, release_notes): + self.assertTrue(self.params.get("UpdaterNewDescription", encoding="utf-8").startswith(f"{version} / {branch}")) + self.assertEqual(self.params.get("UpdaterNewReleaseNotes", encoding="utf-8"), f"

{release_notes}

\n") + + def wait_for_idle(self, timeout=5, min_wait_time=2): + start = time.monotonic() + time.sleep(min_wait_time) + + while True: + waited = time.monotonic() - start + if self.params.get("UpdaterState", encoding="utf-8") == "idle": + print(f"waited {waited}s for idle") + break + + if waited > timeout: + raise TimeoutError("timed out waiting for idle") + + time.sleep(1) + + def test_new_release(self): + # Start on release3, simulate a release3 commit, ensure we fetch that update properly + self.setup_remote_release("release3") + self.setup_basedir_release("release3") + + with processes_context(["updated"]) as [updated]: + self._test_params("release3", False, False) + time.sleep(1) + self._test_params("release3", False, False) + + self.MOCK_RELEASES["release3"] = ("0.1.3", "0.1.3 release notes") + self.update_remote_release("release3") + + self.send_check_for_updates_signal(updated) + + self.wait_for_idle() + + self._test_params("release3", True, False) + + self.send_download_signal(updated) + + self.wait_for_idle() + + self._test_params("release3", False, True) + self._test_update_params("release3", *self.MOCK_RELEASES["release3"]) + + def test_switch_branches(self): + # Start on release3, request to switch to master manually, ensure we switched + self.setup_remote_release("release3") + self.setup_remote_release("master") + self.setup_basedir_release("release3") + + with processes_context(["updated"]) as [updated]: + self._test_params("release3", False, False) + self.wait_for_idle() + self._test_params("release3", False, False) + + self.set_target_branch("master") + self.send_check_for_updates_signal(updated) + + self.wait_for_idle() + + self._test_params("master", True, False) + + self.send_download_signal(updated) + + self.wait_for_idle() + + self._test_params("master", False, True) + self._test_update_params("master", *self.MOCK_RELEASES["master"]) + + +if __name__ == "__main__": + unittest.main()