flow
This commit is contained in:
310
tests/test_stow.py
Normal file
310
tests/test_stow.py
Normal file
@@ -0,0 +1,310 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user