Files
flow/tests/test_dotfiles_folding.py
2026-02-13 02:13:27 +02:00

299 lines
9.4 KiB
Python

"""Integration tests for dotfiles tree folding behavior."""
import os
from pathlib import Path
import pytest
from flow.commands.dotfiles import _discover_packages, _walk_package
from flow.core.config import AppConfig, FlowContext
from flow.core.console import ConsoleLogger
from flow.core.platform import PlatformInfo
from flow.core.stow import LinkTree, TreeFolder
@pytest.fixture
def ctx():
"""Create a mock FlowContext."""
return FlowContext(
config=AppConfig(),
manifest={},
platform=PlatformInfo(),
console=ConsoleLogger(),
)
@pytest.fixture
def dotfiles_with_nested(tmp_path):
"""Create dotfiles with nested directory structure for folding tests."""
common = tmp_path / "common"
# nvim package with nested config
nvim = common / "nvim" / ".config" / "nvim"
nvim.mkdir(parents=True)
(nvim / "init.lua").write_text("-- init")
(nvim / "lua").mkdir()
(nvim / "lua" / "config.lua").write_text("-- config")
(nvim / "lua" / "plugins.lua").write_text("-- plugins")
# zsh package with flat structure
zsh = common / "zsh"
zsh.mkdir(parents=True)
(zsh / ".zshrc").write_text("# zshrc")
(zsh / ".zshenv").write_text("# zshenv")
return tmp_path
@pytest.fixture
def home_dir(tmp_path):
"""Create a temporary home directory."""
home = tmp_path / "home"
home.mkdir()
return home
def test_tree_folding_single_package(dotfiles_with_nested, home_dir):
"""Test that a single package can be folded into directory symlink."""
# Discover nvim package
packages = _discover_packages(dotfiles_with_nested)
nvim_source = packages["nvim"]
# Build link tree
tree = LinkTree()
folder = TreeFolder(tree)
# Plan links for all nvim files
operations = []
for src, dst in _walk_package(nvim_source, home_dir):
ops = folder.plan_link(src, dst, "nvim")
operations.extend(ops)
# Execute operations
folder.execute_operations(operations, dry_run=False)
# Check that we created efficient symlinks
# In ideal case, we'd have one directory symlink instead of 3 file symlinks
nvim_config = home_dir / ".config" / "nvim"
# Verify links work
assert (nvim_config / "init.lua").exists()
assert (nvim_config / "lua" / "config.lua").exists()
def test_tree_unfolding_conflict(dotfiles_with_nested, home_dir):
"""Test that tree unfolds when second package needs same directory."""
common = dotfiles_with_nested / "common"
# Create second package that shares .config
tmux = common / "tmux" / ".config" / "tmux"
tmux.mkdir(parents=True)
(tmux / "tmux.conf").write_text("# tmux")
# First, link nvim (can fold .config/nvim)
tree = LinkTree()
folder = TreeFolder(tree)
nvim_source = common / "nvim"
for src, dst in _walk_package(nvim_source, home_dir):
ops = folder.plan_link(src, dst, "nvim")
folder.execute_operations(ops, dry_run=False)
# Now link tmux (should unfold if needed)
tmux_source = common / "tmux"
for src, dst in _walk_package(tmux_source, home_dir):
ops = folder.plan_link(src, dst, "tmux")
folder.execute_operations(ops, dry_run=False)
# Both packages should be linked
assert (home_dir / ".config" / "nvim" / "init.lua").exists()
assert (home_dir / ".config" / "tmux" / "tmux.conf").exists()
def test_state_format_with_directory_links(dotfiles_with_nested, home_dir):
"""Test that state file correctly tracks directory vs file links."""
tree = LinkTree()
# Add a directory link
tree.add_link(
home_dir / ".config" / "nvim",
dotfiles_with_nested / "common" / "nvim" / ".config" / "nvim",
"nvim",
is_dir_link=True,
)
# Add a file link
tree.add_link(
home_dir / ".zshrc",
dotfiles_with_nested / "common" / "zsh" / ".zshrc",
"zsh",
is_dir_link=False,
)
# Convert to state
state = tree.to_state()
# Verify format
assert state["version"] == 2
nvim_link = state["links"]["nvim"][str(home_dir / ".config" / "nvim")]
assert nvim_link["is_directory_link"] is True
zsh_link = state["links"]["zsh"][str(home_dir / ".zshrc")]
assert zsh_link["is_directory_link"] is False
def test_state_backward_compatibility_rejected(home_dir):
"""Old state format should be rejected (no backward compatibility)."""
old_state = {
"links": {
"zsh": {
str(home_dir / ".zshrc"): str(home_dir.parent / "dotfiles" / "zsh" / ".zshrc"),
}
}
}
with pytest.raises(RuntimeError, match="Unsupported linked state format"):
LinkTree.from_state(old_state)
def test_discover_packages_with_flow_package(tmp_path):
"""Test discovering the flow package itself from dotfiles."""
common = tmp_path / "common"
# Create flow package
flow_pkg = common / "flow" / ".config" / "flow"
flow_pkg.mkdir(parents=True)
(flow_pkg / "manifest.yaml").write_text("profiles: {}")
(flow_pkg / "config").write_text("[repository]\n")
packages = _discover_packages(tmp_path)
# Flow package should be discovered like any other
assert "flow" in packages
assert packages["flow"] == common / "flow"
def test_walk_flow_package(tmp_path):
"""Test walking the flow package structure."""
flow_pkg = tmp_path / "flow"
flow_config = flow_pkg / ".config" / "flow"
flow_config.mkdir(parents=True)
(flow_config / "manifest.yaml").write_text("profiles: {}")
(flow_config / "config").write_text("[repository]\n")
home = Path("/tmp/fakehome")
pairs = list(_walk_package(flow_pkg, home))
# Should find both files
assert len(pairs) == 2
targets = [str(t) for _, t in pairs]
assert str(home / ".config" / "flow" / "manifest.yaml") in targets
assert str(home / ".config" / "flow" / "config") in targets
def test_conflict_detection_before_execution(dotfiles_with_nested, home_dir):
"""Test that conflicts are detected before any changes are made."""
# Create existing file that conflicts
existing = home_dir / ".zshrc"
existing.parent.mkdir(parents=True, exist_ok=True)
existing.write_text("# existing zshrc")
# Try to link package that wants .zshrc
tree = LinkTree()
folder = TreeFolder(tree)
zsh_source = dotfiles_with_nested / "common" / "zsh"
operations = []
for src, dst in _walk_package(zsh_source, home_dir):
ops = folder.plan_link(src, dst, "zsh")
operations.extend(ops)
# Should detect conflict
conflicts = folder.detect_conflicts(operations)
assert len(conflicts) > 0
assert any("already exists" in c for c in conflicts)
# Original file should be unchanged
assert existing.read_text() == "# existing zshrc"
def test_profile_switching_relink(tmp_path):
"""Test switching between profiles maintains correct links."""
# Create profiles
common = tmp_path / "common"
profiles = tmp_path / "profiles"
# Common zsh
(common / "zsh").mkdir(parents=True)
(common / "zsh" / ".zshrc").write_text("# common zsh")
# Work profile override
(profiles / "work" / "zsh").mkdir(parents=True)
(profiles / "work" / "zsh" / ".zshrc").write_text("# work zsh")
# Personal profile override
(profiles / "personal" / "zsh").mkdir(parents=True)
(profiles / "personal" / "zsh" / ".zshrc").write_text("# personal zsh")
# Test that profile discovery works correctly
work_packages = _discover_packages(tmp_path, profile="work")
personal_packages = _discover_packages(tmp_path, profile="personal")
# Both should find zsh, but from different sources
assert "zsh" in work_packages
assert "zsh" in personal_packages
assert work_packages["zsh"] != personal_packages["zsh"]
def test_can_fold_empty_directory():
"""Test can_fold with empty directory."""
tree = LinkTree()
target_dir = Path("/home/user/.config/nvim")
# Empty directory - should be able to fold
assert tree.can_fold(target_dir, "nvim")
def test_can_fold_with_subdirectories():
"""Test can_fold with nested directory structure."""
tree = LinkTree()
base = Path("/home/user/.config/nvim")
# Add nested files from same package
tree.add_link(base / "init.lua", Path("/dotfiles/nvim/init.lua"), "nvim")
tree.add_link(base / "lua" / "config.lua", Path("/dotfiles/nvim/lua/config.lua"), "nvim")
tree.add_link(base / "lua" / "plugins" / "init.lua", Path("/dotfiles/nvim/lua/plugins/init.lua"), "nvim")
# Should be able to fold at base level
assert tree.can_fold(base, "nvim")
# Add file from different package
tree.add_link(base / "other.lua", Path("/dotfiles/other/other.lua"), "other")
# Now cannot fold
assert not tree.can_fold(base, "nvim")
def test_execute_operations_creates_parent_dirs(tmp_path):
"""Test that execute_operations creates necessary parent directories."""
tree = LinkTree()
folder = TreeFolder(tree)
source = tmp_path / "dotfiles" / "nvim" / ".config" / "nvim" / "init.lua"
target = tmp_path / "home" / ".config" / "nvim" / "init.lua"
# Create source
source.parent.mkdir(parents=True)
source.write_text("-- init")
# Target parent doesn't exist yet
assert not target.parent.exists()
# Plan and execute
ops = folder.plan_link(source, target, "nvim")
folder.execute_operations(ops, dry_run=False)
# Parent should be created
assert target.parent.exists()
assert target.is_symlink()