Files
StarPilot/openpilot/starpilot/galaxy/tests/test_software_operations.py
T
firestar5683 a82935f326 User interfaces and Android Auto
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: pharmacomaniac <blittle65@gmail.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

571 lines
28 KiB
Python

from datetime import datetime
from pathlib import Path
import tempfile
import unittest
from types import SimpleNamespace
from unittest import mock
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.software_operations import (
SoftwareOperations, SoftwareOperationError, UpdaterProcess, PROCESS_TITLE,
)
from openpilot.starpilot.software.update_control import UpdaterControlError
class FakeProcess:
def __init__(self):
self.running = True
self.sent = []
self.fast_requests = []
def available(self):
return self.running
def send(self, sig, **identity):
if not self.running:
raise SoftwareOperationError("Updater stopped", 503)
self.sent.append(sig)
if sig in ("update", "fast"):
self.fast_requests.append(identity)
def close(self):
pass
class SoftwareOperationsTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
root = Path(self.temp.name)
self.params = Params(str(root / "params"))
self.params.put("Version", "test", block=True)
self.params.put("GitBranch", "main", block=True)
self.params.put("GitCommit", "a" * 40, block=True)
self.params.put("UpdaterState", "idle", block=True)
self.params.put("UpdaterTargetBranch", "main", block=True)
self.params.put("UpdaterAvailableBranches", "main,release", block=True)
self.params.put_bool("IsOffroad", True, block=True)
self.params.put_bool("UpdateAvailable", False, block=True)
self.params.put_bool("DisableUpdates", False, block=True)
self.params.put("UpdateFailedCount", 0, block=True)
self.process = FakeProcess()
self.parked = True
self.authorized = True
self.finalized = root / "finalized"
self.finalized.mkdir()
self.installed = root / "installed"
self.installed.mkdir()
self.git = {self.installed: ("main", "a" * 40), self.finalized: ("release", "b" * 40)}
self.owner = SoftwareOperations(self.params, parked=lambda: self.parked, process=self.process,
finalized=self.finalized, installed=self.installed,
git_identity=lambda path: self.git.get(path))
self.addCleanup(self.owner.close)
def act(self, action, branch=None):
body = {"action": action}
if branch is not None:
body["branch"] = branch
return self.owner.action(action, body, authorized=lambda: self.authorized)
def denied(self, action, status, branch=None):
with self.assertRaises(SoftwareOperationError) as caught:
self.act(action, branch)
self.assertEqual(caught.exception.status, status)
def test_snapshot_refresh_precedes_parked_read_and_relocks_without_signaling(self):
order = []
def refresh():
order.append('refresh')
self.parked = not self.parked
return True
def parked():
order.append('parked')
return self.parked
self.parked = False
self.owner = SoftwareOperations(self.params, parked=parked, process=self.process, refresh=refresh,
finalized=self.finalized, installed=self.installed, git_identity=lambda path: self.git.get(path))
self.addCleanup(self.owner.close)
for expected in (True, False):
order.clear()
view = self.owner.snapshot(include_history=False)
self.assertEqual(order, ['refresh', 'parked'])
self.assertIs(view['parked'], expected)
for key in ('canCheck', 'canDownload', 'canSelect'):
self.assertIs(view[key], expected)
self.assertIsNone(view['request'])
self.assertEqual(self.process.sent, [])
def test_failed_refresh_never_uses_cached_parked_authority(self):
self.parked = True
for outcome in (False, None, 1, OSError('collector failed')):
with self.subTest(outcome=outcome):
refresh = mock.Mock(side_effect=outcome) if isinstance(outcome, Exception) else mock.Mock(return_value=outcome)
parked = mock.Mock(return_value=True)
owner = SoftwareOperations(self.params, parked=parked, process=self.process, refresh=refresh,
finalized=self.finalized, installed=self.installed, git_identity=lambda path: self.git.get(path))
self.addCleanup(owner.close)
view = owner.snapshot(include_history=False)
refresh.assert_called_once_with()
parked.assert_not_called()
self.assertIs(view['parked'], False)
for key in ('canCheck', 'canDownload', 'canSelect', 'canInstall', 'canFastUpdate', 'canConfigure'):
self.assertIs(view[key], False)
self.assertIsNone(view['request'])
self.assertEqual(self.process.sent, [])
def test_idle_is_not_reported_as_update_progress(self):
view = self.owner.snapshot(include_history=False)
self.assertIsNone(view["progress"]["stage"])
self.assertEqual(view["progress"]["detail"], "")
def test_normal_finalization_completes_only_after_idle_success(self):
for failed in (False, True):
with self.subTest(failed=failed):
self.owner.request = None
self.params.put("UpdateFailedCount", 0, block=True)
self.params.put("UpdaterState", "idle", block=True)
self.params.remove("LastUpdateTime")
self.act("download", "main")
self.params.put("UpdaterState", "finalizing update...", block=True)
self.params.put("UpdaterFastState", {"version": 1, "stage": "complete", "detail": "Old fast update"}, block=True)
view = self.owner.snapshot()
self.assertEqual(view["request"]["state"], "pending")
self.assertEqual(view["progress"], {"stage": "finalizing update...", "detail": "", "percent": None})
self.params.put("UpdaterState", "idle", block=True)
if failed:
self.params.put("UpdateFailedCount", 1, block=True)
self.params.put("LastUpdateException", "Finalization copy failed", block=True)
else:
self.params.put("LastUpdateTime", datetime(2026, 10, 6), block=True)
self.params.put("UpdaterLastFetchTime", datetime(2026, 10, 6), block=True)
result = self.owner.snapshot()["request"]
self.assertEqual(result["state"], "failed" if failed else "complete")
if failed:
self.assertEqual(result["error"], "Finalization copy failed")
def test_unavailable_finalizing_preserves_request_until_status_recovers(self):
self.act("download", "main")
self.params.put("UpdaterState", "finalizing update...", block=True)
self.process.running = False
view = self.owner.snapshot()
self.assertEqual(view["progress"]["stage"], "unavailable")
self.assertIn("device updater service", view["progress"]["detail"])
self.assertNotIn("last reported state", view["progress"]["detail"])
self.assertEqual(view["request"]["state"], "pending")
self.assertEqual(view["reason"], "Updater status is unavailable")
self.assertFalse(view["canCheck"])
self.assertEqual(self.params.get("UpdaterState"), "finalizing update...")
self.process.running = True
self.params.put("UpdaterState", "idle", block=True)
self.params.put("LastUpdateTime", datetime(2026, 10, 6), block=True)
self.params.put("UpdaterLastFetchTime", datetime(2026, 10, 6), block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "complete")
def test_running_finalization_timeout_does_not_fabricate_completion(self):
self.owner.clock = lambda: 0.0
self.act("download", "main")
self.params.put("UpdaterState", "finalizing update...", block=True)
self.owner.clock = lambda: 901.0
view = self.owner.snapshot()
self.assertEqual(view["progress"], {"stage": "timed_out",
"detail": "Request timed out; the updater may still be running", "percent": None})
self.assertTrue(view["updaterAvailable"])
self.assertEqual(view["request"]["state"], "failed")
self.assertFalse(view["canCheck"])
self.assertEqual(self.params.get("UpdaterState"), "finalizing update...")
def test_recent_version_admission_pins_installed_snapshot_and_listed_target(self):
selected = 'c' * 40
recent = {'branch': 'main', 'head': 'b' * 40,
'entries': [{'hash': selected, 'date': '2026-09-29T12:00:00+00:00', 'subject': 'Previous build'}]}
payload = {'action': 'version', 'branch': 'main', 'expectedCommit': 'a' * 40, 'selectedCommit': selected}
with mock.patch('openpilot.starpilot.galaxy.software_operations.recent_versions', return_value=recent), \
mock.patch.object(self.process, 'send') as send:
for field, value in (('selectedCommit', 'f' * 40), ('expectedCommit', 'b' * 40), ('branch', 'release')):
with self.subTest(field=field), self.assertRaises(SoftwareOperationError):
self.owner.action('version', dict(payload, **{field: value}), authorized=lambda: True)
self.parked = False
with self.assertRaises(SoftwareOperationError):
self.owner.action('version', payload, authorized=lambda: True)
self.parked = True
self.owner.action('version', payload, authorized=lambda: True)
send.assert_called_once_with('version', branch='main', commit='a' * 40, selected_commit=selected)
self.assertIsNotNone(self.owner.request)
assert self.owner.request is not None
self.assertEqual(self.owner.request['selectedCommit'], selected)
def test_version_installed_snapshot_cannot_change_at_final_dispatch(self):
selected = 'c' * 40
recent = {'branch': 'main', 'head': selected,
'entries': [{'hash': selected, 'date': '2026-09-29T12:00:00+00:00', 'subject': 'Build'}]}
calls = 0
def authorized():
nonlocal calls
calls += 1
if calls == 3:
self.params.put('GitCommit', 'd' * 40, block=True)
self.git[self.installed] = ('main', 'd' * 40)
return True
with mock.patch('openpilot.starpilot.galaxy.software_operations.recent_versions', return_value=recent), \
mock.patch.object(self.process, 'send') as send, self.assertRaises(SoftwareOperationError):
self.owner.action('version', {'action': 'version', 'branch': 'main', 'expectedCommit': 'a' * 40,
'selectedCommit': selected}, authorized=authorized)
send.assert_not_called()
def test_listing_request_completes_without_install_timestamp_or_reboot(self):
self.params.put('LastUpdateTime', datetime(2026, 9, 1), block=True)
self.params.put('UpdaterLastFetchTime', datetime(2026, 9, 1), block=True)
with mock.patch.object(self.process, 'send') as send:
self.act('versions', 'main')
send.assert_called_once_with('versions', branch='main', commit='a' * 40)
self.params.put('UpdaterLastFetchTime', datetime(2026, 9, 2), block=True)
state = self.owner.snapshot()['request']
self.assertEqual(state['state'], 'complete')
self.assertNotIn('outcome', state)
self.assertEqual(self.params.get('LastUpdateTime'), datetime(2026, 9, 1))
self.assertFalse(self.params.get_bool('DoReboot'))
def test_select_then_check_then_download_signals_exact_updater(self):
self.assertTrue(self.owner.snapshot()["canSelect"])
self.assertEqual(self.owner.snapshot()["selectedTarget"], "main")
selected = self.act("select", "release")
self.assertEqual(selected["selectedTarget"], "release")
self.assertEqual(self.params.get("UpdaterTargetBranch"), "release")
self.assertEqual(self.process.sent, [])
checked = self.act("check")
self.assertEqual(checked["request"]["state"], "pending")
self.assertEqual(self.process.sent, ["check"])
self.denied("download", 409, "release")
self.params.put("UpdaterState", "checking...", block=True)
self.owner.snapshot()
self.params.put("UpdaterState", "idle", block=True)
self.params.put("LastUpdateTime", datetime(2026, 9, 27, 12, 0), block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "complete")
downloaded = self.act("download", "release")
self.assertEqual(downloaded["request"]["target"], "release")
self.assertEqual(self.process.sent, ["check", "download"])
self.denied("download", 409, "release")
def test_fresh_manager_without_target_allows_check_and_select(self):
self.params.remove("UpdaterTargetBranch")
initial = self.owner.snapshot()
self.assertEqual(initial["selectedTarget"], "main")
self.assertTrue(initial["canCheck"])
self.assertTrue(initial["canSelect"])
check = self.act("check")
self.assertIsNone(check["request"]["target"])
self.assertEqual(check["request"]["state"], "pending")
self.params.put("UpdaterTargetBranch", "main", block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "pending")
self.params.put("LastUpdateTime", datetime(2026, 9, 27, 12, 4), block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "complete")
self.act("select", "release")
self.assertEqual(self.params.get("UpdaterTargetBranch"), "release")
def test_bad_or_stale_branch_and_missing_updater_never_mutate(self):
self.denied("select", 400, "--help")
self.denied("select", 409, "unknown")
self.assertEqual(self.params.get("UpdaterTargetBranch"), "main")
self.process.running = False
self.denied("check", 503)
self.assertEqual(self.process.sent, [])
def test_parked_authority_and_session_rechecked_before_effect(self):
for offroad, parked, authorized in ((False, True, True), (True, False, True), (True, True, False)):
with self.subTest(offroad=offroad, parked=parked, authorized=authorized):
self.params.put_bool("IsOffroad", offroad, block=True)
self.parked = parked
self.authorized = authorized
self.denied("check", 403 if not authorized else 409)
self.assertEqual(self.process.sent, [])
self.params.put_bool("IsOffroad", True, block=True)
self.parked = self.authorized = True
checks = iter((True, False))
with self.assertRaises(SoftwareOperationError):
self.owner.action("check", {"action": "check"}, authorized=lambda: next(checks))
self.assertEqual(self.process.sent, [])
def test_install_requires_exact_finalized_target_and_reboot_once(self):
self.act("select", "release")
self.params.put_bool("UpdateAvailable", True, block=True)
self.params.put("UpdaterNewDescription", "test / release / bbbbbbb / Sep 27", block=True)
with mock.patch("openpilot.starpilot.galaxy.software_operations.validate_revision", return_value={}):
self.assertFalse(self.owner.snapshot()["canInstall"])
(self.finalized / ".overlay_consistent").touch()
self.assertTrue(self.owner.snapshot()["canInstall"])
self.denied("install", 409, "main")
self.git[self.finalized] = ("release", "c" * 40)
self.assertFalse(self.owner.snapshot()["canInstall"])
self.denied("install", 409, "release")
self.git[self.finalized] = ("release", "b" * 40)
result = self.act("install", "release")
self.assertEqual(result["request"]["state"], "pending")
self.assertEqual(result["request"]["action"], "install")
self.assertTrue(self.params.get_bool("DoReboot"))
self.denied("install", 409, "release")
self.assertEqual(self.process.sent, [])
def test_target_change_or_failure_is_reported(self):
self.act("select", "release")
self.act("download", "release")
self.params.put("UpdaterTargetBranch", "main", block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "failed")
self.act("select", "release")
self.act("check")
self.params.put("UpdateFailedCount", 1, block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "failed")
def test_counter_reset_does_not_claim_a_completed_check(self):
self.params.put("UpdateFailedCount", 2, block=True)
self.act("check")
self.params.put("UpdateFailedCount", 0, block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "pending")
self.params.put("LastUpdateTime", datetime(2026, 9, 27, 12, 1), block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "complete")
def test_download_without_fetch_cannot_claim_unpublished_target(self):
self.act("select", "release")
self.act("download", "release")
self.params.put_bool("UpdaterFetchAvailable", False, block=True)
self.params.put("LastUpdateTime", datetime(2026, 9, 27, 12, 2), block=True)
request = self.owner.snapshot()["request"]
self.assertEqual(request["state"], "failed")
self.assertIn("No new download", request["error"])
def test_download_does_not_call_missing_remote_branch_up_to_date(self):
self.act("download", "main")
self.params.put_bool("UpdaterFetchAvailable", False, block=True)
self.params.put("LastUpdateTime", datetime(2026, 9, 27, 12, 3), block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "failed")
def test_catalog_keeps_other_branches_when_one_entry_is_invalid(self):
self.params.put("UpdaterAvailableBranches", "main,feature+lab,bad branch,feature@lab", block=True)
branches = self.owner.snapshot()["availableBranches"]
self.assertEqual(branches, ["main", "feature+lab", "feature@lab"])
self.act("select", "feature+lab")
self.assertEqual(self.params.get("UpdaterTargetBranch"), "feature+lab")
def test_published_catalog_with_more_than_64_branches_remains_usable(self):
branches = [f"release/{number:03d}" for number in range(76)]
self.params.put("UpdaterAvailableBranches", ",".join(branches), block=True)
snapshot = self.owner.snapshot()
self.assertEqual(snapshot["availableBranches"], branches)
self.assertTrue(snapshot["canSelect"])
self.act("select", branches[-1])
self.assertEqual(self.params.get("UpdaterTargetBranch"), branches[-1])
def test_slash_branch_description_can_match_finalized_commit(self):
self.params.put("UpdaterAvailableBranches", "main,release/2026", block=True)
self.act("select", "release/2026")
self.git[self.finalized] = ("release/2026", "b" * 40)
self.params.put_bool("UpdateAvailable", True, block=True)
self.params.put("UpdaterNewDescription", "test / release/2026 / bbbbbbb / Sep 27", block=True)
(self.finalized / ".overlay_consistent").touch()
with mock.patch("openpilot.starpilot.galaxy.software_operations.validate_revision", return_value={}):
self.assertTrue(self.owner.snapshot()["canInstall"])
def save_downloads(self, enabled, expected):
return self.owner.action('preferences', {'action': 'preferences', 'automaticDownloads': enabled,
'expectedAutomaticDownloads': expected}, authorized=lambda: self.authorized)
def test_download_preference_is_independent_of_manual_updater_commands(self):
self.assertIs(self.owner.snapshot()['automaticDownloads'], True)
result = self.save_downloads(False, True)
self.assertIs(result['automaticDownloads'], False)
self.assertIsNone(result['request'])
self.assertEqual(self.process.sent, [])
self.assertTrue(result['canDownload'])
self.act('download', 'main')
self.assertEqual(self.process.sent, ['download'])
def test_preference_can_be_saved_without_running_updater_but_not_after_authority_changes(self):
self.process.running = False
self.assertTrue(self.owner.snapshot()['canConfigure'])
self.assertFalse(self.save_downloads(False, True)['automaticDownloads'])
for case in ('stale', 'onroad', 'unauthorized', 'reboot'):
self.parked = case != 'onroad'
self.authorized = case != 'unauthorized'
self.params.put_bool('DoReboot', case == 'reboot', block=True)
with self.assertRaises(SoftwareOperationError):
self.save_downloads(True, True if case == 'stale' else False)
self.assertFalse(self.params.get_bool('UpdaterAutomaticDownloads'))
self.assertEqual(self.process.sent, [])
def test_invalid_preference_can_be_repaired_without_implicit_enabling(self):
Path(self.params.get_param_path('UpdaterAutomaticDownloads')).write_bytes(b'bad')
self.assertIsNone(self.owner.snapshot()['automaticDownloads'])
self.assertFalse(self.save_downloads(False, None)['automaticDownloads'])
with self.assertRaises(SoftwareOperationError):
self.save_downloads(1, False)
def test_repository_link_uses_only_normalized_github_origins(self):
for remote in ('https://github.com/owner/repo.git', 'git@github.com:owner/repo.git', 'ssh://git@github.com/owner/repo'):
self.params.put('GitRemote', remote, block=True)
self.assertEqual(self.owner.snapshot(include_history=False)['repository'], 'owner/repo')
self.params.put('GitRemote', 'https://foreign.test/owner/repo', block=True)
self.assertIsNone(self.owner.snapshot(include_history=False)['repository'])
def test_lightweight_status_preserves_controls_without_reading_history(self):
with mock.patch.object(self.owner, '_history_snapshot') as history:
light = self.owner.snapshot(include_history=False)
self.assertNotIn('history', light)
history.assert_not_called()
full = self.owner.snapshot()
history.assert_called_once()
self.assertEqual(light, {key: value for key, value in full.items() if key != 'history'})
def test_history_caches_local_reads_and_refreshes_when_download_identity_changes(self):
reader = mock.Mock(side_effect=lambda path, sha: [{'hash': sha, 'date': '2026-09-29T12:00:00+00:00', 'subject': path.name}])
self.owner.history_reader = reader
self.params.put('UpdaterCurrentReleaseNotes', b'<h1>Current</h1>', block=True)
self.params.put('UpdaterNewReleaseNotes', b'<h1>Next</h1>', block=True)
with mock.patch('openpilot.starpilot.galaxy.software_operations.recent_versions',
return_value={'branch': 'main', 'head': 'c' * 40, 'entries': []}) as recent:
first = self.owner.snapshot()['history']
self.assertEqual(first['installed'][0]['hash'], 'a' * 40)
self.assertEqual(first['downloaded'][0]['hash'], 'b' * 40)
self.assertEqual(first['currentReleaseNotes'], 'Current')
self.assertEqual(first['downloadedReleaseNotes'], 'Next')
self.assertEqual(reader.call_count, 2)
self.assertEqual(self.owner.snapshot()['history'], first)
self.assertEqual(reader.call_count, 2)
self.assertEqual(recent.call_count, 1)
self.params.put('UpdaterNewDescription', 'new description', block=True)
self.owner.snapshot()
self.assertEqual(reader.call_count, 4)
self.assertEqual(recent.call_count, 2)
self.params.put('UpdaterLastFetchTime', datetime(2026, 10, 4, 12), block=True)
self.owner.snapshot()
self.assertEqual(recent.call_count, 3)
self.assertEqual(reader.call_count, 6)
def test_unified_update_pins_source_and_tracks_completion(self):
result = self.act("update", "release")
self.assertEqual(result["request"]["action"], "update")
self.assertEqual(result["request"]["target"], "release")
self.assertEqual(self.process.sent, ["update"])
self.assertEqual(self.process.fast_requests, [{"branch": "release", "commit": "a" * 40}])
self.params.put("LastUpdateTime", datetime(2026, 10, 3, 12), block=True)
self.params.put("UpdaterLastFetchTime", datetime(2026, 10, 3, 12), block=True)
self.params.put_bool("DoReboot", True, block=True)
result = self.owner.snapshot()["request"]
self.assertEqual(result["state"], "complete")
self.assertEqual(result["outcome"], "restarting")
def test_fast_current_branch_without_branch_list_or_selected_target(self):
self.params.remove("UpdaterAvailableBranches")
self.params.put("UpdaterTargetBranch", "release", block=True)
self.assertTrue(self.owner.snapshot()["canFastUpdate"])
result = self.act("fast", "main")
self.assertEqual(result["request"]["target"], "main")
self.assertEqual(self.process.sent, ["fast"])
self.assertEqual(self.process.fast_requests, [{"branch": "main", "commit": "a" * 40}])
self.assertEqual(self.params.get("UpdaterTargetBranch"), "release")
self.assertFalse(self.params.get_bool("DoReboot"))
self.params.put("LastUpdateTime", datetime(2026, 10, 3, 12), block=True)
self.params.put("UpdaterLastFetchTime", datetime(2026, 10, 3, 12), block=True)
result = self.owner.snapshot()["request"]
self.assertEqual(result["state"], "complete")
self.assertEqual(result["outcome"], "up_to_date")
def test_fast_uses_registered_offroad_and_live_parked_authority(self):
Path(self.params.get_param_path("IsOnroad")).write_bytes(b"1")
self.assertTrue(self.owner.snapshot()["canFastUpdate"])
self.parked = False
self.assertFalse(self.owner.snapshot()["canFastUpdate"])
self.denied("fast", 409, "main")
self.parked = True
self.act("fast", "main")
self.assertEqual(self.process.sent, ["fast"])
def test_fast_rejects_stale_installed_identity(self):
self.git[self.installed] = ("main", "b" * 40)
self.denied("fast", 409, "main")
self.git[self.installed] = ("release", "a" * 40)
self.denied("fast", 409, "main")
self.assertEqual(self.process.sent, [])
self.assertFalse(self.params.get_bool("DoReboot"))
def test_fast_keeps_existing_admission_and_observes_failure(self):
for key in ("DisableUpdates", "DoReboot"):
self.params.put_bool(key, True, block=True)
self.denied("fast", 409, "main")
self.params.put_bool(key, False, block=True)
self.authorized = False
self.denied("fast", 403, "main")
self.authorized = True
self.act("fast", "main")
self.denied("fast", 409, "main")
self.params.put("UpdateFailedCount", 1, block=True)
self.assertEqual(self.owner.snapshot()["request"]["state"], "failed")
self.assertEqual(self.process.sent, ["fast"])
class UpdaterIdentityTest(unittest.TestCase):
def test_fresh_manager_pid_and_same_proc_start_before_signal(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
proc = root / "123"
proc.mkdir()
(proc / "stat").write_text("123 (updater) " + " ".join(["S", *(["0"] * 18), "3456"]))
(proc / "cmdline").write_bytes(PROCESS_TITLE + b"\0")
state = SimpleNamespace(name="updated", running=True, shouldBeRunning=True, pid=123)
class ManagerMessages:
seen = {"managerState": True}
alive = {"managerState": True}
valid = {"managerState": True}
recv_time = {"managerState": 1.25}
logMonoTime = {"managerState": 1_250_000_000}
def update(self, timeout):
pass
def __getitem__(self, key):
return SimpleNamespace(processes=[state])
sm = ManagerMessages()
clock = [1_000_000_000]
class FakeControl:
def __init__(self):
self.available_calls = []
self.sent = []
def available(self, pid, start):
self.available_calls.append((pid, start))
return True
def send(self, pid, start, action):
self.sent.append((pid, start, action))
control = FakeControl()
process = UpdaterProcess(sm, control=control, proc_root=root, mono_ns=lambda: clock[0])
clock[0] = 1_500_000_000
with mock.patch("os.kill") as kill:
self.assertTrue(process.available())
process.send("check")
self.assertEqual(control.available_calls, [(123, 3456)])
self.assertEqual(control.sent, [(123, 3456, "check")])
with mock.patch.object(control, "send", side_effect=UpdaterControlError("declined")):
with self.assertRaises(SoftwareOperationError) as denied:
process.send("download")
self.assertEqual(denied.exception.status, 503)
with mock.patch.object(process, "_manager_pid", side_effect=[123, 124]):
with self.assertRaises(SoftwareOperationError):
process.send("download")
self.assertEqual(control.sent, [(123, 3456, "check")])
kill.assert_not_called()
sm.recv_time["managerState"] = 0.9
self.assertFalse(process.available())
sm.recv_time["managerState"] = 1.25
(proc / "cmdline").write_bytes(b"other\0")
self.assertFalse(process.available())
with self.assertRaises(SoftwareOperationError):
process.send("download")
if __name__ == "__main__":
unittest.main()