239 lines
8.0 KiB
Python
239 lines
8.0 KiB
Python
"""Tests for DotfilesService."""
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from flow.core.config import AppConfig, FlowContext
|
|
from flow.core.console import Console
|
|
from flow.core.platform import PlatformInfo
|
|
from flow.core.runtime import CommandRunner, SystemRuntime
|
|
from flow.core import paths
|
|
from flow.services.dotfiles import DotfilesService
|
|
|
|
|
|
class FakeRunner(CommandRunner):
|
|
def __init__(self):
|
|
self.calls: list[list[str]] = []
|
|
|
|
def run(self, argv, *, cwd=None, env=None, capture_output=True, check=False, timeout=None):
|
|
command = [str(part) for part in argv]
|
|
self.calls.append(command)
|
|
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
|
|
|
|
|
|
def _make_ctx(tmp_path, console=None):
|
|
"""Build a FlowContext for testing."""
|
|
return FlowContext(
|
|
config=AppConfig(),
|
|
manifest={},
|
|
platform=PlatformInfo(),
|
|
console=console or Console(color=False),
|
|
runtime=SystemRuntime(),
|
|
)
|
|
|
|
|
|
def _setup_dotfiles(tmp_path, packages_files):
|
|
"""Set up a fake dotfiles directory structure.
|
|
packages_files: dict of {package_name: {relative_path: content}}
|
|
"""
|
|
dotfiles = tmp_path / "dotfiles"
|
|
shared = dotfiles / "_shared"
|
|
for pkg_name, files in packages_files.items():
|
|
pkg_dir = shared / pkg_name
|
|
for rel_path, content in files.items():
|
|
file_path = pkg_dir / rel_path
|
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
file_path.write_text(content)
|
|
return dotfiles
|
|
|
|
|
|
class TestDotfilesServiceLink:
|
|
def test_link_creates_symlinks(self, tmp_path, monkeypatch):
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
|
|
dotfiles = _setup_dotfiles(tmp_path, {
|
|
"zsh": {".zshrc": "# zsh config"},
|
|
"git": {".config/git/config": "[user]\n name = test"},
|
|
})
|
|
|
|
monkeypatch.setattr(paths, "HOME", home)
|
|
monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles)
|
|
monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules")
|
|
monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json")
|
|
|
|
ctx = _make_ctx(tmp_path)
|
|
svc = DotfilesService(ctx)
|
|
svc.link()
|
|
|
|
assert (home / ".zshrc").is_symlink()
|
|
assert (home / ".config" / "git" / "config").is_symlink()
|
|
|
|
def test_link_with_module(self, tmp_path, monkeypatch):
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
|
|
dotfiles = tmp_path / "dotfiles"
|
|
modules = tmp_path / "modules"
|
|
|
|
# Set up package with _module.yaml
|
|
pkg_dir = dotfiles / "_shared" / "nvim"
|
|
config_dir = pkg_dir / ".config" / "nvim"
|
|
config_dir.mkdir(parents=True)
|
|
(config_dir / "_module.yaml").write_text(yaml.dump({
|
|
"source": "github:test/nvim-config",
|
|
"ref": {"branch": "main"},
|
|
}))
|
|
|
|
# Set up local file outside mount path
|
|
(pkg_dir / ".local" / "bin").mkdir(parents=True)
|
|
(pkg_dir / ".local" / "bin" / "nvim-wrapper").write_text("#!/bin/sh")
|
|
|
|
# Set up cloned module
|
|
module_dir = modules / "_shared--nvim"
|
|
module_dir.mkdir(parents=True)
|
|
(module_dir / "init.lua").write_text("-- init")
|
|
(module_dir / "lua").mkdir()
|
|
(module_dir / "lua" / "plugins.lua").write_text("-- plugins")
|
|
|
|
monkeypatch.setattr(paths, "HOME", home)
|
|
monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles)
|
|
monkeypatch.setattr(paths, "MODULES_DIR", modules)
|
|
monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json")
|
|
|
|
ctx = _make_ctx(tmp_path)
|
|
svc = DotfilesService(ctx)
|
|
svc.link()
|
|
|
|
# Module files should be linked under .config/nvim/
|
|
assert (home / ".config" / "nvim" / "init.lua").is_symlink()
|
|
assert (home / ".config" / "nvim" / "lua" / "plugins.lua").is_symlink()
|
|
# Local file outside mount path should be linked
|
|
assert (home / ".local" / "bin" / "nvim-wrapper").is_symlink()
|
|
|
|
def test_unlink_removes_symlinks(self, tmp_path, monkeypatch):
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
|
|
dotfiles = _setup_dotfiles(tmp_path, {
|
|
"zsh": {".zshrc": "# zsh"},
|
|
})
|
|
|
|
monkeypatch.setattr(paths, "HOME", home)
|
|
monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles)
|
|
monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules")
|
|
monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json")
|
|
|
|
ctx = _make_ctx(tmp_path)
|
|
svc = DotfilesService(ctx)
|
|
|
|
# Link first
|
|
svc.link()
|
|
assert (home / ".zshrc").is_symlink()
|
|
|
|
# Then unlink
|
|
svc.unlink()
|
|
assert not (home / ".zshrc").exists()
|
|
|
|
def test_link_dry_run_no_changes(self, tmp_path, monkeypatch):
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
|
|
dotfiles = _setup_dotfiles(tmp_path, {
|
|
"zsh": {".zshrc": "# zsh"},
|
|
})
|
|
|
|
monkeypatch.setattr(paths, "HOME", home)
|
|
monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles)
|
|
monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules")
|
|
monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json")
|
|
|
|
ctx = _make_ctx(tmp_path)
|
|
svc = DotfilesService(ctx)
|
|
svc.link(dry_run=True)
|
|
|
|
# No symlinks should exist
|
|
assert not (home / ".zshrc").exists()
|
|
|
|
def test_status_shows_packages(self, tmp_path, monkeypatch, capsys):
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
|
|
dotfiles = _setup_dotfiles(tmp_path, {
|
|
"zsh": {".zshrc": "# zsh"},
|
|
})
|
|
|
|
monkeypatch.setattr(paths, "HOME", home)
|
|
monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles)
|
|
monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules")
|
|
monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json")
|
|
|
|
ctx = _make_ctx(tmp_path)
|
|
svc = DotfilesService(ctx)
|
|
|
|
# Link first to populate state
|
|
svc.link()
|
|
|
|
# Check status
|
|
svc.status()
|
|
output = capsys.readouterr().out
|
|
assert "zsh" in output
|
|
|
|
def test_relink_does_not_remove_unmanaged_file(self, tmp_path, monkeypatch):
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
|
|
dotfiles = _setup_dotfiles(tmp_path, {
|
|
"zsh": {".zshrc": "# zsh"},
|
|
})
|
|
|
|
monkeypatch.setattr(paths, "HOME", home)
|
|
monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles)
|
|
monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules")
|
|
monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json")
|
|
|
|
ctx = _make_ctx(tmp_path)
|
|
svc = DotfilesService(ctx)
|
|
svc.link()
|
|
|
|
target = home / ".zshrc"
|
|
target.unlink()
|
|
target.write_text("user managed file")
|
|
|
|
svc.link()
|
|
assert target.read_text() == "user managed file"
|
|
assert not target.is_symlink()
|
|
|
|
def test_sync_modules_includes_profile_layers(self, tmp_path, monkeypatch):
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
dotfiles = tmp_path / "dotfiles"
|
|
profile_pkg = dotfiles / "linux-work" / "nvim" / ".config" / "nvim"
|
|
profile_pkg.mkdir(parents=True)
|
|
(profile_pkg / "_module.yaml").write_text(yaml.dump({
|
|
"source": "github:test/nvim-config",
|
|
"ref": {"branch": "main"},
|
|
}))
|
|
|
|
monkeypatch.setattr(paths, "HOME", home)
|
|
monkeypatch.setattr(paths, "DOTFILES_DIR", dotfiles)
|
|
monkeypatch.setattr(paths, "MODULES_DIR", tmp_path / "modules")
|
|
monkeypatch.setattr(paths, "LINKED_STATE", tmp_path / "state" / "linked.json")
|
|
|
|
runtime = SystemRuntime()
|
|
runner = FakeRunner()
|
|
runtime.runner = runner
|
|
runtime.git.runner = runner
|
|
ctx = FlowContext(
|
|
config=AppConfig(),
|
|
manifest={},
|
|
platform=PlatformInfo(),
|
|
console=Console(color=False),
|
|
runtime=runtime,
|
|
)
|
|
|
|
DotfilesService(ctx).sync_modules()
|
|
assert any("linux-work--nvim" in " ".join(call) for call in runner.calls)
|