summaryrefslogtreecommitdiffstats
path: root/src/borg/testsuite/archiver.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/borg/testsuite/archiver.py')
-rw-r--r--src/borg/testsuite/archiver.py182
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')