Files
StarPilot/openpilot/system/updated/tests/test_vendored_update.py
T

533 lines
27 KiB
Python

"""Updater Git integration against temporary local repositories.
Native Params, logging, hardware, and consistency syncing are explicit test doubles.
Git fetch/checkout/reset/clean and finalization copies run against real temporary
repositories. No device paths, mounts, firmware calls, or network remotes are used.
"""
import importlib.util
import json
import os
import shutil
import subprocess
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
from types import ModuleType
from unittest.mock import Mock, patch
from openpilot.common.vendor_manifest import DEPENDENCIES
from openpilot.starpilot.software.auto_install import InstallSettings
BRANCH = "test-vendored"
class MemoryParams:
def __init__(self):
self.values = {"UpdaterTargetBranch": BRANCH}
def get(self, key):
return self.values.get(key)
def get_bool(self, key):
return bool(self.values.get(key))
def put(self, key, value, **kwargs):
self.values[key] = value
put_bool = put
def load_updater():
"""Load unchanged updater code with explicit service boundaries substituted."""
services = {
"openpilot.common.params": {"Params": MemoryParams},
"openpilot.common.time_helpers": {"system_time_valid": Mock()},
"openpilot.common.markdown": {"parse_markdown": Mock()},
"openpilot.common.swaglog": {"cloudlog": Mock()},
"openpilot.selfdrive.selfdrived.alertmanager": {"set_offroad_alert": Mock()},
"openpilot.common.hardware": {"AGNOS": False, "HARDWARE": Mock()},
"openpilot.starpilot.drive_state.evidence": {"PhysicalSource": Mock()},
"openpilot.common.version": {"get_build_metadata": Mock()},
}
modules = {}
for name, attributes in services.items():
module = ModuleType(name)
module.__dict__.update(attributes)
modules[name] = module
path = Path(__file__).resolve().parents[1] / "updated.py"
spec = importlib.util.spec_from_file_location("updater_git_test", path)
assert spec is not None and spec.loader is not None
updated = importlib.util.module_from_spec(spec)
with patch.dict(sys.modules, modules):
spec.loader.exec_module(updated)
return updated
class TestVendoredUpdate(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.source = self.root / "source"
self.merged = self.root / "merged"
self.finalized = self.root / "finalized"
self.source.mkdir()
self.env_patch = patch.dict(os.environ, {
"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": os.devnull,
"GIT_AUTHOR_NAME": "Updater test", "GIT_COMMITTER_NAME": "Updater test",
"GIT_AUTHOR_EMAIL": "updater@example.invalid", "GIT_COMMITTER_EMAIL": "updater@example.invalid",
})
self.env_patch.start()
self.addCleanup(self.env_patch.stop)
self.git(self.source, "init", "-b", BRANCH)
for name in DEPENDENCIES:
folder = self.source / name
folder.mkdir()
(folder / "README.md").write_text(f"{name} tracked source\n")
self.manifest = {
"schema_version": 1,
"upstream": {"url": "https://github.com/commaai/openpilot.git", "commit": "1" * 40},
"dependencies": [
{"path": name, "url": f"https://github.com/commaai/{name}.git", "commit": "2" * 40, "tree": "3" * 40, "exclude": []}
for name in DEPENDENCIES
],
}
self.write_manifest()
self.initial_commit = self.commit_source("Initial vendored tree")
self.git(self.root, "clone", "--no-local", "--no-hardlinks", str(self.source), str(self.merged))
# Simulate inherited Git settings that would otherwise recurse on fetch/reset.
self.git(self.merged, "config", "submodule.recurse", "true")
self.git(self.merged, "config", "fetch.recurseSubmodules", "true")
self.updated = load_updater()
self.updated.OVERLAY_MERGED = str(self.merged)
self.updated.METADATA_ROOT = self.root / 'metadata.git'
self.updated.FINALIZED = str(self.finalized)
self.updated.BASEDIR = str(self.merged)
self.updated.set_consistent_flag = self.set_consistent_flag
self.updated.handle_agnos_update = Mock()
self.updated.init_overlay = Mock()
self.commands = []
real_run = self.updated.run
def bounded_run(command, cwd=None, **kwargs):
if cwd is None:
raise AssertionError("Every updater command must name its isolated working directory")
self.assertTrue(Path(cwd).resolve().is_relative_to(self.root.resolve()))
self.assertNotEqual(command[:2], ["git", "submodule"])
self.commands.append(command)
if command in (["git", "gc"], ["git", "lfs", "prune"]):
return ""
return real_run(command, cwd, **kwargs)
self.updated.run = bounded_run
self.updater = self.updated.Updater(install_settings=InstallSettings(self.root / 'install-settings'))
self.addCleanup(self.updater.close)
@staticmethod
def git(repo, *args):
return subprocess.check_output(["git", "-C", str(repo), *args], stderr=subprocess.STDOUT, text=True).strip()
def write_manifest(self):
(self.source / "upstream-sync.json").write_text(json.dumps(self.manifest))
def commit_source(self, message):
self.git(self.source, "add", "--all")
self.git(self.source, "commit", "-m", message)
return self.git(self.source, "rev-parse", "HEAD")
def set_consistent_flag(self, consistent):
marker = self.finalized / ".overlay_consistent"
if consistent:
marker.touch()
else:
marker.unlink(missing_ok=True)
def assert_rejected_before_checkout(self):
self.updated.AGNOS = True
with self.assertRaises(ValueError):
self.updater.fetch_update()
self.assertEqual(self.git(self.merged, "rev-parse", "HEAD"), self.initial_commit)
self.assertFalse(any(command[:2] == ["git", "checkout"] for command in self.commands))
self.assertFalse((self.finalized / ".overlay_consistent").exists())
self.updated.handle_agnos_update.assert_not_called()
self.assertFalse(self.updater.params.values["UpdateAvailable"])
def test_automatic_update_uses_fast_path_and_only_falls_back_on_incompatibility(self):
fast_update = importlib.import_module("openpilot.starpilot.software.fast_update")
for error in (None, self.updated.FastUpdateIncompatible("AGNOS changed"), self.updated.FastUpdateError("Tracked edits")):
with self.subTest(error=error), patch.object(fast_update, "fast_update", side_effect=error, return_value="reboot-requested"), \
patch.object(self.updater, "normal_update", return_value="reboot-requested") as normal:
if error is not None and not isinstance(error, self.updated.FastUpdateIncompatible):
with self.assertRaises(self.updated.FastUpdateError):
self.updater.fast_update(BRANCH, self.initial_commit, automatic=True)
else:
self.assertEqual(self.updater.fast_update(BRANCH, self.initial_commit, automatic=True), "reboot-requested")
self.assertEqual(normal.call_count, int(isinstance(error, self.updated.FastUpdateIncompatible)))
with patch.object(fast_update, "fast_update", side_effect=self.updated.FastUpdateIncompatible("AGNOS changed")), \
patch.object(self.updater, "normal_update") as normal:
with self.assertRaises(self.updated.FastUpdateIncompatible):
self.updater.fast_update(BRANCH, self.initial_commit)
normal.assert_not_called() # Explicit fast/rollback/version operations keep their contract.
@patch('openpilot.starpilot.drive_state.evidence.PhysicalSource', return_value=Mock(update_ready=Mock(return_value=True)))
def test_download_selected_version_stages_exact_commit_without_installing(self, _physical):
installed = self.root / 'installed'
self.git(self.root, 'clone', '--no-local', str(self.merged), str(installed))
self.updated.BASEDIR = str(installed)
self.git(installed, 'remote', 'set-url', 'origin', str(self.source))
(self.source / 'version.txt').write_text('selected')
selected = self.commit_source('Selected version')
(self.source / 'version.txt').write_text('newest')
latest = self.commit_source('Newer version')
with patch.object(self.updater, 'set_params') as publish:
self.assertEqual(self.updater.fast_update(BRANCH, self.initial_commit, selected, download_only=True), 'downloaded')
publish.assert_called_once_with(True, 0, None)
self.assertEqual(self.git(installed, 'rev-parse', 'HEAD'), self.initial_commit)
self.assertEqual(self.git(self.finalized, 'rev-parse', 'HEAD'), selected)
self.assertEqual(self.git(self.finalized, 'rev-parse', 'refs/starpilot/previous'), self.initial_commit)
self.assertEqual(self.updater.branches[BRANCH], latest)
self.assertTrue(self.updater.update_ready)
self.assertFalse(self.updater.latest_update_ready)
self.assertFalse(self.updater.params.get_bool('DoReboot'))
@patch('openpilot.starpilot.drive_state.evidence.PhysicalSource', return_value=Mock(update_ready=Mock(return_value=True)))
def test_download_selected_version_rejects_changed_source_and_unlisted_target(self, _physical):
for expected, selected in (('f' * 40, self.initial_commit), (self.initial_commit, 'f' * 40)):
with self.subTest(expected=expected), self.assertRaises(self.updated.FastUpdateError):
self.updater.fast_update(BRANCH, expected, selected, download_only=True)
self.updated.init_overlay.assert_not_called()
self.assertEqual(self.git(self.merged, 'rev-parse', 'HEAD'), self.initial_commit)
self.assertFalse(self.updater.params.get_bool('DoReboot'))
def test_metadata_refresh_does_not_wake_background_but_check_does(self):
(self.source / 'new.txt').write_text('available')
self.commit_source('New update')
self.updater.wake_automatic = Mock()
with patch.object(self.updated, 'download_permitted', return_value=True):
self.updater.request_check(branch=BRANCH)
worker = self.updater._check_thread
if worker is not None:
worker.join(timeout=5)
self.updater.wake_automatic.assert_not_called()
self.updated.init_overlay.assert_not_called()
self.updater.request_check(branch=BRANCH, download_after=True)
worker = self.updater._check_thread
if worker is not None:
worker.join(timeout=5)
self.updater.wake_automatic.assert_called_once_with()
def test_scheduled_install_requires_clock_idle_and_fresh_offroad_evidence(self):
installed = self.root / 'installed'
self.git(self.root, 'clone', '--no-local', str(self.merged), str(installed))
self.updated.BASEDIR = str(installed)
self.git(installed, 'remote', 'set-url', 'origin', str(self.source))
(self.source / 'scheduled.txt').write_text('prepared')
self.commit_source('Prepared update for scheduling')
self.assertTrue(self.updater.fetch_update())
self.updater.params.put('UpdaterState', 'idle')
self.updater.params.put_bool('IsOffroad', True)
settings = self.updater.install_settings
current = settings.read()
assert current is not None
settings.save(True, '* * * * *', current['revision'], lambda: True)
physical = Mock(update_ready=Mock(return_value=True))
with patch('openpilot.starpilot.drive_state.evidence.PhysicalSource', return_value=physical), \
patch.object(self.updated, 'system_time_valid', return_value=False):
self.assertFalse(self.updater.maybe_install())
with patch('openpilot.starpilot.drive_state.evidence.PhysicalSource', return_value=physical), \
patch.object(self.updated, 'system_time_valid', return_value=True):
self.assertFalse(self.updater.maybe_install(lambda: False))
self.updater.params.put_bool('IsOffroad', False)
self.assertFalse(self.updater.maybe_install())
self.assertFalse(self.updater.params.get_bool('DoReboot'))
physical.update_ready.assert_not_called()
self.updater.params.put_bool('IsOffroad', True)
self.assertTrue(self.updater.maybe_install())
self.assertTrue(self.updater.params.get_bool('DoReboot'))
self.assertEqual(self.git(installed, 'rev-parse', 'HEAD'), self.initial_commit)
self.assertEqual(physical.update_ready.call_count, 2)
def test_normal_fallback_stages_flashes_and_requests_reboot_with_fresh_parked_state(self):
installed = self.root / "installed"
self.git(self.root, "clone", "--no-local", str(self.merged), str(installed))
self.updated.BASEDIR = str(installed)
self.git(installed, 'remote', 'set-url', 'origin', str(self.source))
(self.source / "panda/README.md").write_text("New native revision")
target = self.commit_source("Full update")
self.updated.AGNOS = True
progress = Mock()
with patch.object(self.updated, "init_overlay"):
self.assertEqual(self.updater.normal_update(BRANCH, self.initial_commit, lambda: True, progress), "reboot-requested")
self.updated.handle_agnos_update.assert_called_once()
self.assertEqual(self.git(installed, "rev-parse", "HEAD"), self.initial_commit)
self.assertEqual(self.git(self.finalized, "rev-parse", "HEAD"), target)
self.assertTrue(self.updater.params.values["DoReboot"])
self.assertTrue(self.updater.params.values["UpdateAvailable"])
progress.assert_called_with("rebooting", "Update installed. Restarting device.")
def test_normal_fallback_never_reboots_after_parked_authority_is_lost(self):
installed = self.root / "installed"
self.git(self.root, "clone", "--no-local", str(self.merged), str(installed))
self.updated.BASEDIR = str(installed)
self.git(installed, 'remote', 'set-url', 'origin', str(self.source))
(self.source / "panda/README.md").write_text("New native revision")
self.commit_source("Full update")
parked = iter((True, False))
with patch.object(self.updated, "init_overlay"), self.assertRaises(self.updated.FastUpdateError):
self.updater.normal_update(BRANCH, self.initial_commit, lambda: next(parked), Mock())
self.assertNotIn("DoReboot", self.updater.params.values)
def test_check_reports_confirmed_os_requirement_without_changing_installed_files(self):
self.updated.AGNOS = True
self.updated.HARDWARE.get_os_version.return_value = "12"
for launch, required in (("export AGNOS_VERSION='12'\n", False), ("export AGNOS_VERSION='13'\n", True),
("# Unknown OS version\n", None)):
with self.subTest(launch=launch):
(self.source / "launch_env.sh").write_text(launch)
self.commit_source("Change target OS version")
self.updater.check_for_update()
expected = {} if required is None else {"branch": BRANCH, "required": required}
self.assertEqual(self.updater.params.values["UpdaterOSUpdate"], expected)
self.assertEqual(self.git(self.merged, "rev-parse", "HEAD"), self.initial_commit)
self.updated.handle_agnos_update.assert_not_called()
def test_vendored_fetch_and_finalize(self):
(self.source / "panda" / "README.md").write_text("Updated ordinary tracked source\n")
target = self.commit_source("Update dependency source atomically")
self.updater.fetch_update()
self.assertEqual(self.git(self.merged, "rev-parse", "HEAD"), target)
self.assertEqual(self.git(self.finalized, "rev-parse", "HEAD"), target)
self.assertEqual((self.finalized / "panda" / "README.md").read_text(), "Updated ordinary tracked source\n")
self.assertEqual(self.git(self.finalized, "rev-parse", f"refs/starpilot/recent/{BRANCH}"), target)
self.assertTrue((self.finalized / ".overlay_consistent").is_file())
for name in DEPENDENCIES:
self.assertFalse((self.finalized / name / ".git").exists())
self.assertFalse(any(line.startswith("160000 ") for line in self.git(self.finalized, "ls-tree", "-r", "HEAD").splitlines()))
self.assertEqual(self.git(self.merged, "config", "submodule.recurse"), "false")
self.assertEqual(self.git(self.merged, "config", "fetch.recurseSubmodules"), "false")
self.updated.handle_agnos_update.assert_not_called()
def test_filtered_fetch_preserves_history_without_obsolete_artifact_contents(self):
self.git(self.source, 'config', 'uploadpack.allowFilter', 'true')
self.git(self.source, 'config', 'uploadpack.allowReachableSHA1InWant', 'true')
self.git(self.merged, 'remote', 'set-url', 'origin', self.source.as_uri())
artifact = self.source / 'artifact.bin'
artifact.write_bytes(os.urandom(128 * 1024))
middle = self.commit_source('Superseded large artifact')
obsolete_blob = self.git(self.source, 'rev-parse', 'HEAD:artifact.bin')
artifact.write_bytes(os.urandom(128 * 1024))
target = self.commit_source('Chosen large artifact')
target_blob = self.git(self.source, 'rev-parse', 'HEAD:artifact.bin')
self.updater.check_for_update()
objects = set(self.git(self.updated.METADATA_ROOT, 'cat-file', '--batch-all-objects', '--batch-check=%(objectname)').splitlines())
self.assertIn(target, objects)
self.assertIn(middle, objects)
self.assertNotIn(target_blob, objects)
self.assertNotIn(obsolete_blob, objects)
metadata_fetches = sum('fetch' in command for command in self.commands)
self.assertTrue(self.updater.fetch_update())
self.assertEqual(sum('fetch' in command for command in self.commands), metadata_fetches + 2)
objects = set(self.git(self.merged, 'cat-file', '--batch-all-objects', '--batch-check=%(objectname)').splitlines())
self.assertIn(target, objects)
self.assertNotIn(middle, objects)
self.assertIn(target_blob, objects)
self.assertNotIn(obsolete_blob, objects)
self.assertIn(middle, self.git(self.updated.METADATA_ROOT, 'log', '--first-parent', '--format=%H', f'refs/starpilot/recent/{BRANCH}').splitlines())
self.assertEqual((self.finalized / 'artifact.bin').read_bytes(), artifact.read_bytes())
def test_refresh_versions_fetches_only_metadata_in_staging(self):
self.git(self.source, 'config', 'uploadpack.allowFilter', 'true')
self.git(self.merged, 'remote', 'set-url', 'origin', self.source.as_uri())
artifact = self.source / 'artifact.bin'
artifact.write_bytes(os.urandom(128 * 1024))
target = self.commit_source('Version metadata to refresh')
blob = self.git(self.source, 'rev-parse', 'HEAD:artifact.bin')
installed = self.root / 'installed'
self.git(self.root, 'clone', '--no-local', str(self.merged), str(installed))
self.updated.BASEDIR = str(installed)
self.git(installed, 'remote', 'set-url', 'origin', str(self.source))
self.git(self.root, 'clone', '--no-local', str(self.merged), str(self.finalized))
marker = self.finalized / '.overlay_consistent'
marker.touch()
self.updater.params.values['IsOffroad'] = True
with patch.object(self.updated, 'init_overlay'), \
patch('openpilot.starpilot.drive_state.evidence.PhysicalSource', return_value=Mock(update_ready=Mock(return_value=True))):
self.assertEqual(self.updater.fast_update(BRANCH, self.initial_commit, versions_only=True), 'versions-listed')
objects = set(self.git(self.updated.METADATA_ROOT, 'cat-file', '--batch-all-objects', '--batch-check=%(objectname)').splitlines())
self.assertIn(target, objects)
self.assertNotIn(blob, objects)
self.assertEqual(self.git(installed, 'rev-parse', 'HEAD'), self.initial_commit)
self.assertFalse((installed / '.git/FETCH_HEAD').exists())
self.assertTrue(marker.exists())
self.assertNotIn('DoReboot', self.updater.params.values)
self.assertTrue(self.updater.params.values['UpdaterFetchAvailable'])
self.assertFalse(self.updater.params.values['UpdateAvailable'])
def test_scheduled_check_stages_new_commit_when_automatic_downloads_are_enabled(self):
(self.source / "panda/README.md").write_text("New automatic download\n")
target = self.commit_source("Automatic download after checking")
params = self.updater.params
params.values["InstallDate"] = "already installed"
preference = self.root / "UpdaterAutomaticDownloads"
params.get_param_path = lambda key: str(self.root / key)
for enabled in (False, True):
with self.subTest(enabled=enabled):
preference.write_bytes(b"1" if enabled else b"0")
helper = Mock()
helper.current_request.side_effect = [
(self.updated.UserRequest.NONE, 0, None),
(self.updated.UserRequest.NONE, 1, None),
RuntimeError("test loop complete"),
]
with patch.object(self.updated, "Params", return_value=params), \
patch.object(self.updated, "Updater", return_value=self.updater), \
patch.object(self.updated, "WaitTimeHelper", return_value=helper), \
patch.object(self.updated, "init_overlay"), \
patch.object(self.updated, "LOCK_FILE", str(self.root / "updater.lock")), \
patch.object(self.updater, "set_params"), \
self.assertRaisesRegex(RuntimeError, "test loop complete"):
self.updated.main()
self.assertEqual((self.finalized / ".overlay_consistent").exists(), enabled)
self.assertEqual("UpdaterLastFetchTime" in params.values, enabled)
self.assertNotIn("DoReboot", params.values)
if enabled:
self.assertEqual(self.git(self.finalized, "rev-parse", "HEAD"), target)
else:
self.assertEqual(self.git(self.merged, "rev-parse", "HEAD"), self.initial_commit)
def test_up_to_date_check_does_not_repeat_automatic_staging(self):
params = self.updater.params
params.values['InstallDate'] = 'already installed'
params.get_param_path = lambda key: str(self.root / key)
helper = Mock()
helper.current_request.side_effect = [
(self.updated.UserRequest.NONE, 0, None),
(self.updated.UserRequest.CHECK, 1, None),
RuntimeError('test loop complete'),
]
with patch.object(self.updated, 'Params', return_value=params), \
patch.object(self.updated, 'Updater', return_value=self.updater), \
patch.object(self.updated, 'WaitTimeHelper', return_value=helper), \
patch.object(self.updated, 'init_overlay'), \
patch.object(self.updated, 'LOCK_FILE', str(self.root / 'updater.lock')), \
patch.object(self.updater, 'set_params'), \
patch.object(self.updater, 'fetch_update') as download, \
self.assertRaisesRegex(RuntimeError, 'test loop complete'):
self.updated.main()
download.assert_not_called()
self.assertNotIn('UpdaterLastFetchTime', params.values)
self.assertFalse(self.finalized.exists())
def test_metadata_refresh_completes_while_background_finalization_is_blocked(self):
installed = self.root / 'installed'
self.git(self.root, 'clone', '--no-local', str(self.merged), str(installed))
self.git(installed, 'remote', 'set-url', 'origin', str(self.source))
self.updated.BASEDIR = str(installed)
(self.source / 'panda/README.md').write_text('New background version')
self.commit_source('Background download')
entered, release = threading.Event(), threading.Event()
finalize = self.updated.finalize_update
failures = []
def blocked_finalize(*args, **kwargs):
entered.set()
if not release.wait(8):
raise RuntimeError('Test did not release finalization')
return finalize(*args, **kwargs)
def download():
try:
self.updater.fetch_update(background=True)
except Exception as error:
failures.append(error)
with patch.object(self.updated, 'finalize_update', side_effect=blocked_finalize):
worker = threading.Thread(target=download)
worker.start()
try:
self.assertTrue(entered.wait(4))
started = time.monotonic()
self.updater.request_check(branch=BRANCH, commit=self.initial_commit)
deadline = started + 4
while time.monotonic() < deadline:
check = self.updater.params.values['UpdaterFastState'].get('check', {})
if check.get('completedAt', 0) >= started:
break
time.sleep(0.01)
self.assertEqual(check['state'], 'complete')
self.assertGreaterEqual(check['completedAt'], started)
self.assertEqual(self.updater.params.values['UpdaterState'], 'finalizing update...')
self.assertTrue(self.updater.params.values['UpdaterFastState']['background'])
finally:
release.set()
worker.join(5)
self.assertFalse(worker.is_alive())
self.assertEqual(failures, [])
def test_finalized_runtime_files_are_independent_and_git_objects_are_shared(self):
(self.source / 'panda/README.md').write_text('Prepared source')
self.commit_source('New release')
self.updater.fetch_update()
source = self.merged / 'panda/README.md'
copied = self.finalized / 'panda/README.md'
self.assertNotEqual(source.stat().st_ino, copied.stat().st_ino)
copied.write_text('Local edit in prepared tree')
self.assertEqual(source.read_text(), 'Prepared source')
objects = list((self.merged / '.git/objects/pack').glob('*.pack'))
self.assertTrue(objects)
for obj in objects:
final = self.finalized / obj.relative_to(self.merged)
self.assertEqual(obj.stat().st_ino, final.stat().st_ino)
self.assertFalse(any(command[:2] == ['git', 'gc'] for command in self.commands))
def test_invalid_manifest_rejected_before_checkout(self):
self.manifest["schema_version"] = 999
self.write_manifest()
self.commit_source("Unsupported manifest")
self.assert_rejected_before_checkout()
def test_missing_manifest_rejected_before_checkout(self):
(self.source / "upstream-sync.json").unlink()
self.commit_source("Missing manifest")
self.assert_rejected_before_checkout()
def test_malformed_manifest_rejected_before_checkout(self):
(self.source / "upstream-sync.json").write_text("{invalid JSON")
self.commit_source("Malformed manifest")
self.assert_rejected_before_checkout()
def test_missing_dependency_rejected_before_checkout(self):
shutil.rmtree(self.source / "panda")
self.commit_source("Missing ordinary dependency folder")
self.assert_rejected_before_checkout()
def test_gitlink_rejected_before_checkout(self):
self.git(self.source, "rm", "-r", "panda")
self.git(self.source, "update-index", "--add", "--cacheinfo", f"160000,{self.initial_commit},panda")
self.git(self.source, "commit", "-m", "Invalid gitlink dependency")
self.assert_rejected_before_checkout()
def test_gitmodules_rejected_before_checkout(self):
(self.source / ".gitmodules").write_text('[submodule "panda"]\npath = panda\nurl = https://example.invalid/panda\n')
self.commit_source("Invalid submodule configuration")
self.assert_rejected_before_checkout()
def test_finalize_invalid_tree_clears_existing_readiness(self):
self.finalized.mkdir()
(self.finalized / ".overlay_consistent").touch()
self.manifest["dependencies"] = []
self.write_manifest()
self.commit_source("Invalid empty dependency manifest")
shutil.rmtree(self.merged)
self.git(self.root, "clone", "--no-local", "--no-hardlinks", str(self.source), str(self.merged))
with self.assertRaises(ValueError):
self.updated.finalize_update()
self.assertFalse((self.finalized / ".overlay_consistent").exists())
if __name__ == "__main__":
unittest.main()