You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
172 lines
5.4 KiB
172 lines
5.4 KiB
#!/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"<p>{release_notes}</p>\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()
|
|
|