277 lines
7.6 KiB
Python
277 lines
7.6 KiB
Python
"""Containerized e2e tests for dotfiles link safety.
|
|
|
|
These tests are opt-in and run only when FLOW_RUN_E2E_CONTAINER=1.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _docker_available() -> bool:
|
|
if shutil.which("docker") is None:
|
|
return False
|
|
|
|
result = subprocess.run(
|
|
["docker", "info"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
return result.returncode == 0
|
|
|
|
|
|
def _require_container_e2e() -> None:
|
|
if os.environ.get("FLOW_RUN_E2E_CONTAINER") != "1":
|
|
pytest.skip("Set FLOW_RUN_E2E_CONTAINER=1 to run container e2e tests")
|
|
if not _docker_available():
|
|
pytest.skip("Docker is required for container e2e tests")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def e2e_image(tmp_path_factory):
|
|
_require_container_e2e()
|
|
|
|
context_dir = tmp_path_factory.mktemp("flow-e2e-docker-context")
|
|
dockerfile = context_dir / "Dockerfile"
|
|
dockerfile.write_text(
|
|
"FROM python:3.11-slim\n"
|
|
"RUN apt-get update && apt-get install -y --no-install-recommends sudo && rm -rf /var/lib/apt/lists/*\n"
|
|
"RUN pip install --no-cache-dir pyyaml\n"
|
|
"RUN useradd -m -s /bin/bash flow\n"
|
|
"RUN echo 'flow ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/flow && chmod 440 /etc/sudoers.d/flow\n"
|
|
"USER flow\n"
|
|
"WORKDIR /workspace\n"
|
|
)
|
|
|
|
tag = f"flow-e2e-{uuid.uuid4().hex[:10]}"
|
|
subprocess.run(
|
|
["docker", "build", "-t", tag, str(context_dir)],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
try:
|
|
yield tag
|
|
finally:
|
|
subprocess.run(["docker", "rmi", "-f", tag], capture_output=True, text=True, check=False)
|
|
|
|
|
|
def _run_in_container(image_tag: str, script: str) -> subprocess.CompletedProcess:
|
|
return subprocess.run(
|
|
[
|
|
"docker",
|
|
"run",
|
|
"--rm",
|
|
"-v",
|
|
f"{REPO_ROOT}:/workspace/flow-cli:ro",
|
|
image_tag,
|
|
"bash",
|
|
"-lc",
|
|
script,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
|
|
def _assert_ok(run: subprocess.CompletedProcess) -> None:
|
|
if run.returncode != 0:
|
|
raise AssertionError(f"Container e2e failed:\nSTDOUT:\n{run.stdout}\nSTDERR:\n{run.stderr}")
|
|
|
|
|
|
def test_e2e_link_and_undo_with_root_targets(e2e_image):
|
|
script = r"""
|
|
set -euo pipefail
|
|
export HOME=/home/flow
|
|
export XDG_DATA_HOME=/tmp/xdg-data
|
|
export XDG_CONFIG_HOME=/tmp/xdg-config
|
|
export XDG_STATE_HOME=/tmp/xdg-state
|
|
mkdir -p "$XDG_DATA_HOME/flow/dotfiles" "$XDG_CONFIG_HOME/flow" "$XDG_STATE_HOME/flow"
|
|
|
|
dot="$XDG_DATA_HOME/flow/dotfiles"
|
|
mkdir -p "$dot/_shared/zsh"
|
|
mkdir -p "$dot/_shared/rootpkg/_root/tmp"
|
|
echo '# managed zshrc' > "$dot/_shared/zsh/.zshrc"
|
|
echo 'root-target' > "$dot/_shared/rootpkg/_root/tmp/flow-e2e-root-target"
|
|
|
|
echo '# before' > "$HOME/.zshrc"
|
|
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles link --force
|
|
test -L "$HOME/.zshrc"
|
|
test -L /tmp/flow-e2e-root-target
|
|
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles undo
|
|
test -f "$HOME/.zshrc"
|
|
test ! -L "$HOME/.zshrc"
|
|
grep -q '^# before$' "$HOME/.zshrc"
|
|
test ! -e /tmp/flow-e2e-root-target
|
|
"""
|
|
_assert_ok(_run_in_container(e2e_image, script))
|
|
|
|
|
|
def test_e2e_dry_run_force_is_read_only_in_both_flag_orders(e2e_image):
|
|
script = r"""
|
|
set -euo pipefail
|
|
export HOME=/home/flow
|
|
export XDG_DATA_HOME=/tmp/xdg-data
|
|
export XDG_CONFIG_HOME=/tmp/xdg-config
|
|
export XDG_STATE_HOME=/tmp/xdg-state
|
|
mkdir -p "$XDG_DATA_HOME/flow/dotfiles" "$XDG_CONFIG_HOME/flow" "$XDG_STATE_HOME/flow"
|
|
|
|
dot="$XDG_DATA_HOME/flow/dotfiles"
|
|
mkdir -p "$dot/_shared/zsh"
|
|
echo '# managed zshrc' > "$dot/_shared/zsh/.zshrc"
|
|
echo '# original' > "$HOME/.zshrc"
|
|
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles link --dry-run --force
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles link --force --dry-run
|
|
|
|
test -f "$HOME/.zshrc"
|
|
test ! -L "$HOME/.zshrc"
|
|
grep -q '^# original$' "$HOME/.zshrc"
|
|
|
|
state="$XDG_STATE_HOME/flow/linked.json"
|
|
if [ -f "$state" ]; then
|
|
python - "$state" <<'PY'
|
|
import json, sys
|
|
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
assert data.get("links", {}) == {}, data
|
|
assert "last_transaction" not in data, data
|
|
PY
|
|
fi
|
|
"""
|
|
_assert_ok(_run_in_container(e2e_image, script))
|
|
|
|
|
|
def test_e2e_unmanaged_conflict_without_force_is_non_destructive(e2e_image):
|
|
script = r"""
|
|
set -euo pipefail
|
|
export HOME=/home/flow
|
|
export XDG_DATA_HOME=/tmp/xdg-data
|
|
export XDG_CONFIG_HOME=/tmp/xdg-config
|
|
export XDG_STATE_HOME=/tmp/xdg-state
|
|
mkdir -p "$XDG_DATA_HOME/flow/dotfiles" "$XDG_CONFIG_HOME/flow" "$XDG_STATE_HOME/flow"
|
|
|
|
dot="$XDG_DATA_HOME/flow/dotfiles"
|
|
mkdir -p "$dot/_shared/zsh"
|
|
echo '# managed zshrc' > "$dot/_shared/zsh/.zshrc"
|
|
echo '# user-file' > "$HOME/.zshrc"
|
|
|
|
set +e
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles link
|
|
rc=$?
|
|
set -e
|
|
test "$rc" -ne 0
|
|
|
|
test -f "$HOME/.zshrc"
|
|
test ! -L "$HOME/.zshrc"
|
|
grep -q '^# user-file$' "$HOME/.zshrc"
|
|
"""
|
|
_assert_ok(_run_in_container(e2e_image, script))
|
|
|
|
|
|
def test_e2e_managed_drift_requires_force(e2e_image):
|
|
script = r"""
|
|
set -euo pipefail
|
|
export HOME=/home/flow
|
|
export XDG_DATA_HOME=/tmp/xdg-data
|
|
export XDG_CONFIG_HOME=/tmp/xdg-config
|
|
export XDG_STATE_HOME=/tmp/xdg-state
|
|
mkdir -p "$XDG_DATA_HOME/flow/dotfiles" "$XDG_CONFIG_HOME/flow" "$XDG_STATE_HOME/flow"
|
|
|
|
dot="$XDG_DATA_HOME/flow/dotfiles"
|
|
mkdir -p "$dot/_shared/zsh"
|
|
echo '# managed zshrc' > "$dot/_shared/zsh/.zshrc"
|
|
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles link --force
|
|
test -L "$HOME/.zshrc"
|
|
|
|
rm -f "$HOME/.zshrc"
|
|
echo '# drifted-manual' > "$HOME/.zshrc"
|
|
|
|
set +e
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles link
|
|
rc=$?
|
|
set -e
|
|
test "$rc" -ne 0
|
|
test -f "$HOME/.zshrc"
|
|
test ! -L "$HOME/.zshrc"
|
|
grep -q '^# drifted-manual$' "$HOME/.zshrc"
|
|
"""
|
|
_assert_ok(_run_in_container(e2e_image, script))
|
|
|
|
|
|
def test_e2e_directory_conflict_is_atomic_even_with_force(e2e_image):
|
|
script = r"""
|
|
set -euo pipefail
|
|
export HOME=/home/flow
|
|
export XDG_DATA_HOME=/tmp/xdg-data
|
|
export XDG_CONFIG_HOME=/tmp/xdg-config
|
|
export XDG_STATE_HOME=/tmp/xdg-state
|
|
mkdir -p "$XDG_DATA_HOME/flow/dotfiles" "$XDG_CONFIG_HOME/flow" "$XDG_STATE_HOME/flow"
|
|
|
|
dot="$XDG_DATA_HOME/flow/dotfiles"
|
|
mkdir -p "$dot/_shared/zsh" "$dot/_shared/git"
|
|
echo '# managed zshrc' > "$dot/_shared/zsh/.zshrc"
|
|
echo '[user]' > "$dot/_shared/git/.gitconfig"
|
|
|
|
mkdir -p "$HOME/.zshrc"
|
|
|
|
set +e
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles link --force
|
|
rc=$?
|
|
set -e
|
|
test "$rc" -ne 0
|
|
|
|
test -d "$HOME/.zshrc"
|
|
test ! -e "$HOME/.gitconfig"
|
|
"""
|
|
_assert_ok(_run_in_container(e2e_image, script))
|
|
|
|
|
|
def test_e2e_undo_after_failed_followup_link_restores_last_transaction(e2e_image):
|
|
script = r"""
|
|
set -euo pipefail
|
|
export HOME=/home/flow
|
|
export XDG_DATA_HOME=/tmp/xdg-data
|
|
export XDG_CONFIG_HOME=/tmp/xdg-config
|
|
export XDG_STATE_HOME=/tmp/xdg-state
|
|
mkdir -p "$XDG_DATA_HOME/flow/dotfiles" "$XDG_CONFIG_HOME/flow" "$XDG_STATE_HOME/flow"
|
|
|
|
dot="$XDG_DATA_HOME/flow/dotfiles"
|
|
mkdir -p "$dot/_shared/a" "$dot/_shared/b"
|
|
echo '# aaa' > "$dot/_shared/a/.a"
|
|
echo '# bbb' > "$dot/_shared/b/.b"
|
|
|
|
echo '# pre-a' > "$HOME/.a"
|
|
echo '# pre-b' > "$HOME/.b"
|
|
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles link --force a
|
|
test -L "$HOME/.a"
|
|
|
|
# Turn .b into a directory to force a fatal conflict, while .a stays desired and unchanged.
|
|
rm -f "$HOME/.b"
|
|
mkdir -p "$HOME/.b"
|
|
set +e
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles link --force
|
|
rc=$?
|
|
set -e
|
|
test "$rc" -ne 0
|
|
|
|
PYTHONPATH=/workspace/flow-cli/src python -m flow dotfiles undo
|
|
test -f "$HOME/.a"
|
|
test ! -L "$HOME/.a"
|
|
grep -q '^# pre-a$' "$HOME/.a"
|
|
"""
|
|
_assert_ok(_run_in_container(e2e_image, script))
|