import hashlib import os from pathlib import Path import subprocess import tempfile import unittest from unittest.mock import patch from tools.host_runtime import HostRuntime, parse class TestDeveloperRuntime(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) / 'source with spaces' self.root.mkdir() self.git('init', '-q') self.git('config', 'user.email', 'test@example.invalid') self.git('config', 'user.name', 'Test') (self.root / '.gitignore').write_text('.host_runtime/\n*.so\n') (self.root / 'source.py').write_text('original\n') self.git('add', '.') self.git('commit', '-qm', 'fixture') self.runtime = HostRuntime(self.root, 'shared', system='Darwin', machine='arm64') def git(self, *args): return subprocess.check_output(['git', *args], cwd=self.root, text=True) def test_sync_includes_local_edits_and_untracked_but_not_device_artifacts(self): (self.root / 'source.py').write_text('local edit\n') (self.root / 'new.py').write_text('new\n') (self.root / 'native.so').write_bytes(b'device binary') index_before = hashlib.sha256((self.root / '.git/index').read_bytes()).hexdigest() with self.runtime.locked(): self.runtime.sync() self.assertEqual((self.runtime.work / 'source.py').read_text(), 'local edit\n') self.assertTrue((self.runtime.work / 'new.py').is_file()) self.assertFalse((self.runtime.work / 'native.so').exists()) self.assertFalse((self.runtime.work / '.git').is_symlink()) self.assertEqual(hashlib.sha256((self.root / '.git/index').read_bytes()).hexdigest(), index_before) subprocess.check_call(['git', 'add', 'source.py'], cwd=self.runtime.work) self.assertEqual(hashlib.sha256((self.root / '.git/index').read_bytes()).hexdigest(), index_before) def test_resync_removes_deleted_source_and_keeps_host_builds_incremental(self): self.runtime.sync() native = self.runtime.work / 'native.so' native.write_bytes(b'host binary') source = self.runtime.work / 'source.py' before = source.stat().st_mtime_ns self.runtime.sync() self.assertEqual(source.stat().st_mtime_ns, before) (self.root / 'source.py').unlink() self.runtime.sync() self.assertFalse(source.exists()) self.assertEqual(native.read_bytes(), b'host binary') def test_cache_environment_ignores_external_imports_and_settings(self): with patch.dict(os.environ, PYTHONPATH='/elsewhere', PARAMS_ROOT='/real/params', OPENPILOT_PREFIX='real', CC='cross-compiler'): env = self.runtime.environment() self.assertNotIn('/elsewhere', env['PYTHONPATH']) self.assertEqual(env['CC'], '/usr/bin/clang') self.assertTrue(Path(env['PARAMS_ROOT']).is_relative_to(self.runtime.cache)) self.assertNotEqual(env['OPENPILOT_PREFIX'], 'real') second = HostRuntime(self.root, 'shared', system='Darwin', machine='arm64') cabana = HostRuntime(self.root, 'cabana', system='Darwin', machine='arm64') self.assertEqual(second.prefix, self.runtime.prefix) self.assertNotEqual(cabana.prefix, self.runtime.prefix) def test_cache_symlink_cannot_write_into_source(self): self.runtime.cache.mkdir(parents=True) self.runtime.work.symlink_to(self.root, target_is_directory=True) with self.assertRaises(RuntimeError): self.runtime.sync() self.assertEqual((self.root / 'source.py').read_text(), 'original\n') def test_cache_cannot_borrow_source_git_index(self): self.runtime.work.mkdir(parents=True) (self.runtime.work / '.git').symlink_to(self.root / '.git', target_is_directory=True) with self.assertRaisesRegex(RuntimeError, 'shared Git'): self.runtime.sync_git() def test_command_parsing_preserves_tool_arguments(self): self.assertEqual(parse(['c3', '8', '--demo']), ('c3', 8, ['--demo'])) self.assertEqual(parse(['juggle', '4', '--demo']), ('plotjuggler', 4, ['--demo'])) self.assertEqual(parse(['python', '-c', 'print(1)'])[2], ['-c', 'print(1)']) self.assertEqual(parse(['pytest', '-n', '2'])[2], ['-n', '2']) self.assertEqual(parse(['--help'])[0], 'help') for args in (['unknown'], ['c4', '0'], ['sync', '../oops'], ['sync', 'shared', 'extra']): with self.subTest(args=args), self.assertRaises(ValueError): parse(args) if __name__ == '__main__': unittest.main()