debian-forge/test/mod/test_objectstore.py
Christian Kellner 496d21de54 objectstore: sub-tree support for read_at
Add the ability to only read a sub-tree of a tree via `Object.read_at`.
Expose the functionality via the `Store{Server,Client}.read_tree_at`.
Extend the tests to check this new functionality.
2021-06-09 18:37:47 +01:00

372 lines
14 KiB
Python

#
# Tests for the 'osbuild.objectstore' module.
#
import contextlib
import os
import shutil
import tempfile
import unittest
from pathlib import Path
from osbuild import objectstore
from .. import test
@unittest.skipUnless(test.TestBase.can_bind_mount(), "root-only")
class TestObjectStore(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.store = os.getenv("OSBUILD_TEST_STORE")
if not cls.store:
cls.store = tempfile.mkdtemp(prefix="osbuild-test-", dir="/var/tmp")
@classmethod
def tearDownClass(cls):
if not os.getenv("OSBUILD_TEST_STORE"):
shutil.rmtree(cls.store)
def test_basic(self):
# always use a temporary store so item counting works
with tempfile.TemporaryDirectory(dir="/var/tmp") as tmp:
object_store = objectstore.ObjectStore(tmp)
# No objects or references should be in the store
assert len(os.listdir(object_store.refs)) == 0
assert len(os.listdir(object_store.objects)) == 0
with object_store.new() as tree:
with tree.write() as path:
p = Path(path, "A")
p.touch()
object_store.commit(tree, "a")
assert object_store.contains("a")
assert os.path.exists(f"{object_store.refs}/a")
assert os.path.exists(f"{object_store.refs}/a/A")
assert len(os.listdir(object_store.refs)) == 1
assert len(os.listdir(object_store.objects)) == 1
assert len(os.listdir(f"{object_store.refs}/a/")) == 1
with object_store.new() as tree:
with tree.write() as path:
p = Path(path, "A")
p.touch()
p = Path(path, "B")
p.touch()
object_store.commit(tree, "b")
assert object_store.contains("b")
assert os.path.exists(f"{object_store.refs}/b")
assert os.path.exists(f"{object_store.refs}/b/B")
assert len(os.listdir(object_store.refs)) == 2
assert len(os.listdir(object_store.objects)) == 2
assert len(os.listdir(f"{object_store.refs}/b/")) == 2
self.assertEqual(object_store.resolve_ref(None), None)
self.assertEqual(object_store.resolve_ref("a"),
f"{object_store.refs}/a")
def test_cleanup(self):
# always use a temporary store so item counting works
with tempfile.TemporaryDirectory(dir="/var/tmp") as tmp:
with objectstore.ObjectStore(tmp) as object_store:
tree = object_store.new()
self.assertEqual(len(os.listdir(object_store.tmp)), 1)
with tree.write() as path:
p = Path(path, "A")
p.touch()
# there should be no temporary Objects dirs anymore
self.assertEqual(len(os.listdir(object_store.tmp)), 0)
# pylint: disable=no-self-use
def test_duplicate(self):
with tempfile.TemporaryDirectory(dir="/var/tmp") as tmp:
object_store = objectstore.ObjectStore(tmp)
with object_store.new() as tree:
with tree.write() as path:
p = Path(path, "A")
p.touch()
object_store.commit(tree, "a")
with object_store.new() as tree:
with tree.write() as path:
shutil.copy2(f"{object_store.refs}/a/A",
os.path.join(path, "A"))
object_store.commit(tree, "b")
assert os.path.exists(f"{object_store.refs}/a")
assert os.path.exists(f"{object_store.refs}/a/A")
assert os.path.exists(f"{object_store.refs}/b/A")
assert len(os.listdir(object_store.refs)) == 2
assert len(os.listdir(object_store.objects)) == 1
assert len(os.listdir(f"{object_store.refs}/a/")) == 1
assert len(os.listdir(f"{object_store.refs}/b/")) == 1
# pylint: disable=no-self-use
def test_object_base(self):
# operate with a clean object store
with tempfile.TemporaryDirectory(dir="/var/tmp") as tmp:
object_store = objectstore.ObjectStore(tmp)
with object_store.new() as tree:
with tree.write() as path:
p = Path(path, "A")
p.touch()
object_store.commit(tree, "a")
with object_store.new() as tree:
tree.base = "a"
object_store.commit(tree, "b")
with object_store.new() as tree:
tree.base = "b"
with tree.write() as path:
p = Path(path, "C")
p.touch()
object_store.commit(tree, "c")
assert os.path.exists(f"{object_store.refs}/a/A")
assert os.path.exists(f"{object_store.refs}/b/A")
assert os.path.exists(f"{object_store.refs}/c/A")
assert os.path.exists(f"{object_store.refs}/c/C")
assert len(os.listdir(object_store.refs)) == 3
assert len(os.listdir(object_store.objects)) == 2
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 tree.write() as path, \
open(os.path.join(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(os.path.join(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(os.path.join(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)
with tree.write() as path:
# "data" must of course still be present
assert os.path.exists(os.path.join(path, "data"))
# but since it is a copy, have a different inode
st = os.stat(os.path.join(path, "data"))
self.assertNotEqual(st.st_ino, data_inode)
p = Path(path, "other_data")
p.touch()
# now that we have written, the treesum
# should have changed
self.assertNotEqual(tree.treesum, x_hash)
def test_object_mode(self):
object_store = objectstore.ObjectStore(self.store)
with object_store.new() as tree:
# check that trying to write to a tree that is
# currently being read from fails
with tree.read() as _:
with self.assertRaises(ValueError):
with tree.write() as _:
pass
# check multiple readers are ok
with tree.read() as _:
# calculating the treesum also is reading,
# so this is 3 nested readers
_ = tree.treesum
# writing should still fail
with self.assertRaises(ValueError):
with tree.write() as _:
pass
# Now that all readers are gone, writing should
# work
with tree.write() as _:
pass
# and back to reading, one last time
with tree.read() as _:
with self.assertRaises(ValueError):
with tree.write() as _:
pass
# Only one single writer
with tree.write() as _:
# no other readers
with self.assertRaises(ValueError):
with tree.read() as _:
pass
# or other writers
with self.assertRaises(ValueError):
with tree.write() as _:
pass
# one more time
with tree.write() as _:
pass
# tree has exited the context, it should NOT be
# writable anymore
with self.assertRaises(ValueError):
with tree.write() as _:
pass
def test_snapshot(self):
object_store = objectstore.ObjectStore(self.store)
with object_store.new() as tree:
with tree.write() as path:
p = Path(path, "A")
p.touch()
assert not object_store.contains("a")
object_store.commit(tree, "a")
assert object_store.contains("a")
with tree.write() as path:
p = Path(path, "B")
p.touch()
object_store.commit(tree, "b")
# check the references exist
assert os.path.exists(f"{object_store.refs}/a")
assert os.path.exists(f"{object_store.refs}/b")
# check the contents of the trees
assert os.path.exists(f"{object_store.refs}/a/A")
assert not os.path.exists(f"{object_store.refs}/a/B")
assert os.path.exists(f"{object_store.refs}/b/A")
assert os.path.exists(f"{object_store.refs}/b/B")
def test_host_tree(self):
object_store = objectstore.ObjectStore(self.store)
host = objectstore.HostTree(object_store)
# check we cannot call `write`
with self.assertRaises(ValueError):
with host.write() as _:
pass
# check we actually cannot write to the path
with host.read() as path:
p = Path(path, "osbuild-test-file")
with self.assertRaises(OSError):
p.touch()
# pylint: disable=too-many-statements
def test_store_server(self):
with contextlib.ExitStack() as stack:
store = objectstore.ObjectStore(self.store)
stack.enter_context(stack)
tmpdir = tempfile.TemporaryDirectory()
tmpdir = stack.enter_context(tmpdir)
server = objectstore.StoreServer(store)
stack.enter_context(server)
client = objectstore.StoreClient(server.socket_address)
have = client.source("org.osbuild.files")
want = os.path.join(self.store, "sources")
assert have.startswith(want)
tmp = client.mkdtemp(suffix="suffix", prefix="prefix")
assert tmp.startswith(store.tmp)
name = os.path.basename(tmp)
assert name.startswith("prefix")
assert name.endswith("suffix")
path = client.read_tree("42")
assert path is None
obj = store.new()
with obj.write() as path:
p = Path(path, "file.txt")
p.write_text("osbuild")
p = Path(path, "directory")
p.mkdir()
obj.id = "42"
mountpoint = Path(tmpdir, "mountpoint")
mountpoint.mkdir()
assert store.contains("42")
path = client.read_tree_at("42", mountpoint)
assert Path(path) == mountpoint
filepath = Path(mountpoint, "file.txt")
assert filepath.exists()
txt = filepath.read_text()
assert txt == "osbuild"
# check we can mount subtrees via `read_tree_at`
filemount = Path(tmpdir, "file")
filemount.touch()
path = client.read_tree_at("42", filemount, "/file.txt")
filepath = Path(path)
assert filepath.is_file()
txt = filepath.read_text()
assert txt == "osbuild"
dirmount = Path(tmpdir, "dir")
dirmount.mkdir()
path = client.read_tree_at("42", dirmount, "/directory")
dirpath = Path(path)
assert dirpath.is_dir()
# check proper exceptions are raised for non existent
# mount points and sub-trees
with self.assertRaises(RuntimeError):
nonexistent = os.path.join(tmpdir, "nonexistent")
_ = client.read_tree_at("42", nonexistent)
with self.assertRaises(RuntimeError):
_ = client.read_tree_at("42", tmpdir, "/nonexistent")
# The tree is being read via the client, should
# not be able to write to it
with self.assertRaises(ValueError):
with obj.write() as _:
pass