|  |  |  | import pytest
 | 
					
						
							|  |  |  | import os
 | 
					
						
							|  |  |  | import pathlib
 | 
					
						
							|  |  |  | import tempfile
 | 
					
						
							|  |  |  | import subprocess
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from openpilot.system.updated.casync import casync
 | 
					
						
							|  |  |  | from openpilot.system.updated.casync import tar
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # dd if=/dev/zero of=/tmp/img.raw bs=1M count=2
 | 
					
						
							|  |  |  | # sudo losetup -f /tmp/img.raw
 | 
					
						
							|  |  |  | # losetup -a | grep img.raw
 | 
					
						
							|  |  |  | LOOPBACK = os.environ.get('LOOPBACK', None)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.mark.skip("not used yet")
 | 
					
						
							|  |  |  | class TestCasync:
 | 
					
						
							|  |  |  |   @classmethod
 | 
					
						
							|  |  |  |   def setup_class(cls):
 | 
					
						
							|  |  |  |     cls.tmpdir = tempfile.TemporaryDirectory()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Build example contents
 | 
					
						
							|  |  |  |     chunk_a = [i % 256 for i in range(1024)] * 512
 | 
					
						
							|  |  |  |     chunk_b = [(256 - i) % 256 for i in range(1024)] * 512
 | 
					
						
							|  |  |  |     zeroes = [0] * (1024 * 128)
 | 
					
						
							|  |  |  |     contents = chunk_a + chunk_b + zeroes + chunk_a
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     cls.contents = bytes(contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Write to file
 | 
					
						
							|  |  |  |     cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.bin')
 | 
					
						
							|  |  |  |     with open(cls.orig_fn, 'wb') as f:
 | 
					
						
							|  |  |  |       f.write(cls.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Create casync files
 | 
					
						
							|  |  |  |     cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx')
 | 
					
						
							|  |  |  |     cls.store_fn = os.path.join(cls.tmpdir.name, 'store')
 | 
					
						
							|  |  |  |     subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn])
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     target = casync.parse_caibx(cls.manifest_fn)
 | 
					
						
							|  |  |  |     hashes = [c.sha.hex() for c in target]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Ensure we have chunk reuse
 | 
					
						
							|  |  |  |     assert len(hashes) > len(set(hashes))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def setup_method(self):
 | 
					
						
							|  |  |  |     # Clear target_lo
 | 
					
						
							|  |  |  |     if LOOPBACK is not None:
 | 
					
						
							|  |  |  |       self.target_lo = LOOPBACK
 | 
					
						
							|  |  |  |       with open(self.target_lo, 'wb') as f:
 | 
					
						
							|  |  |  |         f.write(b"0" * len(self.contents))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     self.target_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
 | 
					
						
							|  |  |  |     self.seed_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def teardown_method(self):
 | 
					
						
							|  |  |  |     for fn in [self.target_fn, self.seed_fn]:
 | 
					
						
							|  |  |  |       try:
 | 
					
						
							|  |  |  |         os.unlink(fn)
 | 
					
						
							|  |  |  |       except FileNotFoundError:
 | 
					
						
							|  |  |  |         pass
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_simple_extract(self):
 | 
					
						
							|  |  |  |     target = casync.parse_caibx(self.manifest_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  |     stats = casync.extract(target, sources, self.target_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(self.target_fn, 'rb') as target_f:
 | 
					
						
							|  |  |  |       assert target_f.read() == self.contents
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     assert stats['remote'] == len(self.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_seed(self):
 | 
					
						
							|  |  |  |     target = casync.parse_caibx(self.manifest_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Populate seed with half of the target contents
 | 
					
						
							|  |  |  |     with open(self.seed_fn, 'wb') as seed_f:
 | 
					
						
							|  |  |  |       seed_f.write(self.contents[:len(self.contents) // 2])
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sources = [('seed', casync.FileChunkReader(self.seed_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  |     sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  |     stats = casync.extract(target, sources, self.target_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(self.target_fn, 'rb') as target_f:
 | 
					
						
							|  |  |  |       assert target_f.read() == self.contents
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     assert stats['seed'] > 0
 | 
					
						
							|  |  |  |     assert stats['remote'] < len(self.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_already_done(self):
 | 
					
						
							|  |  |  |     """Test that an already flashed target doesn't download any chunks"""
 | 
					
						
							|  |  |  |     target = casync.parse_caibx(self.manifest_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(self.target_fn, 'wb') as f:
 | 
					
						
							|  |  |  |       f.write(self.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  |     sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     stats = casync.extract(target, sources, self.target_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(self.target_fn, 'rb') as f:
 | 
					
						
							|  |  |  |       assert f.read() == self.contents
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     assert stats['target'] == len(self.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_chunk_reuse(self):
 | 
					
						
							|  |  |  |     """Test that chunks that are reused are only downloaded once"""
 | 
					
						
							|  |  |  |     target = casync.parse_caibx(self.manifest_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Ensure target exists
 | 
					
						
							|  |  |  |     with open(self.target_fn, 'wb'):
 | 
					
						
							|  |  |  |       pass
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  |     sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     stats = casync.extract(target, sources, self.target_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(self.target_fn, 'rb') as f:
 | 
					
						
							|  |  |  |       assert f.read() == self.contents
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     assert stats['remote'] < len(self.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @pytest.mark.skipif(not LOOPBACK, reason="requires loopback device")
 | 
					
						
							|  |  |  |   def test_lo_simple_extract(self):
 | 
					
						
							|  |  |  |     target = casync.parse_caibx(self.manifest_fn)
 | 
					
						
							|  |  |  |     sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     stats = casync.extract(target, sources, self.target_lo)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(self.target_lo, 'rb') as target_f:
 | 
					
						
							|  |  |  |       assert target_f.read(len(self.contents)) == self.contents
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     assert stats['remote'] == len(self.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @pytest.mark.skipif(not LOOPBACK, reason="requires loopback device")
 | 
					
						
							|  |  |  |   def test_lo_chunk_reuse(self):
 | 
					
						
							|  |  |  |     """Test that chunks that are reused are only downloaded once"""
 | 
					
						
							|  |  |  |     target = casync.parse_caibx(self.manifest_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sources = [('target', casync.FileChunkReader(self.target_lo), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  |     sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     stats = casync.extract(target, sources, self.target_lo)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(self.target_lo, 'rb') as f:
 | 
					
						
							|  |  |  |       assert f.read(len(self.contents)) == self.contents
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     assert stats['remote'] < len(self.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.mark.skip("not used yet")
 | 
					
						
							|  |  |  | class TestCasyncDirectory:
 | 
					
						
							|  |  |  |   """Tests extracting a directory stored as a casync tar archive"""
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   NUM_FILES = 16
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @classmethod
 | 
					
						
							|  |  |  |   def setup_cache(cls, directory, files=None):
 | 
					
						
							|  |  |  |     if files is None:
 | 
					
						
							|  |  |  |       files = range(cls.NUM_FILES)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     chunk_a = [i % 256 for i in range(1024)] * 512
 | 
					
						
							|  |  |  |     chunk_b = [(256 - i) % 256 for i in range(1024)] * 512
 | 
					
						
							|  |  |  |     zeroes = [0] * (1024 * 128)
 | 
					
						
							|  |  |  |     cls.contents = chunk_a + chunk_b + zeroes + chunk_a
 | 
					
						
							|  |  |  |     cls.contents = bytes(cls.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for i in files:
 | 
					
						
							|  |  |  |       with open(os.path.join(directory, f"file_{i}.txt"), "wb") as f:
 | 
					
						
							|  |  |  |         f.write(cls.contents)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       os.symlink(f"file_{i}.txt", os.path.join(directory, f"link_{i}.txt"))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @classmethod
 | 
					
						
							|  |  |  |   def setup_class(cls):
 | 
					
						
							|  |  |  |     cls.tmpdir = tempfile.TemporaryDirectory()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Create casync files
 | 
					
						
							|  |  |  |     cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx')
 | 
					
						
							|  |  |  |     cls.store_fn = os.path.join(cls.tmpdir.name, 'store')
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     cls.directory_to_extract = tempfile.TemporaryDirectory()
 | 
					
						
							|  |  |  |     cls.setup_cache(cls.directory_to_extract.name)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.tar')
 | 
					
						
							|  |  |  |     tar.create_tar_archive(cls.orig_fn, pathlib.Path(cls.directory_to_extract.name))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn])
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @classmethod
 | 
					
						
							|  |  |  |   def teardown_class(cls):
 | 
					
						
							|  |  |  |     cls.tmpdir.cleanup()
 | 
					
						
							|  |  |  |     cls.directory_to_extract.cleanup()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def setup_method(self):
 | 
					
						
							|  |  |  |     self.cache_dir = tempfile.TemporaryDirectory()
 | 
					
						
							|  |  |  |     self.working_dir = tempfile.TemporaryDirectory()
 | 
					
						
							|  |  |  |     self.out_dir = tempfile.TemporaryDirectory()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def teardown_method(self):
 | 
					
						
							|  |  |  |     self.cache_dir.cleanup()
 | 
					
						
							|  |  |  |     self.working_dir.cleanup()
 | 
					
						
							|  |  |  |     self.out_dir.cleanup()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def run_test(self):
 | 
					
						
							|  |  |  |     target = casync.parse_caibx(self.manifest_fn)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     cache_filename = os.path.join(self.working_dir.name, "cache.tar")
 | 
					
						
							|  |  |  |     tmp_filename = os.path.join(self.working_dir.name, "tmp.tar")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sources = [('cache', casync.DirectoryTarChunkReader(self.cache_dir.name, cache_filename), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  |     sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     stats = casync.extract_directory(target, sources, pathlib.Path(self.out_dir.name), tmp_filename)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(os.path.join(self.out_dir.name, "file_0.txt"), "rb") as f:
 | 
					
						
							|  |  |  |       assert f.read() == self.contents
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(os.path.join(self.out_dir.name, "link_0.txt"), "rb") as f:
 | 
					
						
							|  |  |  |       assert f.read() == self.contents
 | 
					
						
							|  |  |  |       assert os.readlink(os.path.join(self.out_dir.name, "link_0.txt")) == "file_0.txt"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return stats
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_no_cache(self):
 | 
					
						
							|  |  |  |     self.setup_cache(self.cache_dir.name, [])
 | 
					
						
							|  |  |  |     stats = self.run_test()
 | 
					
						
							|  |  |  |     assert stats['remote'] > 0
 | 
					
						
							|  |  |  |     assert stats['cache'] == 0
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_full_cache(self):
 | 
					
						
							|  |  |  |     self.setup_cache(self.cache_dir.name, range(self.NUM_FILES))
 | 
					
						
							|  |  |  |     stats = self.run_test()
 | 
					
						
							|  |  |  |     assert stats['remote'] == 0
 | 
					
						
							|  |  |  |     assert stats['cache'] > 0
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_one_file_cache(self):
 | 
					
						
							|  |  |  |     self.setup_cache(self.cache_dir.name, range(1))
 | 
					
						
							|  |  |  |     stats = self.run_test()
 | 
					
						
							|  |  |  |     assert stats['remote'] > 0
 | 
					
						
							|  |  |  |     assert stats['cache'] > 0
 | 
					
						
							|  |  |  |     assert stats['cache'] < stats['remote']
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_one_file_incorrect_cache(self):
 | 
					
						
							|  |  |  |     self.setup_cache(self.cache_dir.name, range(self.NUM_FILES))
 | 
					
						
							|  |  |  |     with open(os.path.join(self.cache_dir.name, "file_0.txt"), "wb") as f:
 | 
					
						
							|  |  |  |       f.write(b"1234")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     stats = self.run_test()
 | 
					
						
							|  |  |  |     assert stats['remote'] > 0
 | 
					
						
							|  |  |  |     assert stats['cache'] > 0
 | 
					
						
							|  |  |  |     assert stats['cache'] > stats['remote']
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_one_file_missing_cache(self):
 | 
					
						
							|  |  |  |     self.setup_cache(self.cache_dir.name, range(self.NUM_FILES))
 | 
					
						
							|  |  |  |     os.unlink(os.path.join(self.cache_dir.name, "file_12.txt"))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     stats = self.run_test()
 | 
					
						
							|  |  |  |     assert stats['remote'] > 0
 | 
					
						
							|  |  |  |     assert stats['cache'] > 0
 | 
					
						
							|  |  |  |     assert stats['cache'] > stats['remote']
 |