Files
wehub-resource-sync 0418dc5cf9
CI / Python tests (3.10) (push) Has been cancelled
CI / Python tests (3.11) (push) Has been cancelled
CI / Python quality (push) Has been cancelled
CI / Frontend typecheck (push) Has been cancelled
Autopilot Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:45:00 +08:00

176 lines
6.2 KiB
Python

"""Tests for :mod:`openharness.utils.fs` atomic-write helpers."""
from __future__ import annotations
import json
import multiprocessing as mp
import os
import stat
import sys
from pathlib import Path
import pytest
from openharness.utils.fs import atomic_write_bytes, atomic_write_text
# ---------------------------------------------------------------------------
# Core behaviour
# ---------------------------------------------------------------------------
def test_atomic_write_text_creates_file(tmp_path: Path) -> None:
path = tmp_path / "out.json"
atomic_write_text(path, '{"hello": "world"}\n')
assert path.read_text() == '{"hello": "world"}\n'
def test_atomic_write_bytes_creates_file(tmp_path: Path) -> None:
path = tmp_path / "out.bin"
atomic_write_bytes(path, b"\x00\x01\x02")
assert path.read_bytes() == b"\x00\x01\x02"
def test_atomic_write_creates_parent_directory(tmp_path: Path) -> None:
path = tmp_path / "nested" / "deep" / "out.txt"
atomic_write_text(path, "hi")
assert path.read_text() == "hi"
def test_atomic_write_overwrites_existing_file(tmp_path: Path) -> None:
path = tmp_path / "out.txt"
path.write_text("old contents")
atomic_write_text(path, "new contents")
assert path.read_text() == "new contents"
def test_atomic_write_does_not_leave_tempfiles(tmp_path: Path) -> None:
path = tmp_path / "out.txt"
atomic_write_text(path, "payload")
assert path.exists()
leftover = [p for p in tmp_path.iterdir() if p != path]
assert leftover == []
# ---------------------------------------------------------------------------
# Mode handling
# ---------------------------------------------------------------------------
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes not enforced on Windows")
def test_mode_is_applied_to_new_file(tmp_path: Path) -> None:
path = tmp_path / "creds.json"
atomic_write_text(path, "secret", mode=0o600)
assert stat.S_IMODE(path.stat().st_mode) == 0o600
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes not enforced on Windows")
def test_credentials_are_never_world_readable(tmp_path: Path) -> None:
"""Regression test: the file must be 0o600 from the very first byte.
The previous ``write_text`` + ``chmod`` sequence left a window during
which a co-resident attacker could stat the file with the default umask
mode (commonly 0o644). The atomic helper closes that window by applying
the mode before the tempfile is renamed into place.
"""
path = tmp_path / "credentials.json"
atomic_write_text(
path,
json.dumps({"anthropic": {"api_key": "sk-secret"}}),
mode=0o600,
)
mode = stat.S_IMODE(path.stat().st_mode)
assert mode & 0o077 == 0, f"file is readable by group/other: {oct(mode)}"
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes not enforced on Windows")
def test_mode_preserved_on_overwrite_when_not_specified(tmp_path: Path) -> None:
path = tmp_path / "settings.json"
path.write_text("{}")
os.chmod(path, 0o640)
atomic_write_text(path, '{"updated": true}')
assert stat.S_IMODE(path.stat().st_mode) == 0o640
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes not enforced on Windows")
def test_explicit_mode_overrides_existing_mode(tmp_path: Path) -> None:
path = tmp_path / "credentials.json"
path.write_text("{}")
os.chmod(path, 0o644)
atomic_write_text(path, "{}", mode=0o600)
assert stat.S_IMODE(path.stat().st_mode) == 0o600
# ---------------------------------------------------------------------------
# Atomicity under write failure
# ---------------------------------------------------------------------------
def test_existing_file_is_untouched_on_write_failure(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""If the write raises before ``os.replace`` runs, the old file survives."""
path = tmp_path / "settings.json"
path.write_text('{"kept": true}')
def _boom(*args: object, **kwargs: object) -> None:
raise OSError("disk full")
monkeypatch.setattr("openharness.utils.fs.os.replace", _boom)
with pytest.raises(OSError, match="disk full"):
atomic_write_text(path, '{"overwritten": true}')
assert json.loads(path.read_text()) == {"kept": True}
leftover = sorted(p.name for p in tmp_path.iterdir() if p != path)
assert leftover == [], f"tempfile leaked: {leftover}"
# ---------------------------------------------------------------------------
# Concurrent writers (end-to-end — exercises lock + atomic write together)
# ---------------------------------------------------------------------------
def _concurrent_writer(target_path: str, lock_path: str, key: str, value: str) -> None:
"""Read-modify-write entry point for :func:`test_concurrent_writers_all_survive`.
Must be a module-level function so it is picklable by ``multiprocessing``.
"""
from openharness.utils.file_lock import exclusive_file_lock
from openharness.utils.fs import atomic_write_text
target = Path(target_path)
lock = Path(lock_path)
with exclusive_file_lock(lock):
if target.exists():
data = json.loads(target.read_text())
else:
data = {}
data[key] = value
atomic_write_text(target, json.dumps(data, indent=2) + "\n")
@pytest.mark.skipif(
sys.platform == "win32",
reason="POSIX fork semantics keep this test deterministic; skip on Windows CI",
)
def test_concurrent_writers_all_survive(tmp_path: Path) -> None:
"""Two concurrent read-modify-write processes must not lose updates."""
target = tmp_path / "credentials.json"
lock = tmp_path / "credentials.json.lock"
ctx = mp.get_context("fork")
writers = [
ctx.Process(target=_concurrent_writer, args=(str(target), str(lock), f"key_{i}", f"value_{i}"))
for i in range(8)
]
for w in writers:
w.start()
for w in writers:
w.join(timeout=10)
assert w.exitcode == 0, f"writer {w.pid} failed with exit code {w.exitcode}"
result = json.loads(target.read_text())
assert set(result) == {f"key_{i}" for i in range(8)}
assert all(result[f"key_{i}"] == f"value_{i}" for i in range(8))