Files
flow/tests/test_core_runtime.py

229 lines
7.9 KiB
Python

"""Tests for flow.core.runtime."""
import json
import os
import pytest
from flow.adapters.containers import ContainerRuntime
from flow.core.errors import FlowError
from flow.core.runtime import CommandRunner, FileSystem, GitClient, SystemRuntime
from flow.adapters.tmux import TmuxClient
class TestFileSystem:
def test_ensure_dir_creates_nested(self, tmp_path):
fs = FileSystem()
target = tmp_path / "a" / "b" / "c"
fs.ensure_dir(target)
assert target.is_dir()
def test_write_and_read_text(self, tmp_path):
fs = FileSystem()
path = tmp_path / "test.txt"
fs.write_text(path, "hello")
assert fs.read_text(path) == "hello"
def test_write_and_read_json(self, tmp_path):
fs = FileSystem()
path = tmp_path / "data.json"
fs.write_json(path, {"key": "value"})
assert fs.read_json(path) == {"key": "value"}
def test_create_symlink(self, tmp_path):
fs = FileSystem()
source = tmp_path / "source"
source.write_text("content")
target = tmp_path / "link"
fs.create_symlink(source, target)
assert target.is_symlink()
assert target.resolve() == source.resolve()
def test_same_symlink_true(self, tmp_path):
fs = FileSystem()
source = tmp_path / "source"
source.write_text("content")
target = tmp_path / "link"
target.symlink_to(source)
assert fs.same_symlink(target, source) is True
def test_same_symlink_false(self, tmp_path):
fs = FileSystem()
source = tmp_path / "source"
source.write_text("content")
other = tmp_path / "other"
other.write_text("other")
target = tmp_path / "link"
target.symlink_to(other)
assert fs.same_symlink(target, source) is False
def test_remove_file(self, tmp_path):
fs = FileSystem()
path = tmp_path / "file"
path.write_text("x")
fs.remove_file(path)
assert not path.exists()
def test_remove_file_missing_ok(self, tmp_path):
fs = FileSystem()
fs.remove_file(tmp_path / "missing", missing_ok=True) # no error
def test_copy_file(self, tmp_path):
fs = FileSystem()
src = tmp_path / "src"
src.write_text("data")
dst = tmp_path / "sub" / "dst"
fs.copy_file(src, dst)
assert dst.read_text() == "data"
def test_write_json_atomic_leaves_no_tmp_residue(self, tmp_path):
fs = FileSystem()
path = tmp_path / "state" / "data.json"
fs.write_json(path, {"a": 1})
assert path.read_text()
assert json.loads(path.read_text()) == {"a": 1}
residue = list(path.parent.glob("*.tmp"))
assert residue == []
def test_write_json_overwrites_stale_tmp(self, tmp_path):
fs = FileSystem()
path = tmp_path / "data.json"
stale = path.with_suffix(path.suffix + ".tmp")
stale.write_text("garbage")
fs.write_json(path, {"k": "v"})
assert json.loads(path.read_text()) == {"k": "v"}
assert not stale.exists()
def test_remove_tree_raises_when_missing(self, tmp_path):
fs = FileSystem()
with pytest.raises(FileNotFoundError):
fs.remove_tree(tmp_path / "absent")
def test_remove_tree_missing_ok(self, tmp_path):
fs = FileSystem()
fs.remove_tree(tmp_path / "absent", missing_ok=True) # no error
def test_remove_tree_raises_on_permission_error(self, tmp_path):
fs = FileSystem()
# Create a parent dir we can't traverse, holding a child directory.
# If we can't actually drop permissions (e.g. running as root), skip.
if os.geteuid() == 0:
pytest.skip("Running as root: cannot simulate permission error")
parent = tmp_path / "locked"
parent.mkdir()
(parent / "child").mkdir()
try:
parent.chmod(0o500) # r-x: can list but not modify
with pytest.raises(OSError):
fs.remove_tree(parent / "child")
finally:
parent.chmod(0o700)
def test_create_symlink_refuses_real_file(self, tmp_path):
fs = FileSystem()
source = tmp_path / "source"
source.write_text("src")
target = tmp_path / "target"
target.write_text("user content")
with pytest.raises(FlowError, match="Refusing to overwrite"):
fs.create_symlink(source, target)
# Target untouched.
assert target.read_text() == "user content"
def test_create_symlink_refuses_foreign_symlink(self, tmp_path):
fs = FileSystem()
source = tmp_path / "source"
source.write_text("src")
elsewhere = tmp_path / "elsewhere"
elsewhere.write_text("other")
target = tmp_path / "target"
target.symlink_to(elsewhere)
with pytest.raises(FlowError, match="Refusing to overwrite"):
fs.create_symlink(source, target)
# Foreign symlink preserved.
assert target.readlink() == elsewhere
def test_create_symlink_when_target_absent(self, tmp_path):
fs = FileSystem()
source = tmp_path / "source"
source.write_text("src")
target = tmp_path / "sub" / "target"
fs.create_symlink(source, target)
assert target.is_symlink()
assert target.readlink() == source
def test_create_symlink_idempotent_overwrite(self, tmp_path):
fs = FileSystem()
source = tmp_path / "source"
source.write_text("src")
target = tmp_path / "target"
target.symlink_to(source)
fs.create_symlink(source, target) # must not raise
assert target.is_symlink()
assert target.readlink() == source
def test_remove_symlink_with_matching_expected(self, tmp_path):
fs = FileSystem()
source = tmp_path / "source"
source.write_text("src")
target = tmp_path / "target"
target.symlink_to(source)
fs.remove_symlink(target, expected_source=source)
assert not target.exists()
assert not target.is_symlink()
def test_remove_symlink_refuses_regular_file(self, tmp_path):
fs = FileSystem()
target = tmp_path / "target"
target.write_text("real file")
with pytest.raises(FlowError, match="Refusing to remove non-symlink"):
fs.remove_symlink(target)
assert target.read_text() == "real file"
def test_remove_symlink_refuses_mismatched_expected(self, tmp_path):
fs = FileSystem()
source = tmp_path / "source"
source.write_text("src")
elsewhere = tmp_path / "elsewhere"
elsewhere.write_text("other")
target = tmp_path / "target"
target.symlink_to(elsewhere)
with pytest.raises(FlowError, match="Refusing to remove symlink"):
fs.remove_symlink(target, expected_source=source)
# Untouched.
assert target.is_symlink()
assert target.readlink() == elsewhere
def test_remove_symlink_absent_is_noop(self, tmp_path):
fs = FileSystem()
fs.remove_symlink(tmp_path / "missing") # no error
class TestCommandRunner:
def test_run_echo(self):
runner = CommandRunner()
result = runner.run(["echo", "hello"], capture_output=True)
assert result.stdout.strip() == "hello"
def test_require_binary_finds_echo(self):
runner = CommandRunner()
path = runner.require_binary("echo")
assert path is not None
class TestSystemRuntime:
def test_creates_git_client(self):
rt = SystemRuntime()
assert isinstance(rt.git, GitClient)
assert rt.git.runner is rt.runner
def test_creates_tmux_client(self):
rt = SystemRuntime()
assert isinstance(rt.tmux, TmuxClient)
assert rt.tmux.runner is rt.runner
def test_creates_container_runtime(self):
rt = SystemRuntime()
assert isinstance(rt.containers, ContainerRuntime)
assert rt.containers.runner is rt.runner