From a2cbed0ceb6d11893ec3e9d1b7aa917e559ab848 Mon Sep 17 00:00:00 2001 From: Christian Kellner Date: Thu, 27 Feb 2020 10:55:45 +0100 Subject: [PATCH] tests: copy on write checks for objectstore.Object Verify the copy on write semantics of `objectstore.Object`, i.e. content will only be copied at the moment a client wants to write to `Object`. This also checks that `Object.base` works. Modify the CI to execute the unit tests in a privileged container because `Object.read()` works internally by bind mounting a path. The mount operation needs at least CAP_SYS_ADMIN and overwriting the file permissions CAP_DAC_OVERRIDE. --- .github/workflows/tests.yml | 1 + test/test_objectstore.py | 57 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9a5edc6f..5946754b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,6 +35,7 @@ jobs: runs-on: ubuntu-latest container: image: docker.io/library/python:3.7 + options: --privileged # Needed for bind mounts in unit tests steps: - name: Clone repository uses: actions/checkout@v2 diff --git a/test/test_objectstore.py b/test/test_objectstore.py index 941ad489..003400c2 100644 --- a/test/test_objectstore.py +++ b/test/test_objectstore.py @@ -85,6 +85,63 @@ class TestObjectStore(unittest.TestCase): assert len(os.listdir(f"{object_store.refs}/a/")) == 1 assert len(os.listdir(f"{object_store.refs}/b/")) == 1 + def test_object_copy_on_write(self): + # operate with a clean object store + with tempfile.TemporaryDirectory(dir="/var/tmp") as tmp: + # sample data to be used for read, write checks + data = "23" + + object_store = objectstore.ObjectStore(tmp) + assert len(os.listdir(object_store.refs)) == 0 + + with object_store.new() as tree: + path = tree.write() + with open(f"{path}/data", "w") as f: + f.write(data) + st = os.fstat(f.fileno()) + data_inode = st.st_ino + # commit the object as "x" + x_hash = object_store.commit(tree, "x") + # after the commit, "x" is now the base + # of "tree" + self.assertEqual(tree.base, "x") + # check that "data" is still the very + # same file after committing + with tree.read() as path: + with open(f"{path}/data", "r") as f: + st = os.fstat(f.fileno()) + self.assertEqual(st.st_ino, data_inode) + data_read = f.read() + self.assertEqual(data, data_read) + + # the object referenced by "x" should act as + # the base of a new object. As long as the + # new one is not modified, it should have + # the very same content + with object_store.new(base_id="x") as tree: + self.assertEqual(tree.base, "x") + self.assertEqual(tree.treesum, x_hash) + with tree.read() as path: + with open(f"{path}/data", "r") as f: + # copy-on-write: since we have not written + # to the tree yet, "data" should be the + # very same file as that one of object "x" + st = os.fstat(f.fileno()) + self.assertEqual(st.st_ino, data_inode) + data_read = f.read() + self.assertEqual(data, data_read) + path = tree.write() + # "data" must of course still be present + assert os.path.exists(f"{path}/data") + # but since it is a copy, have a different inode + st = os.stat(f"{path}/data") + self.assertNotEqual(st.st_ino, data_inode) + p = Path(f"{path}/other_data") + p.touch() + # now that we have written, the treesum + # should have changed + self.assertNotEqual(tree.treesum, x_hash) + def test_snapshot(self): object_store = objectstore.ObjectStore(self.store) with object_store.new() as tree: