mirror of
https://github.com/borgbackup/borg.git
synced 2026-07-11 19:15:43 -04:00
max_count=3 was a testing value. The default is now max_count=None, max_size=50_000_000; the testsuite sets BORG_PACK_MAX_COUNT=3 instead, so small test data still produces multiple multi-object packs.
224 lines
8.2 KiB
Python
224 lines
8.2 KiB
Python
import contextlib
|
|
import getpass
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
if hasattr(pytest, "register_assert_rewrite"):
|
|
pytest.register_assert_rewrite("borg.testsuite")
|
|
|
|
# Ensure that the loggers exist for all tests
|
|
from borg.logger import setup_logging # noqa: E402
|
|
|
|
setup_logging()
|
|
|
|
from borg.archiver import Archiver # noqa: E402
|
|
from borg.testsuite import has_lchflags, has_llfuse, has_pyfuse3, has_mfusepy # noqa: E402
|
|
from borg.testsuite import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported # noqa: E402
|
|
from borg.testsuite.archiver import BORG_EXES
|
|
from borg.testsuite.platform.platform_test import fakeroot_detected # noqa: E402
|
|
from borg.helpers import umount # noqa: E402
|
|
|
|
|
|
def _pytest_tmp_root():
|
|
# Mirror the parent of pytest's basetemp: <temproot>/pytest-of-<user>.
|
|
# Old (already-removed-from-retention) run dirs live here too, which is where
|
|
# FUSE mounts orphaned by aborted runs hide.
|
|
try:
|
|
user = getpass.getuser()
|
|
except Exception:
|
|
user = "unknown"
|
|
return os.path.join(tempfile.gettempdir(), f"pytest-of-{user}")
|
|
|
|
|
|
def _looks_mounted(path):
|
|
# A live FUSE mount reports as a mount point; a dead/stale one makes the
|
|
# directory inaccessible (os.path.ismount swallows that and returns False).
|
|
if os.path.ismount(path):
|
|
return True
|
|
try:
|
|
os.listdir(path)
|
|
except OSError:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _sweep_stale_fuse_mounts():
|
|
# The fuse test helper always mounts at a directory named "mountpoint" under the
|
|
# pytest temp tree and unmounts it in a finally block. If a run is aborted, that
|
|
# cleanup is skipped and the mount lingers, so the next run's temp-dir GC (rm_rf)
|
|
# trips over it ("Resource busy" etc.). Walk the temp tree, find such leftover
|
|
# mountpoints and unmount them with borg's own (cross-platform) umount helper.
|
|
root = _pytest_tmp_root()
|
|
for dirpath, dirnames, filenames in os.walk(root, onerror=lambda err: None):
|
|
if "mountpoint" not in dirnames:
|
|
continue
|
|
dirnames.remove("mountpoint") # never descend into a (possibly stale) mount
|
|
mountpoint = os.path.join(dirpath, "mountpoint")
|
|
if _looks_mounted(mountpoint):
|
|
try:
|
|
umount(mountpoint)
|
|
except Exception: # nosec B110
|
|
pass # best-effort: never let cleanup break the test session
|
|
|
|
|
|
def pytest_sessionstart(session):
|
|
# Run once in the main process (not in each xdist worker), before any temp-dir
|
|
# GC, to clear mounts left behind by a previous, aborted session.
|
|
if not hasattr(session.config, "workerinput"):
|
|
_sweep_stale_fuse_mounts()
|
|
|
|
|
|
def pytest_sessionfinish(session, exitstatus):
|
|
# Safety net for anything stranded by this session (e.g. a hard interrupt).
|
|
if not hasattr(session.config, "workerinput"):
|
|
_sweep_stale_fuse_mounts()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_env(tmpdir_factory, monkeypatch):
|
|
# also avoid to use anything from the outside environment:
|
|
keys = [key for key in os.environ if key.startswith("BORG_") and key not in ("BORG_FUSE_IMPL",)]
|
|
for key in keys:
|
|
monkeypatch.delenv(key, raising=False)
|
|
# avoid that we access / modify the user's normal .config / .cache directory:
|
|
base_dir = tmpdir_factory.mktemp("borg-base-dir")
|
|
monkeypatch.setenv("BORG_BASE_DIR", str(base_dir))
|
|
# Speed up tests
|
|
monkeypatch.setenv("BORG_TESTONLY_WEAKEN_KDF", "1")
|
|
monkeypatch.setenv("BORG_STORE_DATA_LEVELS", "0") # flat storage for few objects
|
|
# tiny packs, so small test data still produces multiple multi-object packs
|
|
monkeypatch.setenv("BORG_PACK_MAX_COUNT", "3")
|
|
yield
|
|
shutil.rmtree(str(base_dir), ignore_errors=True) # clean up
|
|
|
|
|
|
def pytest_report_header(config, start_path):
|
|
tests = {
|
|
"BSD flags": has_lchflags,
|
|
"llfuse": has_llfuse,
|
|
"pyfuse3": has_pyfuse3,
|
|
"mfusepy": has_mfusepy,
|
|
"root": not fakeroot_detected(),
|
|
"symlinks": are_symlinks_supported(),
|
|
"hardlinks": are_hardlinks_supported(),
|
|
"atime/mtime": is_utime_fully_supported(),
|
|
"modes": "BORG_TESTS_IGNORE_MODES" not in os.environ,
|
|
}
|
|
enabled = []
|
|
disabled = []
|
|
for test in tests:
|
|
if tests[test]:
|
|
enabled.append(test)
|
|
else:
|
|
disabled.append(test)
|
|
output = "Tests enabled: " + ", ".join(enabled) + "\n"
|
|
output += "Tests disabled: " + ", ".join(disabled)
|
|
return output
|
|
|
|
|
|
@pytest.fixture()
|
|
def set_env_variables():
|
|
os.environ["BORG_CHECK_I_KNOW_WHAT_I_AM_DOING"] = "YES"
|
|
os.environ["BORG_DELETE_I_KNOW_WHAT_I_AM_DOING"] = "YES"
|
|
os.environ["BORG_PASSPHRASE"] = "waytooeasyonlyfortests" # nosec B105
|
|
os.environ["BORG_SELFTEST"] = "disabled"
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def backup_files(tmp_path_factory):
|
|
# create a relatively simple / minimal set of test files
|
|
path = tmp_path_factory.mktemp("backup")
|
|
(path / "empty").write_bytes(b"")
|
|
(path / "dir1").mkdir()
|
|
(path / "dir1" / "text.txt").write_text("text content")
|
|
(path / "dir2").mkdir()
|
|
(path / "dir2" / "binary.bin").write_bytes(b"\x00\x01\x02\x03")
|
|
return str(path)
|
|
|
|
|
|
class ArchiverSetup:
|
|
EXE: str = None # python source based
|
|
FORK_DEFAULT = False
|
|
BORG_EXES: list[str] = []
|
|
|
|
def __init__(self):
|
|
self.archiver = None
|
|
self.tmpdir: str | None = None
|
|
self.repository_path: str | None = None
|
|
self.repository_location: str | None = None
|
|
self.input_path: str | None = None
|
|
self.output_path: str | None = None
|
|
self.keys_path: str | None = None
|
|
self.cache_path: str | None = None
|
|
self.exclude_file_path: str | None = None
|
|
self.patterns_file_path: str | None = None
|
|
|
|
def get_kind(self) -> str:
|
|
if self.repository_location.startswith("rest://"):
|
|
return "remote"
|
|
elif self.EXE == "borg.exe":
|
|
return "binary"
|
|
else:
|
|
return "local"
|
|
|
|
|
|
@pytest.fixture()
|
|
def archiver(tmp_path, set_env_variables):
|
|
archiver = ArchiverSetup()
|
|
archiver.archiver = not archiver.FORK_DEFAULT and Archiver() or None
|
|
archiver.tmpdir = tmp_path
|
|
archiver.repository_path = os.fspath(tmp_path / "repository")
|
|
archiver.repository_location = archiver.repository_path
|
|
archiver.input_path = os.fspath(tmp_path / "input")
|
|
archiver.output_path = os.fspath(tmp_path / "output")
|
|
archiver.keys_path = os.fspath(tmp_path / "keys")
|
|
archiver.cache_path = os.fspath(tmp_path / "cache")
|
|
archiver.exclude_file_path = os.fspath(tmp_path / "excludes")
|
|
archiver.patterns_file_path = os.fspath(tmp_path / "patterns")
|
|
os.environ["BORG_KEYS_DIR"] = archiver.keys_path
|
|
os.environ["BORG_CACHE_DIR"] = archiver.cache_path
|
|
os.mkdir(archiver.input_path)
|
|
# avoid troubles with fakeroot / FUSE:
|
|
os.chmod(archiver.input_path, 0o777) # nosec B103
|
|
os.mkdir(archiver.output_path)
|
|
os.mkdir(archiver.keys_path)
|
|
os.mkdir(archiver.cache_path)
|
|
with open(archiver.exclude_file_path, "wb") as fd:
|
|
fd.write(b"input/file2\n# A comment line, then a blank line\n\n")
|
|
with open(archiver.patterns_file_path, "wb") as fd:
|
|
fd.write(b"+input/file_important\n- input/file*\n# A comment line, then a blank line\n\n")
|
|
with contextlib.chdir(archiver.tmpdir):
|
|
yield archiver
|
|
|
|
def maybe_clear_flags_and_retry(func, path, _exc_info):
|
|
if has_lchflags:
|
|
# Clear any BSD flags (e.g. UF_APPEND) that may have prevented removal, then retry once.
|
|
try:
|
|
os.lchflags(path, 0)
|
|
func(path)
|
|
except OSError:
|
|
pass
|
|
else:
|
|
# Do nothing (equivalent to `ignore_errors=True`) if flags aren't supported on this platform.
|
|
pass
|
|
|
|
# Clean up archiver temp files
|
|
shutil.rmtree(archiver.tmpdir, onerror=maybe_clear_flags_and_retry)
|
|
|
|
|
|
@pytest.fixture()
|
|
def remote_archiver(archiver):
|
|
archiver.repository_location = "rest://" + "/" + str(archiver.repository_path)
|
|
yield archiver
|
|
|
|
|
|
@pytest.fixture()
|
|
def binary_archiver(archiver):
|
|
if "binary" not in BORG_EXES:
|
|
pytest.skip("No borg.exe binary available")
|
|
archiver.EXE = "borg.exe"
|
|
archiver.FORK_DEFAULT = True
|
|
yield archiver
|