|
|
|
|
@@ -1,10 +1,17 @@
|
|
|
|
|
import errno
|
|
|
|
|
import itertools
|
|
|
|
|
import os
|
|
|
|
|
import struct
|
|
|
|
|
import subprocess
|
|
|
|
|
import tempfile
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest import TestCase
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
from unittest.mock import MagicMock, call, patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from app.helper.system import SystemHelper
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
from app.core.config import ConfigModel, settings
|
|
|
|
|
from app.utils.system import SystemUtils
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -172,3 +179,395 @@ def test_execute_with_subprocess_redacts_unknown_error_userinfo_and_invalid_port
|
|
|
|
|
assert not success
|
|
|
|
|
assert "https://example.com:notaport/simple" in message
|
|
|
|
|
assert "user:pass" not in message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fake_stat_with_devs(dev_by_path):
|
|
|
|
|
"""构造按路径返回指定 st_dev 的 os.stat 桩。"""
|
|
|
|
|
def _fake_stat(path, *args, **kwargs):
|
|
|
|
|
result = MagicMock()
|
|
|
|
|
result.st_dev = dev_by_path[str(path)]
|
|
|
|
|
return result
|
|
|
|
|
return _fake_stat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fill_fs_info(fsid, num_devices=1, truncate=False):
|
|
|
|
|
"""构造填充 BTRFS_IOC_FS_INFO 缓冲区的 ioctl 桩。"""
|
|
|
|
|
def _ioctl(fd, request, buffer, mutate_flag):
|
|
|
|
|
assert fd == 42
|
|
|
|
|
assert request == 0x8400941F
|
|
|
|
|
assert mutate_flag is True
|
|
|
|
|
if truncate:
|
|
|
|
|
del buffer[64:]
|
|
|
|
|
return 0
|
|
|
|
|
struct.pack_into("=Q", buffer, 8, num_devices)
|
|
|
|
|
buffer[16:32] = fsid
|
|
|
|
|
return 0
|
|
|
|
|
return _ioctl
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def linux_platform(monkeypatch):
|
|
|
|
|
"""将当前用例的平台标识切换为 Linux amd64。"""
|
|
|
|
|
monkeypatch.setattr("app.utils.system.sys.platform", "linux")
|
|
|
|
|
monkeypatch.setattr(SystemUtils, "is_x86_64", lambda: True)
|
|
|
|
|
monkeypatch.setattr(SystemUtils, "is_aarch64", lambda: False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("num_devices", [1, 2])
|
|
|
|
|
def test_get_btrfs_fsid_reads_kernel_result_and_closes_fd(num_devices, linux_platform):
|
|
|
|
|
fsid = bytes.fromhex("88e3aff5fa2946d591a55977be984655")
|
|
|
|
|
with patch("app.utils.system.os.open", return_value=42), \
|
|
|
|
|
patch("app.utils.system.fcntl.ioctl", side_effect=_fill_fs_info(fsid, num_devices)), \
|
|
|
|
|
patch("app.utils.system.os.close") as close_mock:
|
|
|
|
|
result = SystemUtils._get_btrfs_fsid(Path("/data"))
|
|
|
|
|
|
|
|
|
|
assert result == fsid
|
|
|
|
|
close_mock.assert_called_once_with(42)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("error_number", [errno.ENOTTY, errno.EACCES, errno.EPERM, errno.EINVAL])
|
|
|
|
|
def test_get_btrfs_fsid_falls_back_on_expected_ioctl_errors(error_number, linux_platform):
|
|
|
|
|
with patch("app.utils.system.os.open", return_value=42), \
|
|
|
|
|
patch("app.utils.system.fcntl.ioctl", side_effect=OSError(error_number, os.strerror(error_number))), \
|
|
|
|
|
patch("app.utils.system.os.close") as close_mock:
|
|
|
|
|
result = SystemUtils._get_btrfs_fsid(Path("/data"))
|
|
|
|
|
|
|
|
|
|
assert result is None
|
|
|
|
|
close_mock.assert_called_once_with(42)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_btrfs_fsid_falls_back_when_directory_cannot_be_opened(linux_platform):
|
|
|
|
|
with patch("app.utils.system.os.open", side_effect=OSError(errno.EACCES, "denied")), \
|
|
|
|
|
patch("app.utils.system.os.close") as close_mock:
|
|
|
|
|
result = SystemUtils._get_btrfs_fsid(Path("/data"))
|
|
|
|
|
|
|
|
|
|
assert result is None
|
|
|
|
|
close_mock.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("fsid", "num_devices", "truncate"),
|
|
|
|
|
[
|
|
|
|
|
(bytes(16), 1, False),
|
|
|
|
|
(b"valid-fsid-value", 0, False),
|
|
|
|
|
(b"valid-fsid-value", 1, True),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_get_btrfs_fsid_rejects_invalid_kernel_results(fsid, num_devices, truncate, linux_platform):
|
|
|
|
|
with patch("app.utils.system.os.open", return_value=42), \
|
|
|
|
|
patch("app.utils.system.fcntl.ioctl", side_effect=_fill_fs_info(fsid, num_devices, truncate)), \
|
|
|
|
|
patch("app.utils.system.os.close") as close_mock:
|
|
|
|
|
result = SystemUtils._get_btrfs_fsid(Path("/data"))
|
|
|
|
|
|
|
|
|
|
assert result is None
|
|
|
|
|
close_mock.assert_called_once_with(42)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_btrfs_fsid_is_disabled_outside_linux():
|
|
|
|
|
with patch("app.utils.system.sys.platform", "darwin"), \
|
|
|
|
|
patch("app.utils.system.os.open") as open_mock:
|
|
|
|
|
result = SystemUtils._get_btrfs_fsid(Path("/data"))
|
|
|
|
|
|
|
|
|
|
assert result is None
|
|
|
|
|
open_mock.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_btrfs_fsid_is_disabled_on_unsupported_linux_architecture():
|
|
|
|
|
with patch("app.utils.system.sys.platform", "linux"), \
|
|
|
|
|
patch.object(SystemUtils, "is_x86_64", return_value=False), \
|
|
|
|
|
patch.object(SystemUtils, "is_aarch64", return_value=False), \
|
|
|
|
|
patch("app.utils.system.os.open") as open_mock:
|
|
|
|
|
result = SystemUtils._get_btrfs_fsid(Path("/data"))
|
|
|
|
|
|
|
|
|
|
assert result is None
|
|
|
|
|
open_mock.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_btrfs_fsid_dedup_setting_is_opt_in():
|
|
|
|
|
assert ConfigModel().BTRFS_FSID_DEDUP is False
|
|
|
|
|
assert ConfigModel(BTRFS_FSID_DEDUP="true").BTRFS_FSID_DEDUP is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_default_path_does_not_read_fsid():
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2)]
|
|
|
|
|
dev_by_path = {str(paths[0]): 38, str(paths[1]): 32}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid") as fsid_mock, \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths)
|
|
|
|
|
|
|
|
|
|
assert total == 4.0
|
|
|
|
|
assert free == 2.0
|
|
|
|
|
fsid_mock.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("system_platform", ["darwin", "win32"])
|
|
|
|
|
def test_space_usage_opt_in_does_not_read_fsid_outside_linux(system_platform):
|
|
|
|
|
path = MagicMock()
|
|
|
|
|
path.exists.return_value = True
|
|
|
|
|
path.drive = "D:"
|
|
|
|
|
with patch("app.utils.system.sys.platform", system_platform), \
|
|
|
|
|
patch.object(SystemUtils, "is_x86_64") as x86_mock, \
|
|
|
|
|
patch.object(SystemUtils, "is_aarch64") as arm_mock, \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid") as fsid_mock, \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0), \
|
|
|
|
|
patch("app.utils.system.os.stat", return_value=MagicMock(st_dev=38)):
|
|
|
|
|
total, free = SystemUtils.space_usage([path], btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 2.0
|
|
|
|
|
assert free == 1.0
|
|
|
|
|
x86_mock.assert_not_called()
|
|
|
|
|
arm_mock.assert_not_called()
|
|
|
|
|
fsid_mock.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_opt_in_uses_original_behavior_on_unsupported_linux_architecture():
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2)]
|
|
|
|
|
dev_by_path = {str(path): 38 for path in paths}
|
|
|
|
|
with patch("app.utils.system.sys.platform", "linux"), \
|
|
|
|
|
patch.object(SystemUtils, "is_x86_64", return_value=False), \
|
|
|
|
|
patch.object(SystemUtils, "is_aarch64", return_value=False), \
|
|
|
|
|
patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid") as fsid_mock, \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 2.0
|
|
|
|
|
assert free == 1.0
|
|
|
|
|
fsid_mock.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(("is_x86_64", "is_aarch64"), [(True, False), (False, True)])
|
|
|
|
|
def test_space_usage_merges_btrfs_subvolumes_with_same_fsid(is_x86_64, is_aarch64, monkeypatch):
|
|
|
|
|
monkeypatch.setattr("app.utils.system.sys.platform", "linux")
|
|
|
|
|
monkeypatch.setattr(SystemUtils, "is_x86_64", lambda: is_x86_64)
|
|
|
|
|
monkeypatch.setattr(SystemUtils, "is_aarch64", lambda: is_aarch64)
|
|
|
|
|
fsid = bytes.fromhex("88e3aff5fa2946d591a55977be984655")
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2)]
|
|
|
|
|
dev_by_path = {str(paths[0]): 38, str(paths[1]): 32}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid", side_effect=[fsid, fsid]), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=3.49 * 1024 ** 4), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.94 * 1024 ** 4):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 3.49 * 1024 ** 4
|
|
|
|
|
assert free == 1.94 * 1024 ** 4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_counts_different_btrfs_fsids_separately(linux_platform):
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2)]
|
|
|
|
|
dev_by_path = {str(paths[0]): 38, str(paths[1]): 32}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid", side_effect=[b"a" * 16, b"b" * 16]), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 4.0
|
|
|
|
|
assert free == 2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_falls_back_to_st_dev_when_fsid_is_unavailable(linux_platform):
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2)]
|
|
|
|
|
dev_by_path = {str(paths[0]): 60, str(paths[1]): 60}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid", return_value=None), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 2.0
|
|
|
|
|
assert free == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("fsids", [(b"a" * 16, None), (None, b"a" * 16)])
|
|
|
|
|
def test_space_usage_keeps_st_dev_dedup_when_fsid_availability_differs(fsids, linux_platform):
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2)]
|
|
|
|
|
dev_by_path = {str(path): 38 for path in paths}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid", side_effect=fsids), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 2.0
|
|
|
|
|
assert free == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_keeps_st_dev_dedup_when_fsid_is_consistent(linux_platform):
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2)]
|
|
|
|
|
dev_by_path = {str(path): 38 for path in paths}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid", side_effect=[b"a" * 16, b"a" * 16]), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 2.0
|
|
|
|
|
assert free == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_does_not_merge_different_st_devs_when_one_fsid_is_unavailable(linux_platform):
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2)]
|
|
|
|
|
dev_by_path = {str(paths[0]): 38, str(paths[1]): 32}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid", side_effect=[b"a" * 16, None]), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 4.0
|
|
|
|
|
assert free == 2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_counts_a_repeated_path_once():
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
path = Path(tmp)
|
|
|
|
|
with patch.object(SystemUtils, "_get_btrfs_fsid", return_value=None), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage([path, path])
|
|
|
|
|
|
|
|
|
|
assert total == 2.0
|
|
|
|
|
assert free == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_merges_consistent_fsid_observed_within_same_st_dev(linux_platform):
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2, \
|
|
|
|
|
tempfile.TemporaryDirectory() as tmp3:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2), Path(tmp3)]
|
|
|
|
|
dev_by_path = {str(paths[0]): 38, str(paths[1]): 38, str(paths[2]): 32}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid", side_effect=[None, b"a" * 16, b"a" * 16]), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 2.0
|
|
|
|
|
assert free == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_transitive_merge_is_independent_of_path_order(linux_platform):
|
|
|
|
|
fsid = b"a" * 16
|
|
|
|
|
records = [("first", 38, None), ("bridge", 38, fsid), ("other", 32, fsid)]
|
|
|
|
|
|
|
|
|
|
for permutation in itertools.permutations(records):
|
|
|
|
|
paths = [MagicMock(name=name) for name, _, _ in permutation]
|
|
|
|
|
for path in paths:
|
|
|
|
|
path.exists.return_value = True
|
|
|
|
|
dev_by_path = {str(path): record[1] for path, record in zip(paths, permutation)}
|
|
|
|
|
fsid_by_path = {str(path): record[2] for path, record in zip(paths, permutation)}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid", side_effect=lambda path: fsid_by_path[str(path)]), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 2.0
|
|
|
|
|
assert free == 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_conflicting_fsids_do_not_bridge_independent_groups(linux_platform):
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2, \
|
|
|
|
|
tempfile.TemporaryDirectory() as tmp3, tempfile.TemporaryDirectory() as tmp4:
|
|
|
|
|
paths = [Path(tmp1), Path(tmp2), Path(tmp3), Path(tmp4)]
|
|
|
|
|
dev_by_path = {
|
|
|
|
|
str(paths[0]): 38,
|
|
|
|
|
str(paths[1]): 38,
|
|
|
|
|
str(paths[2]): 32,
|
|
|
|
|
str(paths[3]): 101,
|
|
|
|
|
}
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid",
|
|
|
|
|
side_effect=[b"a" * 16, b"b" * 16, b"a" * 16, b"b" * 16]), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 6.0
|
|
|
|
|
assert free == 3.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_uses_earliest_path_for_each_final_group(linux_platform):
|
|
|
|
|
fsid = b"a" * 16
|
|
|
|
|
paths = [MagicMock(name=name) for name in ("first", "same-dev", "same-fsid", "independent")]
|
|
|
|
|
for path in paths:
|
|
|
|
|
path.exists.return_value = True
|
|
|
|
|
dev_by_path = {str(paths[0]): 38, str(paths[1]): 38, str(paths[2]): 32, str(paths[3]): 101}
|
|
|
|
|
fsid_by_path = {str(paths[0]): None, str(paths[1]): fsid, str(paths[2]): fsid, str(paths[3]): None}
|
|
|
|
|
|
|
|
|
|
with patch("app.utils.system.os.stat", side_effect=_fake_stat_with_devs(dev_by_path)), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid", side_effect=lambda path: fsid_by_path[str(path)]), \
|
|
|
|
|
patch.object(SystemUtils, "total_space", side_effect=[2.0, 3.0]) as total_mock, \
|
|
|
|
|
patch.object(SystemUtils, "free_space", side_effect=[1.0, 1.5]) as free_mock:
|
|
|
|
|
total, free = SystemUtils.space_usage(paths, btrfs_fsid_dedup=True)
|
|
|
|
|
|
|
|
|
|
assert total == 5.0
|
|
|
|
|
assert free == 2.5
|
|
|
|
|
assert total_mock.call_args_list == [call(paths[0]), call(paths[3])]
|
|
|
|
|
assert free_mock.call_args_list == [call(paths[0]), call(paths[3])]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_space_usage_keeps_windows_drive_behavior_without_fsid_lookup():
|
|
|
|
|
path = MagicMock()
|
|
|
|
|
path.exists.return_value = True
|
|
|
|
|
path.drive = "D:"
|
|
|
|
|
with patch("app.utils.system.os.name", "nt"), \
|
|
|
|
|
patch.object(SystemUtils, "_get_btrfs_fsid") as fsid_mock, \
|
|
|
|
|
patch.object(SystemUtils, "total_space", return_value=2.0), \
|
|
|
|
|
patch.object(SystemUtils, "free_space", return_value=1.0):
|
|
|
|
|
total, free = SystemUtils.space_usage([path])
|
|
|
|
|
|
|
|
|
|
assert total == 2.0
|
|
|
|
|
assert free == 1.0
|
|
|
|
|
fsid_mock.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_local_storage_usage_forwards_btrfs_fsid_setting():
|
|
|
|
|
from app.modules.filemanager.storages import local as local_storage_module
|
|
|
|
|
|
|
|
|
|
download_dir = MagicMock(download_path="/downloads")
|
|
|
|
|
library_dir = MagicMock(library_path="/library")
|
|
|
|
|
with patch.object(local_storage_module.settings, "BTRFS_FSID_DEDUP", True), \
|
|
|
|
|
patch.object(local_storage_module.DirectoryHelper, "get_local_download_dirs",
|
|
|
|
|
return_value=[download_dir]), \
|
|
|
|
|
patch.object(local_storage_module.DirectoryHelper, "get_local_library_dirs",
|
|
|
|
|
return_value=[library_dir]), \
|
|
|
|
|
patch.object(SystemUtils, "space_usage", return_value=(4.0, 2.0)) as usage_mock:
|
|
|
|
|
usage = object.__new__(local_storage_module.LocalStorage).usage()
|
|
|
|
|
|
|
|
|
|
assert usage.total == 4.0
|
|
|
|
|
assert usage.available == 2.0
|
|
|
|
|
usage_mock.assert_called_once_with(
|
|
|
|
|
[Path("/downloads"), Path("/library")],
|
|
|
|
|
btrfs_fsid_dedup=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_dashboard_downloader_forwards_btrfs_fsid_setting():
|
|
|
|
|
from app.api.endpoints import dashboard as dashboard_module
|
|
|
|
|
|
|
|
|
|
download_dir = MagicMock(download_path="/downloads")
|
|
|
|
|
with patch.object(dashboard_module.settings, "BTRFS_FSID_DEDUP", True), \
|
|
|
|
|
patch.object(dashboard_module.DirectoryHelper, "get_local_download_dirs",
|
|
|
|
|
return_value=[download_dir]), \
|
|
|
|
|
patch.object(SystemUtils, "space_usage", return_value=(4.0, 2.0)) as usage_mock, \
|
|
|
|
|
patch.object(dashboard_module.DashboardChain, "downloader_info", return_value=[]):
|
|
|
|
|
dashboard_module._build_downloader()
|
|
|
|
|
|
|
|
|
|
usage_mock.assert_called_once_with(
|
|
|
|
|
[Path("/downloads")],
|
|
|
|
|
btrfs_fsid_dedup=True,
|
|
|
|
|
)
|
|
|
|
|
|