diff options
Diffstat (limited to 'src/borg/testsuite/archiver.py')
-rw-r--r-- | src/borg/testsuite/archiver.py | 182 |
1 files changed, 132 insertions, 50 deletions
diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 3a2dc4f07..f5a44bd9a 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -223,7 +223,8 @@ class ArchiverTestCaseBase(BaseTestCase): def tearDown(self): os.chdir(self._old_wd) - shutil.rmtree(self.tmpdir) + # note: ignore_errors=True as workaround for issue #862 + shutil.rmtree(self.tmpdir, ignore_errors=True) def cmd(self, *args, **kw): exit_code = kw.pop('exit_code', 0) @@ -239,6 +240,13 @@ class ArchiverTestCaseBase(BaseTestCase): def create_src_archive(self, name): self.cmd('create', self.repository_location + '::' + name, src_dir) + def open_archive(self, name): + repository = Repository(self.repository_path) + with repository: + manifest, key = Manifest.load(repository) + archive = Archive(repository, key, manifest, name) + return archive, repository + def create_regular_file(self, name, size=0, contents=None): filename = os.path.join(self.input_path, name) if not os.path.exists(os.path.dirname(filename)): @@ -1283,52 +1291,96 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'This command initializes' not in self.cmd('help', 'init', '--usage-only') @unittest.skipUnless(has_llfuse, 'llfuse not installed') - def test_fuse_mount_repository(self): - mountpoint = os.path.join(self.tmpdir, 'mountpoint') - os.mkdir(mountpoint) + def test_fuse(self): self.cmd('init', self.repository_location) self.create_test_files() self.cmd('create', self.repository_location + '::archive', 'input') self.cmd('create', self.repository_location + '::archive2', 'input') - try: - self.cmd('mount', self.repository_location, mountpoint, fork=True) - self.wait_for_mount(mountpoint) - if has_lchflags: - # remove the file we did not backup, so input and output become equal - os.remove(os.path.join('input', 'flagfile')) + if has_lchflags: + # remove the file we did not backup, so input and output become equal + os.remove(os.path.join('input', 'flagfile')) + mountpoint = os.path.join(self.tmpdir, 'mountpoint') + # mount the whole repository, archive contents shall show up in archivename subdirs of mountpoint: + with self.fuse_mount(self.repository_location, mountpoint): self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input')) self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input')) - finally: - if sys.platform.startswith('linux'): - os.system('fusermount -u ' + mountpoint) + # mount only 1 archive, its contents shall show up directly in mountpoint: + with self.fuse_mount(self.repository_location + '::archive', mountpoint): + self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input')) + # regular file + in_fn = 'input/file1' + out_fn = os.path.join(mountpoint, 'input', 'file1') + # stat + sti1 = os.stat(in_fn) + sto1 = os.stat(out_fn) + assert sti1.st_mode == sto1.st_mode + assert sti1.st_uid == sto1.st_uid + assert sti1.st_gid == sto1.st_gid + assert sti1.st_size == sto1.st_size + assert sti1.st_atime == sto1.st_atime + assert sti1.st_ctime == sto1.st_ctime + assert sti1.st_mtime == sto1.st_mtime + # note: there is another hardlink to this, see below + assert sti1.st_nlink == sto1.st_nlink == 2 + # read + with open(in_fn, 'rb') as in_f, open(out_fn, 'rb') as out_f: + assert in_f.read() == out_f.read() + # list/read xattrs + if xattr.is_enabled(self.input_path): + assert xattr.listxattr(out_fn) == ['user.foo', ] + assert xattr.getxattr(out_fn, 'user.foo') == b'bar' else: - os.system('umount ' + mountpoint) - os.rmdir(mountpoint) - # Give the daemon some time to exit - time.sleep(.2) + assert xattr.listxattr(out_fn) == [] + try: + xattr.getxattr(out_fn, 'user.foo') + except OSError as e: + assert e.errno == llfuse.ENOATTR + else: + assert False, "expected OSError(ENOATTR), but no error was raised" + # hardlink (to 'input/file1') + in_fn = 'input/hardlink' + out_fn = os.path.join(mountpoint, 'input', 'hardlink') + sti2 = os.stat(in_fn) + sto2 = os.stat(out_fn) + assert sti2.st_nlink == sto2.st_nlink == 2 + assert sto1.st_ino == sto2.st_ino + # symlink + in_fn = 'input/link1' + out_fn = os.path.join(mountpoint, 'input', 'link1') + sti = os.stat(in_fn, follow_symlinks=False) + sto = os.stat(out_fn, follow_symlinks=False) + assert stat.S_ISLNK(sti.st_mode) + assert stat.S_ISLNK(sto.st_mode) + assert os.readlink(in_fn) == os.readlink(out_fn) + # FIFO + out_fn = os.path.join(mountpoint, 'input', 'fifo1') + sto = os.stat(out_fn) + assert stat.S_ISFIFO(sto.st_mode) @unittest.skipUnless(has_llfuse, 'llfuse not installed') - def test_fuse_mount_archive(self): - mountpoint = os.path.join(self.tmpdir, 'mountpoint') - os.mkdir(mountpoint) + def test_fuse_allow_damaged_files(self): self.cmd('init', self.repository_location) - self.create_test_files() - self.cmd('create', self.repository_location + '::archive', 'input') - try: - self.cmd('mount', self.repository_location + '::archive', mountpoint, fork=True) - self.wait_for_mount(mountpoint) - if has_lchflags: - # remove the file we did not backup, so input and output become equal - os.remove(os.path.join('input', 'flagfile')) - self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input')) - finally: - if sys.platform.startswith('linux'): - os.system('fusermount -u ' + mountpoint) + self.create_src_archive('archive') + # Get rid of a chunk and repair it + archive, repository = self.open_archive('archive') + with repository: + for item in archive.iter_items(): + if item.path.endswith('testsuite/archiver.py'): + repository.delete(item.chunks[-1].id) + path = item.path # store full path for later + break else: - os.system('umount ' + mountpoint) - os.rmdir(mountpoint) - # Give the daemon some time to exit - time.sleep(.2) + assert False # missed the file + repository.commit() + self.cmd('check', '--repair', self.repository_location, exit_code=0) + + mountpoint = os.path.join(self.tmpdir, 'mountpoint') + with self.fuse_mount(self.repository_location + '::archive', mountpoint): + with pytest.raises(OSError) as excinfo: + open(os.path.join(mountpoint, path)) + assert excinfo.value.errno == errno.EIO + with self.fuse_mount(self.repository_location + '::archive', mountpoint, 'allow_damaged_files'): + open(os.path.join(mountpoint, path)).close() def verify_aes_counter_uniqueness(self, method): seen = set() # Chunks already seen @@ -1633,6 +1685,14 @@ class ArchiverTestCaseBinary(ArchiverTestCase): def test_recreate_changed_source(self): pass + @unittest.skip('test_basic_functionality seems incompatible with fakeroot and/or the binary.') + def test_basic_functionality(self): + pass + + @unittest.skip('test_overwrite seems incompatible with fakeroot and/or the binary.') + def test_overwrite(self): + pass + class ArchiverCheckTestCase(ArchiverTestCaseBase): @@ -1643,13 +1703,6 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.create_src_archive('archive1') self.create_src_archive('archive2') - def open_archive(self, name): - repository = Repository(self.repository_path) - with repository: - manifest, key = Manifest.load(repository) - archive = Archive(repository, key, manifest, name) - return archive, repository - def test_check_usage(self): output = self.cmd('check', '-v', '--progress', self.repository_location, exit_code=0) self.assert_in('Starting repository check', output) @@ -1672,12 +1725,45 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): with repository: for item in archive.iter_items(): if item.path.endswith('testsuite/archiver.py'): - repository.delete(item.chunks[-1].id) + valid_chunks = item.chunks + killed_chunk = valid_chunks[-1] + repository.delete(killed_chunk.id) break + else: + self.assert_true(False) # should not happen repository.commit() self.cmd('check', self.repository_location, exit_code=1) - self.cmd('check', '--repair', self.repository_location, exit_code=0) + output = self.cmd('check', '--repair', self.repository_location, exit_code=0) + self.assert_in('New missing file chunk detected', output) self.cmd('check', self.repository_location, exit_code=0) + # check that the file in the old archives has now a different chunk list without the killed chunk + for archive_name in ('archive1', 'archive2'): + archive, repository = self.open_archive(archive_name) + with repository: + for item in archive.iter_items(): + if item.path.endswith('testsuite/archiver.py'): + self.assert_not_equal(valid_chunks, item.chunks) + self.assert_not_in(killed_chunk, item.chunks) + break + else: + self.assert_true(False) # should not happen + # do a fresh backup (that will include the killed chunk) + with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10): + self.create_src_archive('archive3') + # check should be able to heal the file now: + output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0) + self.assert_in('Healed previously missing file chunk', output) + self.assert_in('testsuite/archiver.py: Completely healed previously damaged file!', output) + # check that the file in the old archives has the correct chunks again + for archive_name in ('archive1', 'archive2'): + archive, repository = self.open_archive(archive_name) + with repository: + for item in archive.iter_items(): + if item.path.endswith('testsuite/archiver.py'): + self.assert_equal(valid_chunks, item.chunks) + break + else: + self.assert_true(False) # should not happen def test_missing_archive_item_chunk(self): archive, repository = self.open_archive('archive1') @@ -1762,11 +1848,7 @@ class RemoteArchiverTestCase(ArchiverTestCase): # this was introduced because some tests expect stderr contents to show up # in "output" also. Also, the non-forking exec_cmd catches both, too. @unittest.skip('deadlock issues') - def test_fuse_mount_repository(self): - pass - - @unittest.skip('deadlock issues') - def test_fuse_mount_archive(self): + def test_fuse(self): pass @unittest.skip('only works locally') |