blob: e9932e0a4dfe28d41edff88a35b80eb869a55841 [file] [log] [blame]
from contextlib import contextmanager
import os
import pprint
import shutil
import stat
import glob
import hashlib
from pathlib import Path
from typing import List, Optional
import pytest
from buildstream._cas import CASCache
from buildstream.storage._casbaseddirectory import CasBasedDirectory
from buildstream.storage._filebaseddirectory import FileBasedDirectory
from buildstream.storage.directory import _FileType, VirtualDirectoryError
DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "storage")
@contextmanager
def setup_backend(backend_class, tmpdir):
if backend_class == FileBasedDirectory:
path = os.path.join(tmpdir, "vdir")
os.mkdir(path)
yield backend_class(path)
else:
cas_cache = CASCache(os.path.join(tmpdir, "cas"), log_directory=os.path.join(tmpdir, "logs"))
try:
yield backend_class(cas_cache)
finally:
cas_cache.release_resources()
@pytest.mark.parametrize("backend", [FileBasedDirectory, CasBasedDirectory])
@pytest.mark.datafiles(DATA_DIR)
def test_import(tmpdir, datafiles, backend):
original = os.path.join(str(datafiles), "original")
with setup_backend(backend, str(tmpdir)) as c:
c.import_files(original)
assert "bin/bash" in c.list_relative_paths()
assert "bin/hello" in c.list_relative_paths()
@pytest.mark.parametrize("backend", [FileBasedDirectory, CasBasedDirectory])
@pytest.mark.datafiles(DATA_DIR)
def test_modified_file_list(tmpdir, datafiles, backend):
original = os.path.join(str(datafiles), "original")
overlay = os.path.join(str(datafiles), "overlay")
with setup_backend(backend, str(tmpdir)) as c:
c.import_files(original)
c.mark_unmodified()
c.import_files(overlay)
print("List of all paths in imported results: {}".format(c.list_relative_paths()))
assert "bin/bash" in c.list_relative_paths()
assert "bin/bash" in c.list_modified_paths()
assert "bin/hello" not in c.list_modified_paths()
@pytest.mark.parametrize(
"directories", [("merge-base", "merge-base"), ("empty", "empty"),],
)
@pytest.mark.datafiles(DATA_DIR)
def test_merge_same_casdirs(tmpdir, datafiles, directories):
buildtree = os.path.join(str(datafiles), "merge-buildtree")
before = os.path.join(str(datafiles), directories[0])
after = os.path.join(str(datafiles), directories[1])
# Bring the directories into a canonical state
for directory in (buildtree, before, after):
clear_gitkeeps(directory)
utime_recursively(directory, (100, 100))
with setup_backend(CasBasedDirectory, str(tmpdir)) as c, setup_backend(
CasBasedDirectory, str(tmpdir)
) as a, setup_backend(CasBasedDirectory, str(tmpdir)) as b:
a.import_files(before)
b.import_files(after)
c.import_files(buildtree)
assert a._get_digest() == b._get_digest(), "{}\n{}".format(
pprint.pformat(list_relative_paths(a)), pprint.pformat(list_relative_paths(b))
)
old_digest = c._get_digest()
c._apply_changes(a, b)
# Assert that the build tree stays the same (since there were
# no changes between a and b)
assert c._get_digest() == old_digest
@pytest.mark.parametrize(
"directories",
[
("merge-base", "merge-replace"),
("merge-base", "merge-remove"),
("merge-base", "merge-add"),
("merge-base", "merge-link"),
("merge-base", "merge-subdirectory-replace"),
("merge-base", "merge-subdirectory-remove"),
("merge-base", "merge-subdirectory-add"),
("merge-base", "merge-subdirectory-link"),
("merge-link", "merge-link-change"),
("merge-subdirectory-link", "merge-link-change"),
("merge-base", "merge-override-with-file"),
("merge-base", "merge-override-with-directory"),
("merge-base", "merge-override-in-subdir-with-file"),
("merge-base", "merge-override-in-subdir-with-directory"),
("merge-base", "merge-override-subdirectory"),
("merge-override-with-new-subdirectory", "merge-subdirectory-add"),
("empty", "merge-subdirectory-add"),
],
)
@pytest.mark.datafiles(DATA_DIR)
def test_merge_casdirs(tmpdir, datafiles, directories):
buildtree = os.path.join(str(datafiles), "merge-buildtree")
before = os.path.join(str(datafiles), directories[0])
after = os.path.join(str(datafiles), directories[1])
# Bring the directories into a canonical state
for directory in (buildtree, before, after):
clear_gitkeeps(directory)
utime_recursively(directory, (100, 100))
_test_merge_dirs(before, after, buildtree, str(tmpdir))
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize("modification", ["executable", "time"])
def test_merge_casdir_properties(tmpdir, datafiles, modification):
buildtree = os.path.join(str(datafiles), "merge-buildtree")
before = os.path.join(str(datafiles), "merge-base")
after = os.path.join(str(tmpdir), "merge-modified")
shutil.copytree(before, after, symlinks=True)
# Bring the directories into a canonical state
for directory in (buildtree, before, after):
clear_gitkeeps(directory)
utime_recursively(directory, (100, 100))
if modification == "executable":
os.chmod(os.path.join(after, "root-file"), 0o755)
elif modification == "time":
os.utime(os.path.join(after, "root-file"), (200, 200))
_test_merge_dirs(before, after, buildtree, str(tmpdir), properties=["mtime"])
def _test_merge_dirs(
before: str, after: str, buildtree: str, tmpdir: str, properties: Optional[List[str]] = None
) -> bool:
with setup_backend(CasBasedDirectory, tmpdir) as c, setup_backend(
CasBasedDirectory, tmpdir
) as copy, setup_backend(CasBasedDirectory, tmpdir) as a, setup_backend(CasBasedDirectory, tmpdir) as b:
a.import_files(before, properties=properties)
b.import_files(after, properties=properties)
c.import_files(buildtree, properties=properties)
copy.import_files(buildtree, properties=properties)
assert c._get_digest() == copy._get_digest()
assert a._get_digest() != b._get_digest(), "{}\n{}".format(
pprint.pformat(list_relative_paths(a)), pprint.pformat(list_relative_paths(b))
)
c._apply_changes(a, b)
# The files in c now should contain changes from b, so these
# shouldn't be the same anymore
assert c._get_digest() != copy._get_digest(), "{}\n{}".format(
pprint.pformat(list_relative_paths(c)), pprint.pformat(list_relative_paths(copy))
)
# This is the set of paths that should have been removed
removed = [path for path in list_paths_with_properties(a) if path not in list_paths_with_properties(b)]
# This is the set of paths that were added in the new set
added = [path for path in list_paths_with_properties(b) if path not in list_paths_with_properties(a)]
# We need to strip some types of values, since they're more
# than our little list comparisons can handle
def make_info(entry, list_props=None):
ret = {k: v for k, v in vars(entry).items() if k != "buildstream_object"}
if entry.type == _FileType.REGULAR_FILE:
# Only file digests make sense here (directory digests
# need to be re-calculated taking into account their
# contents).
ret["digest"] = entry.get_digest()
else:
ret["digest"] = None
return ret
combined = [path for path in list_paths_with_properties(copy) if path not in removed]
# Add the new list, overriding any old entries that already
# exist.
for path in added:
if path.name in (o.name for o in combined):
# Any paths that already exist must be removed
# first
combined = [o for o in combined if o.name != path.name]
combined.append(path)
else:
combined.append(path)
# If any paths don't have a parent directory, we need to
# remove them now
for e in combined:
path = Path(e.name)
for parent in list(path.parents)[:-1]:
if not str(parent) in (e.name for e in combined if e.type == _FileType.DIRECTORY):
# If not all parent directories are existing
# directories
combined = [e for e in combined if e.name != str(path)]
assert sorted(list(make_info(e) for e in combined), key=lambda x: x["name"]) == sorted(
list(make_info(e) for e in list_paths_with_properties(c)), key=lambda x: x["name"]
)
@pytest.mark.parametrize("backend", [FileBasedDirectory, CasBasedDirectory])
@pytest.mark.datafiles(DATA_DIR)
def test_file_types(tmpdir, datafiles, backend):
with setup_backend(backend, str(tmpdir)) as c:
c.import_files(os.path.join(str(datafiles), "merge-link"))
# Test __iter__
assert set(c) == {"link", "root-file", "subdirectory"}
assert c.exists("root-file")
assert c.isfile("root-file")
assert not c.isdir("root-file")
assert not c.islink("root-file")
st = c.stat("root-file")
assert stat.S_ISREG(st.st_mode)
assert c.exists("link")
assert c.islink("link")
assert not c.isfile("link")
assert c.readlink("link") == "root-file"
st = c.stat("link")
assert stat.S_ISLNK(st.st_mode)
assert c.exists("subdirectory")
assert c.isdir("subdirectory")
assert not c.isfile("subdirectory")
subdir = c.descend("subdirectory")
assert set(subdir) == {"subdir-file"}
st = c.stat("subdirectory")
assert stat.S_ISDIR(st.st_mode)
@pytest.mark.parametrize("backend", [FileBasedDirectory, CasBasedDirectory])
@pytest.mark.datafiles(DATA_DIR)
def test_open_file(tmpdir, datafiles, backend):
with setup_backend(backend, str(tmpdir)) as c:
assert not c.isfile("hello")
with c.open_file("hello", mode="w") as f:
f.write("world")
assert c.isfile("hello")
assert c.file_digest("hello") == hashlib.sha256(b"world").hexdigest()
with c.open_file("hello", mode="r") as f:
assert f.read() == "world"
@pytest.mark.parametrize("backend", [FileBasedDirectory, CasBasedDirectory])
@pytest.mark.datafiles(DATA_DIR)
def test_remove(tmpdir, datafiles, backend):
with setup_backend(backend, str(tmpdir)) as c:
c.import_files(os.path.join(str(datafiles), "merge-link"))
with pytest.raises((OSError, VirtualDirectoryError)):
c.remove("subdirectory")
with pytest.raises(FileNotFoundError):
c.remove("subdirectory", "does-not-exist")
# Check that `remove()` doesn't follow symlinks
c.remove("link")
assert not c.exists("link")
assert c.exists("root-file")
c.remove("subdirectory", recursive=True)
assert not c.exists("subdirectory")
# Removing an empty directory does not require recursive=True
c.descend("empty-directory", create=True)
c.remove("empty-directory")
@pytest.mark.parametrize("backend", [FileBasedDirectory, CasBasedDirectory])
@pytest.mark.datafiles(DATA_DIR)
def test_rename(tmpdir, datafiles, backend):
with setup_backend(backend, str(tmpdir)) as c:
c.import_files(os.path.join(str(datafiles), "original"))
c.rename(["bin", "hello"], ["bin", "hello2"])
c.rename(["bin"], ["bin2"])
assert c.isfile("bin2", "hello2")
# This is purely for error output; lists relative paths and
# their digests so differences are human-grokkable
def list_relative_paths(directory):
def entry_output(entry):
if entry.type == _FileType.DIRECTORY:
return list_relative_paths(entry.get_directory(directory))
elif entry.type == _FileType.SYMLINK:
return "-> " + entry.target
else:
return entry.get_digest().hash
return {name: entry_output(entry) for name, entry in directory.index.items()}
def list_paths_with_properties(directory, prefix=""):
for leaf in directory.index.keys():
entry = directory.index[leaf].clone()
if directory.filename:
entry.name = directory.filename + os.path.sep + entry.name
yield entry
if entry.type == _FileType.DIRECTORY:
subdir = entry.get_directory(directory)
yield from list_paths_with_properties(subdir)
def utime_recursively(directory, time):
for f in glob.glob(os.path.join(directory, "**"), recursive=True):
os.utime(f, time)
def clear_gitkeeps(directory):
for f in glob.glob(os.path.join(directory, "**", ".gitkeep"), recursive=True):
os.remove(f)