coulomb-social/tests/test_backup_capacity.py
tegwick 11cab7298b
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m27s
Guard paired capture and mirror scratch capacity inside the writer lock
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
2026-10-30 00:37:37 +01:00

136 lines
5.9 KiB
Python

"""Capacity checks happen inside writer locks; actual file limits run in children."""
import importlib.util
import resource
import subprocess
import sys
from pathlib import Path
import pytest
from tests.test_paired_backup import NOW, setup
from tests.test_paired_backup import backup as backup
@pytest.fixture
def capacity(monkeypatch):
source = Path(__file__).resolve().parents[1] / "scripts/backup_capacity.py"
spec = importlib.util.spec_from_file_location("capacity_cases", source)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
monkeypatch.setattr(module, "active_file_limit", lambda: module.FILE_LIMIT)
monkeypatch.setattr(module, "disk_free", lambda path: 128 * 1024**3)
monkeypatch.setattr(module, "free_inodes", lambda path: 1000000)
return module
@pytest.mark.parametrize("phase,slots", [("capture", 7), ("resume", 3), ("mirror", 1)])
def test_reserve_includes_complete_phase_scratch_and_preserves_state(
capacity, tmp_path, phase, slots
):
tmp_path.chmod(0o700)
original = tmp_path / "retained.age"
original.write_bytes(b"retained synthetic ciphertext")
original.chmod(0o600)
before = original.read_bytes()
plan = capacity.require_capacity(tmp_path, phase=phase)
assert plan["status"] == "ready"
assert plan["state_reserve_bytes"] == slots * capacity.FILE_LIMIT + capacity.CONTROL_MARGIN
assert plan["disk_reserve_bytes"] == plan["state_reserve_bytes"] + capacity.FILE_LIMIT
assert not plan["filesystem_blocks_reserved"] and not plan["limits_owner_admitted"]
assert list(tmp_path.iterdir()) == [original] and original.read_bytes() == before
@pytest.mark.parametrize("condition", ["disk", "budget", "inodes", "unbounded"])
def test_limits_refuse_before_capture_or_pending_creation(backup, monkeypatch, tmp_path, condition):
state, calls, publications = setup(backup, monkeypatch, tmp_path)
if condition == "disk":
monkeypatch.setattr(backup.capacity, "disk_free", lambda path: 1)
elif condition == "budget":
monkeypatch.setattr(backup.capacity, "state_usage", lambda path: 8 * 1024**3)
elif condition == "inodes":
monkeypatch.setattr(backup.capacity, "free_inodes", lambda path: 0)
else:
monkeypatch.setattr(backup.capacity, "active_file_limit", lambda: resource.RLIM_INFINITY)
with pytest.raises(ValueError, match="capacity_review_required"):
backup.run_backup(state, tmp_path / "primary.py", "age1-synthetic", NOW)
assert not calls and not publications and not list(state.glob("pending-*"))
def test_read_only_preflight_distinguishes_unbounded_executor(capacity, tmp_path, monkeypatch):
tmp_path.chmod(0o700)
monkeypatch.setattr(capacity, "active_file_limit", lambda: resource.RLIM_INFINITY)
report = capacity.reserve_plan(tmp_path, require_file_limit=False)
assert report["status"] == "reviewed_unbounded_executor"
assert not report["bounded_file_executor"] and not list(tmp_path.iterdir())
with pytest.raises(ValueError, match="capacity_review_required"):
capacity.require_capacity(tmp_path)
def test_state_scan_counts_allocated_blocks_and_refuses_symlinks(capacity, tmp_path):
tmp_path.chmod(0o700)
path = tmp_path / "metadata.json"
path.write_bytes(b"{}")
path.chmod(0o600)
assert capacity.state_usage(tmp_path) >= max(path.stat().st_size, path.stat().st_blocks * 512)
(tmp_path / "pending-link").symlink_to(path)
with pytest.raises(ValueError, match="unsafe_state_entry"):
capacity.state_usage(tmp_path)
def test_resume_capacity_refusal_keeps_exact_pending_evidence(backup, monkeypatch, tmp_path):
state, _, _ = setup(backup, monkeypatch, tmp_path, "upload")
with pytest.raises(backup.Refused):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW)
before = {str(p): p.read_bytes() for p in state.rglob("*") if p.is_file()}
_, calls, publications = setup(backup, monkeypatch, tmp_path)
monkeypatch.setattr(backup.capacity, "disk_free", lambda path: 1)
with pytest.raises(ValueError, match="capacity_review_required"):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW, resume=True)
assert {str(p): p.read_bytes() for p in state.rglob("*") if p.is_file()} == before
assert not calls and not publications
def test_real_inherited_file_limit_raises_write_error_and_runs_cleanup(tmp_path):
root = Path(__file__).resolve().parents[1]
code = '''
import errno,json,subprocess,sys
from pathlib import Path
sys.path.insert(0,sys.argv[1])
import backup_capacity as capacity
assert capacity.enforce_file_limit(65536)==65536
assert capacity.enforce_file_limit(1073741824)==65536
child = r"""
import errno,json,resource,sys
from pathlib import Path
root=Path(sys.argv[1])
try:
(root/'bounded.bin').write_bytes(b'x'*131072)
except OSError as error:
assert error.errno==errno.EFBIG
print(json.dumps({'bounded':resource.getrlimit(resource.RLIMIT_FSIZE)[0]==65536}))
else:
raise AssertionError('file write escaped inherited cap')
finally:
(root/'cleanup-ran').write_text('cleanup')
"""
result=subprocess.run([sys.executable,'-c',child,sys.argv[2]],capture_output=True,text=True)
assert result.returncode==0
assert json.loads(result.stdout)['bounded']
assert (Path(sys.argv[2])/'cleanup-ran').read_text()=='cleanup'
assert (Path(sys.argv[2])/'bounded.bin').stat().st_size<=65536
print('inherited limit and finally cleanup verified')
'''
result = subprocess.run(
[sys.executable, "-c", code, str(root / "scripts"), str(tmp_path)],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
assert "cleanup verified" in result.stdout
def test_service_applies_file_limit_to_capture_and_mirror_commands():
root = Path(__file__).resolve().parents[1]
service = (root / "deploy/backup/coulomb-social-paired-backup.service").read_text()
assert "\nLimitFSIZE=1G\n" in service