311 lines
8.6 KiB
Python
311 lines
8.6 KiB
Python
"""Tests for flow.core.stow — GNU Stow-style tree folding/unfolding."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from flow.core.stow import LinkOperation, LinkTree, TreeFolder
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_home(tmp_path):
|
|
"""Create a temporary home directory."""
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
return home
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dotfiles(tmp_path):
|
|
"""Create a temporary dotfiles repository."""
|
|
dotfiles = tmp_path / "dotfiles"
|
|
dotfiles.mkdir()
|
|
return dotfiles
|
|
|
|
|
|
def test_linktree_add_remove():
|
|
"""Test basic LinkTree operations."""
|
|
tree = LinkTree()
|
|
source = Path("/dotfiles/zsh/.zshrc")
|
|
target = Path("/home/user/.zshrc")
|
|
|
|
tree.add_link(target, source, "zsh", is_dir_link=False)
|
|
assert target in tree.links
|
|
assert tree.links[target] == source
|
|
assert tree.packages[target] == "zsh"
|
|
assert not tree.is_directory_link(target)
|
|
|
|
tree.remove_link(target)
|
|
assert target not in tree.links
|
|
assert target not in tree.packages
|
|
|
|
|
|
def test_linktree_directory_link():
|
|
"""Test directory link tracking."""
|
|
tree = LinkTree()
|
|
source = Path("/dotfiles/nvim/.config/nvim")
|
|
target = Path("/home/user/.config/nvim")
|
|
|
|
tree.add_link(target, source, "nvim", is_dir_link=True)
|
|
assert tree.is_directory_link(target)
|
|
|
|
|
|
def test_linktree_can_fold_single_package():
|
|
"""Test can_fold with single package."""
|
|
tree = LinkTree()
|
|
target_dir = Path("/home/user/.config/nvim")
|
|
|
|
# Add files from same package
|
|
tree.add_link(target_dir / "init.lua", Path("/dotfiles/nvim/.config/nvim/init.lua"), "nvim")
|
|
tree.add_link(target_dir / "lua" / "config.lua", Path("/dotfiles/nvim/.config/nvim/lua/config.lua"), "nvim")
|
|
|
|
# Should be able to fold since all files are from same package
|
|
assert tree.can_fold(target_dir, "nvim")
|
|
|
|
|
|
def test_linktree_can_fold_multiple_packages():
|
|
"""Test can_fold with multiple packages."""
|
|
tree = LinkTree()
|
|
target_dir = Path("/home/user/.config")
|
|
|
|
# Add files from different packages
|
|
tree.add_link(target_dir / "nvim", Path("/dotfiles/nvim/.config/nvim"), "nvim", is_dir_link=True)
|
|
tree.add_link(target_dir / "tmux", Path("/dotfiles/tmux/.config/tmux"), "tmux", is_dir_link=True)
|
|
|
|
# Cannot fold .config since it has files from multiple packages
|
|
assert not tree.can_fold(target_dir, "nvim")
|
|
|
|
|
|
def test_linktree_from_state_old_format_rejected():
|
|
"""Old state format should be rejected (no backward compatibility)."""
|
|
state = {
|
|
"links": {
|
|
"zsh": {
|
|
"/home/user/.zshrc": "/dotfiles/zsh/.zshrc",
|
|
"/home/user/.zshenv": "/dotfiles/zsh/.zshenv",
|
|
}
|
|
}
|
|
}
|
|
|
|
with pytest.raises(RuntimeError, match="Unsupported linked state format"):
|
|
LinkTree.from_state(state)
|
|
|
|
|
|
def test_linktree_from_state_new_format():
|
|
"""Test loading from new state format (with is_directory_link)."""
|
|
state = {
|
|
"version": 2,
|
|
"links": {
|
|
"nvim": {
|
|
"/home/user/.config/nvim": {
|
|
"source": "/dotfiles/nvim/.config/nvim",
|
|
"is_directory_link": True,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tree = LinkTree.from_state(state)
|
|
target = Path("/home/user/.config/nvim")
|
|
assert target in tree.links
|
|
assert tree.is_directory_link(target)
|
|
assert tree.packages[target] == "nvim"
|
|
|
|
|
|
def test_linktree_to_state():
|
|
"""Test converting LinkTree to state format."""
|
|
tree = LinkTree()
|
|
tree.add_link(
|
|
Path("/home/user/.config/nvim"),
|
|
Path("/dotfiles/nvim/.config/nvim"),
|
|
"nvim",
|
|
is_dir_link=True,
|
|
)
|
|
tree.add_link(
|
|
Path("/home/user/.zshrc"),
|
|
Path("/dotfiles/zsh/.zshrc"),
|
|
"zsh",
|
|
is_dir_link=False,
|
|
)
|
|
|
|
state = tree.to_state()
|
|
assert state["version"] == 2
|
|
assert "nvim" in state["links"]
|
|
assert "zsh" in state["links"]
|
|
|
|
nvim_link = state["links"]["nvim"]["/home/user/.config/nvim"]
|
|
assert nvim_link["is_directory_link"] is True
|
|
|
|
zsh_link = state["links"]["zsh"]["/home/user/.zshrc"]
|
|
assert zsh_link["is_directory_link"] is False
|
|
|
|
|
|
def test_treefolder_plan_link_simple(temp_home, temp_dotfiles):
|
|
"""Test planning a simple file link."""
|
|
tree = LinkTree()
|
|
folder = TreeFolder(tree)
|
|
|
|
source = temp_dotfiles / "zsh" / ".zshrc"
|
|
target = temp_home / ".zshrc"
|
|
|
|
# Create source file
|
|
source.parent.mkdir(parents=True)
|
|
source.write_text("# zshrc")
|
|
|
|
ops = folder.plan_link(source, target, "zsh")
|
|
assert len(ops) == 1
|
|
assert ops[0].type == "create_symlink"
|
|
assert ops[0].source == source
|
|
assert ops[0].target == target
|
|
assert ops[0].package == "zsh"
|
|
|
|
|
|
def test_treefolder_detect_conflicts_existing_file(temp_home, temp_dotfiles):
|
|
"""Test conflict detection for existing files."""
|
|
tree = LinkTree()
|
|
folder = TreeFolder(tree)
|
|
|
|
source = temp_dotfiles / "zsh" / ".zshrc"
|
|
target = temp_home / ".zshrc"
|
|
|
|
# Create existing file
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text("# existing")
|
|
|
|
source.parent.mkdir(parents=True)
|
|
source.write_text("# zshrc")
|
|
|
|
ops = folder.plan_link(source, target, "zsh")
|
|
conflicts = folder.detect_conflicts(ops)
|
|
|
|
assert len(conflicts) == 1
|
|
assert "already exists" in conflicts[0]
|
|
|
|
|
|
def test_treefolder_detect_conflicts_different_package(temp_home, temp_dotfiles):
|
|
"""Test conflict detection for links from different packages."""
|
|
tree = LinkTree()
|
|
target = temp_home / ".bashrc"
|
|
|
|
# Add existing link from different package
|
|
tree.add_link(target, Path("/dotfiles/bash/.bashrc"), "bash")
|
|
|
|
folder = TreeFolder(tree)
|
|
source = temp_dotfiles / "zsh" / ".bashrc"
|
|
source.parent.mkdir(parents=True)
|
|
source.write_text("# bashrc")
|
|
|
|
ops = folder.plan_link(source, target, "zsh")
|
|
conflicts = folder.detect_conflicts(ops)
|
|
|
|
assert len(conflicts) == 1
|
|
assert "bash" in conflicts[0]
|
|
|
|
|
|
def test_treefolder_execute_operations_dry_run(temp_home, temp_dotfiles, capsys):
|
|
"""Test dry-run mode."""
|
|
tree = LinkTree()
|
|
folder = TreeFolder(tree)
|
|
|
|
source = temp_dotfiles / "zsh" / ".zshrc"
|
|
target = temp_home / ".zshrc"
|
|
|
|
source.parent.mkdir(parents=True)
|
|
source.write_text("# zshrc")
|
|
|
|
ops = folder.plan_link(source, target, "zsh")
|
|
folder.execute_operations(ops, dry_run=True)
|
|
|
|
# Check output
|
|
captured = capsys.readouterr()
|
|
assert "FILE LINK" in captured.out
|
|
assert str(target) in captured.out
|
|
|
|
# No actual symlink created
|
|
assert not target.exists()
|
|
|
|
|
|
def test_treefolder_execute_operations_create_symlink(temp_home, temp_dotfiles):
|
|
"""Test creating actual symlinks."""
|
|
tree = LinkTree()
|
|
folder = TreeFolder(tree)
|
|
|
|
source = temp_dotfiles / "zsh" / ".zshrc"
|
|
target = temp_home / ".zshrc"
|
|
|
|
source.parent.mkdir(parents=True)
|
|
source.write_text("# zshrc")
|
|
|
|
ops = folder.plan_link(source, target, "zsh")
|
|
folder.execute_operations(ops, dry_run=False)
|
|
|
|
# Check symlink created
|
|
assert target.is_symlink()
|
|
assert target.resolve() == source.resolve()
|
|
|
|
# Check tree updated
|
|
assert target in folder.tree.links
|
|
|
|
|
|
def test_treefolder_plan_unlink(temp_home, temp_dotfiles):
|
|
"""Test planning unlink operations."""
|
|
tree = LinkTree()
|
|
target = temp_home / ".zshrc"
|
|
source = temp_dotfiles / "zsh" / ".zshrc"
|
|
|
|
tree.add_link(target, source, "zsh")
|
|
|
|
folder = TreeFolder(tree)
|
|
ops = folder.plan_unlink(target, "zsh")
|
|
|
|
assert len(ops) == 1
|
|
assert ops[0].type == "remove"
|
|
assert ops[0].target == target
|
|
|
|
|
|
def test_treefolder_plan_unlink_directory_link(temp_home, temp_dotfiles):
|
|
"""Test planning unlink for directory symlink."""
|
|
tree = LinkTree()
|
|
target = temp_home / ".config" / "nvim"
|
|
source = temp_dotfiles / "nvim" / ".config" / "nvim"
|
|
|
|
tree.add_link(target, source, "nvim", is_dir_link=True)
|
|
|
|
folder = TreeFolder(tree)
|
|
ops = folder.plan_unlink(target, "nvim")
|
|
|
|
# Should remove the directory link
|
|
assert len(ops) >= 1
|
|
assert ops[-1].type == "remove"
|
|
assert ops[-1].is_directory_link
|
|
|
|
|
|
def test_linkoperation_str():
|
|
"""Test LinkOperation string representation."""
|
|
op1 = LinkOperation(
|
|
type="create_symlink",
|
|
source=Path("/src"),
|
|
target=Path("/dst"),
|
|
package="test",
|
|
is_directory_link=False,
|
|
)
|
|
assert "FILE LINK" in str(op1)
|
|
|
|
op2 = LinkOperation(
|
|
type="create_symlink",
|
|
source=Path("/src"),
|
|
target=Path("/dst"),
|
|
package="test",
|
|
is_directory_link=True,
|
|
)
|
|
assert "DIR LINK" in str(op2)
|
|
|
|
op3 = LinkOperation(
|
|
type="unfold",
|
|
source=Path("/src"),
|
|
target=Path("/dst"),
|
|
package="test",
|
|
)
|
|
assert "UNFOLD" in str(op3)
|