refactor
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
"""Tests for CLI."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_version_flag():
|
||||
@@ -46,3 +44,43 @@ def test_packages_help():
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "install" in result.stdout
|
||||
|
||||
|
||||
def test_invalid_config_is_reported_without_traceback(tmp_path):
|
||||
config_root = tmp_path / "config"
|
||||
(config_root / "flow").mkdir(parents=True)
|
||||
(config_root / "flow" / "config.yaml").write_text(":\n bad\n")
|
||||
|
||||
env = dict(os.environ)
|
||||
env["XDG_CONFIG_HOME"] = str(config_root)
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "completion"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
assert result.returncode == 1
|
||||
assert "Invalid YAML" in result.stderr
|
||||
assert "Traceback" not in result.stderr
|
||||
|
||||
|
||||
def test_local_manifest_is_loaded(tmp_path):
|
||||
config_root = tmp_path / "config"
|
||||
flow_dir = config_root / "flow"
|
||||
flow_dir.mkdir(parents=True)
|
||||
(flow_dir / "manifest.yaml").write_text(
|
||||
"profiles:\n"
|
||||
" demo:\n"
|
||||
" os: linux\n"
|
||||
)
|
||||
|
||||
env = dict(os.environ)
|
||||
env["XDG_CONFIG_HOME"] = str(config_root)
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "setup", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "demo" in result.stdout
|
||||
|
||||
@@ -75,3 +75,59 @@ def test_load_manifest_merges_files(tmp_path):
|
||||
data = load_manifest(tmp_path)
|
||||
assert "packages" in data
|
||||
assert "profiles" in data
|
||||
|
||||
|
||||
def test_load_config_merges_local_and_overlay(tmp_path):
|
||||
local = tmp_path / "local"
|
||||
overlay = tmp_path / "overlay"
|
||||
local.mkdir()
|
||||
overlay.mkdir()
|
||||
(local / "config.yaml").write_text(
|
||||
"repository:\n"
|
||||
" url: git@github.com:user/dots.git\n"
|
||||
"targets:\n"
|
||||
" personal@orb: personal.orb\n"
|
||||
)
|
||||
(overlay / "config.yaml").write_text(
|
||||
"repository:\n"
|
||||
" branch: dev\n"
|
||||
"defaults:\n"
|
||||
" tmux-session: main\n"
|
||||
)
|
||||
|
||||
cfg = load_config(local, overlay)
|
||||
assert cfg.dotfiles_url == "git@github.com:user/dots.git"
|
||||
assert cfg.dotfiles_branch == "dev"
|
||||
assert cfg.tmux_session == "main"
|
||||
assert cfg.targets[0].host == "personal.orb"
|
||||
|
||||
|
||||
def test_load_config_parses_legacy_targets(tmp_path):
|
||||
(tmp_path / "01-targets.yaml").write_text(
|
||||
"targets:\n"
|
||||
" personal: orb personal.orb ~/.ssh/id_personal\n"
|
||||
)
|
||||
(tmp_path / "02-targets.yaml").write_text(
|
||||
"targets:\n"
|
||||
" - namespace: work\n"
|
||||
" platform: ec2\n"
|
||||
" host: work.ec2.internal\n"
|
||||
" identity: ~/.ssh/id_work\n"
|
||||
)
|
||||
cfg = load_config(tmp_path)
|
||||
assert len(cfg.targets) == 2
|
||||
assert cfg.targets[0].platform == "orb"
|
||||
assert cfg.targets[0].identity == "~/.ssh/id_personal"
|
||||
assert cfg.targets[1].namespace == "work"
|
||||
|
||||
|
||||
def test_load_manifest_merges_local_and_overlay(tmp_path):
|
||||
local = tmp_path / "local"
|
||||
overlay = tmp_path / "overlay"
|
||||
local.mkdir()
|
||||
overlay.mkdir()
|
||||
(local / "manifest.yaml").write_text("profiles:\n local:\n os: linux\n")
|
||||
(overlay / "packages.yaml").write_text("packages:\n - name: fd\n type: pkg\n")
|
||||
data = load_manifest(local, overlay)
|
||||
assert "profiles" in data
|
||||
assert "packages" in data
|
||||
|
||||
@@ -33,6 +33,20 @@ class TestParseProfile:
|
||||
profile = parse_profile("test", raw)
|
||||
assert len(profile.ssh_keys) == 1
|
||||
|
||||
def test_ssh_keygen_alias(self):
|
||||
raw = {"ssh-keygen": [{"filename": "id_work", "type": "ed25519"}]}
|
||||
profile = parse_profile("test", raw)
|
||||
assert profile.ssh_keys[0]["path"] == "~/.ssh/id_work"
|
||||
|
||||
def test_requires_alias(self):
|
||||
profile = parse_profile("test", {"requires": ["USER_EMAIL"]})
|
||||
assert profile.env_required == ("USER_EMAIL",)
|
||||
|
||||
def test_post_link_and_dotfiles_profile(self):
|
||||
profile = parse_profile("test", {"dotfiles-profile": "linux-work", "post-link": "echo done"})
|
||||
assert profile.dotfiles_profile == "linux-work"
|
||||
assert profile.post_link == "echo done"
|
||||
|
||||
|
||||
class TestPlanBootstrap:
|
||||
def test_basic_plan(self):
|
||||
@@ -73,6 +87,16 @@ class TestPlanBootstrap:
|
||||
runcmd_actions = [a for a in plan.actions if "custom command" in a.description.lower()]
|
||||
assert len(runcmd_actions) == 1
|
||||
|
||||
def test_post_link_produces_action(self):
|
||||
profile = Profile(
|
||||
name="test", os="linux", arch=None,
|
||||
hostname=None, locale=None, shell=None,
|
||||
ssh_keys=[], runcmd=[], packages=[], env_required=[],
|
||||
post_link="echo done",
|
||||
)
|
||||
plan = plan_bootstrap(profile, {})
|
||||
assert any(action.phase == "post-link" for action in plan.actions)
|
||||
|
||||
def test_ssh_keys_action(self):
|
||||
profile = Profile(
|
||||
name="test", os="linux", arch=None,
|
||||
|
||||
@@ -15,7 +15,7 @@ class TestParseImageRef:
|
||||
def test_simple_name(self):
|
||||
ref = parse_image_ref("devbox")
|
||||
assert ref.registry == "registry.tomastm.com"
|
||||
assert ref.name == "devbox"
|
||||
assert ref.repo == "devbox"
|
||||
assert ref.tag == "latest"
|
||||
|
||||
def test_with_tag(self):
|
||||
@@ -25,7 +25,7 @@ class TestParseImageRef:
|
||||
def test_full_ref(self):
|
||||
ref = parse_image_ref("ghcr.io/user/image:main")
|
||||
assert ref.registry == "ghcr.io"
|
||||
assert ref.name == "user/image"
|
||||
assert ref.repo == "user/image"
|
||||
assert ref.tag == "main"
|
||||
|
||||
def test_full_image_string(self):
|
||||
@@ -35,38 +35,35 @@ class TestParseImageRef:
|
||||
|
||||
class TestContainerName:
|
||||
def test_basic(self):
|
||||
assert container_name("personal", "devbox") == "flow-personal-devbox"
|
||||
assert container_name("devbox") == "dev-devbox"
|
||||
|
||||
|
||||
class TestResolveMounts:
|
||||
def test_projects_mount(self, tmp_path):
|
||||
projects = tmp_path / "projects"
|
||||
projects.mkdir()
|
||||
mounts = resolve_mounts(tmp_path, str(projects))
|
||||
project_mounts = [m for m in mounts if m.target == "/home/user/projects"]
|
||||
mounts = resolve_mounts(tmp_path, project_path=str(projects))
|
||||
project_mounts = [m for m in mounts if m.target == "/workspace"]
|
||||
assert len(project_mounts) == 1
|
||||
|
||||
def test_extra_mounts(self, tmp_path):
|
||||
mounts = resolve_mounts(
|
||||
tmp_path, str(tmp_path),
|
||||
extra_mounts=[{"source": str(tmp_path), "target": "/data"}],
|
||||
)
|
||||
extra = [m for m in mounts if m.target == "/data"]
|
||||
assert len(extra) == 1
|
||||
def test_dotfiles_mount(self, tmp_path):
|
||||
dotfiles = tmp_path / "dotfiles"
|
||||
dotfiles.mkdir()
|
||||
mounts = resolve_mounts(tmp_path, dotfiles_dir=dotfiles)
|
||||
assert any(m.target.endswith("/flow/dotfiles") for m in mounts)
|
||||
|
||||
|
||||
class TestBuildContainerSpec:
|
||||
def test_basic(self):
|
||||
image = ImageRef(registry="reg", name="img", tag="v1")
|
||||
spec = build_container_spec("personal", image, [])
|
||||
assert spec.name == "flow-personal-img"
|
||||
assert spec.env["DF_NAMESPACE"] == "personal"
|
||||
assert spec.env["DF_PLATFORM"] == "container"
|
||||
image = ImageRef(registry="reg", repo="img", tag="v1", label="reg/img")
|
||||
spec = build_container_spec("api", image, [])
|
||||
assert spec.name == "dev-api"
|
||||
assert spec.labels["dev.name"] == "api"
|
||||
|
||||
def test_with_mounts(self):
|
||||
image = ImageRef(registry="reg", name="img", tag="v1")
|
||||
image = ImageRef(registry="reg", repo="img", tag="v1", label="reg/img")
|
||||
mounts = [Mount(source=Path("/a"), target="/b")]
|
||||
spec = build_container_spec("ns", image, mounts)
|
||||
spec = build_container_spec("api", image, mounts)
|
||||
assert len(spec.mounts) == 1
|
||||
|
||||
|
||||
|
||||
@@ -4,16 +4,20 @@ import pytest
|
||||
|
||||
from flow.core.errors import ConfigError, FlowError
|
||||
from flow.domain.packages.catalog import normalize_profile_entry, parse_catalog
|
||||
from flow.domain.packages.planning import plan_install
|
||||
from flow.domain.packages.resolution import (
|
||||
binary_template_context,
|
||||
detect_package_manager,
|
||||
pm_cask_install_command,
|
||||
pm_install_command,
|
||||
pm_update_command,
|
||||
resolve_binary_asset,
|
||||
resolve_download_url,
|
||||
resolve_extract_dir,
|
||||
resolve_source_name,
|
||||
resolve_spec,
|
||||
)
|
||||
from flow.domain.packages.models import PackageDef, ProfilePackageRef
|
||||
from flow.domain.packages.models import InstalledState, PackageDef, ProfilePackageRef
|
||||
|
||||
|
||||
class TestParseCatalog:
|
||||
@@ -77,6 +81,26 @@ class TestResolveSpec:
|
||||
assert result.name == "unknown"
|
||||
assert result.type == "binary"
|
||||
|
||||
def test_profile_object_overrides_catalog(self):
|
||||
catalog = {"docker": PackageDef(
|
||||
name="docker", type="pkg", sources={"apt": "docker-ce"},
|
||||
source=None, version=None, asset_pattern=None,
|
||||
platform_map={}, extract_dir=None, install={},
|
||||
post_install=None, allow_sudo=False,
|
||||
)}
|
||||
ref = ProfilePackageRef(
|
||||
name="docker",
|
||||
type=None,
|
||||
source=None,
|
||||
version=None,
|
||||
asset_pattern=None,
|
||||
post_install="sudo groupadd docker || true",
|
||||
allow_sudo=True,
|
||||
)
|
||||
result = resolve_spec(ref, catalog)
|
||||
assert result.post_install == "sudo groupadd docker || true"
|
||||
assert result.allow_sudo is True
|
||||
|
||||
|
||||
class TestResolveSourceName:
|
||||
def test_with_pm_mapping(self):
|
||||
@@ -125,6 +149,19 @@ class TestResolveBinaryAsset:
|
||||
assert "x64" in result
|
||||
assert "linux" in result
|
||||
|
||||
def test_double_brace_pattern_uses_platform_map_context(self):
|
||||
pkg = PackageDef(
|
||||
name="nvim", type="binary", sources={},
|
||||
source="github:neovim/neovim",
|
||||
version="0.10.4",
|
||||
asset_pattern="nvim-{{os}}-{{arch}}.tar.gz",
|
||||
platform_map={"linux-x64": {"os": "linux", "arch": "x86_64"}},
|
||||
extract_dir="nvim-{{os}}64", install={},
|
||||
post_install=None, allow_sudo=False,
|
||||
)
|
||||
assert resolve_binary_asset(pkg, "linux-x64") == "nvim-linux-x86_64.tar.gz"
|
||||
assert resolve_extract_dir(pkg, "linux-x64") == "nvim-linux64"
|
||||
|
||||
|
||||
class TestResolveDownloadUrl:
|
||||
def test_github_shorthand_with_version(self):
|
||||
@@ -140,6 +177,18 @@ class TestResolveDownloadUrl:
|
||||
assert "github.com/neovim/neovim" in url
|
||||
assert "v0.10.4" in url
|
||||
|
||||
def test_github_shorthand_prefixes_v(self):
|
||||
pkg = PackageDef(
|
||||
name="nvim", type="binary", sources={},
|
||||
source="github:neovim/neovim",
|
||||
version="0.10.4",
|
||||
asset_pattern=None, platform_map={},
|
||||
extract_dir=None, install={},
|
||||
post_install=None, allow_sudo=False,
|
||||
)
|
||||
url = resolve_download_url(pkg, "nvim.tar.gz", "linux-x64")
|
||||
assert "/download/v0.10.4/" in url
|
||||
|
||||
def test_github_latest(self):
|
||||
pkg = PackageDef(
|
||||
name="nvim", type="binary", sources={},
|
||||
@@ -181,7 +230,24 @@ class TestPmCommands:
|
||||
cmd = pm_install_command("apt", ["fd-find"])
|
||||
assert "apt-get install" in cmd
|
||||
|
||||
def test_brew_cask_install(self):
|
||||
cmd = pm_cask_install_command("brew", ["wezterm"])
|
||||
assert "--cask" in cmd
|
||||
assert "wezterm" in cmd
|
||||
|
||||
def test_detect_package_manager_returns_something(self):
|
||||
# Just verify it doesn't error
|
||||
result = detect_package_manager()
|
||||
assert result is None or result in ("apt", "dnf", "brew")
|
||||
|
||||
|
||||
class TestPlanning:
|
||||
def test_cask_package_is_planned(self):
|
||||
pkg = PackageDef(
|
||||
name="wezterm", type="cask", sources={"brew": "wezterm"},
|
||||
source=None, version=None, asset_pattern=None,
|
||||
platform_map={}, extract_dir=None, install={},
|
||||
post_install=None, allow_sudo=False,
|
||||
)
|
||||
plan = plan_install([pkg], InstalledState(), "macos-arm64", "brew")
|
||||
assert plan.install_ops[0].method == "cask"
|
||||
|
||||
@@ -16,7 +16,14 @@ from flow.domain.remote.resolution import (
|
||||
|
||||
class TestParseTarget:
|
||||
def test_valid_spec(self):
|
||||
ns, plat = parse_target("personal@orb")
|
||||
user, ns, plat = parse_target("personal@orb")
|
||||
assert user is None
|
||||
assert ns == "personal"
|
||||
assert plat == "orb"
|
||||
|
||||
def test_valid_spec_with_user(self):
|
||||
user, ns, plat = parse_target("alice@personal@orb")
|
||||
assert user == "alice"
|
||||
assert ns == "personal"
|
||||
assert plat == "orb"
|
||||
|
||||
@@ -32,33 +39,40 @@ class TestParseTarget:
|
||||
class TestResolveTarget:
|
||||
def test_found(self):
|
||||
targets = [TargetConfig(namespace="personal", platform="orb", host="personal.orb")]
|
||||
result = resolve_target("personal@orb", targets)
|
||||
result = resolve_target("personal@orb", targets, default_user="tomas")
|
||||
assert result.host == "personal.orb"
|
||||
assert result.label == "personal@orb"
|
||||
assert result.user == "tomas"
|
||||
|
||||
def test_not_found(self):
|
||||
with pytest.raises(FlowError, match="Unknown target"):
|
||||
resolve_target("missing@host", [])
|
||||
resolve_target("missing@host", [], default_user="tomas")
|
||||
|
||||
def test_falls_back_to_host_template(self):
|
||||
result = resolve_target("personal@orb", [], default_user="tomas")
|
||||
assert result.host == "personal.orb"
|
||||
|
||||
|
||||
class TestBuildSSHCommand:
|
||||
def test_basic(self):
|
||||
target = Target(namespace="personal", platform="orb", host="personal.orb")
|
||||
target = Target(namespace="personal", platform="orb", host="personal.orb", user="tomas")
|
||||
cmd = build_ssh_command(target)
|
||||
assert "ssh" in cmd.argv
|
||||
assert "personal.orb" in cmd.argv
|
||||
assert cmd.destination == "tomas@personal.orb"
|
||||
assert cmd.env["DF_NAMESPACE"] == "personal"
|
||||
assert "tmux" in cmd.argv
|
||||
|
||||
def test_with_identity(self):
|
||||
target = Target(namespace="work", platform="ec2", host="work.ec2", identity="~/.ssh/id_work")
|
||||
target = Target(namespace="work", platform="ec2", host="work.ec2", identity="~/.ssh/id_work", user="tomas")
|
||||
cmd = build_ssh_command(target)
|
||||
assert "-i" in cmd.argv
|
||||
assert "~/.ssh/id_work" in cmd.argv
|
||||
|
||||
def test_with_remote_command(self):
|
||||
target = Target(namespace="p", platform="o", host="h")
|
||||
cmd = build_ssh_command(target, remote_command="ls -la")
|
||||
assert cmd.argv[-1] == "ls -la"
|
||||
def test_without_tmux(self):
|
||||
target = Target(namespace="p", platform="o", host="h", user="tomas")
|
||||
cmd = build_ssh_command(target, no_tmux=True)
|
||||
assert "tmux" not in cmd.argv
|
||||
assert cmd.destination == "tomas@h"
|
||||
|
||||
|
||||
class TestListTargets:
|
||||
@@ -73,7 +87,6 @@ class TestListTargets:
|
||||
|
||||
|
||||
class TestTerminfoFix:
|
||||
def test_returns_commands(self):
|
||||
cmds = terminfo_fix_command()
|
||||
assert len(cmds) == 2
|
||||
assert "infocmp" in cmds[0]
|
||||
def test_returns_command(self):
|
||||
cmd = terminfo_fix_command()
|
||||
assert "infocmp" in cmd
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Tests for BootstrapService."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from flow.core.config import AppConfig, FlowContext
|
||||
@@ -65,3 +63,61 @@ class TestBootstrapService:
|
||||
svc = BootstrapService(ctx)
|
||||
svc.list_profiles()
|
||||
assert "No profiles" in capsys.readouterr().out
|
||||
|
||||
def test_run_preserves_profile_package_overrides(self, monkeypatch):
|
||||
captured = {}
|
||||
|
||||
class StubPackageService:
|
||||
def __init__(self, ctx):
|
||||
pass
|
||||
|
||||
def install(self, packages, *, dry_run=False):
|
||||
captured["packages"] = packages
|
||||
|
||||
monkeypatch.setattr("flow.services.packages.PackageService", StubPackageService)
|
||||
monkeypatch.setattr("flow.services.dotfiles.DotfilesService.link", lambda self, profile=None: None)
|
||||
|
||||
manifest = {
|
||||
"profiles": {
|
||||
"linux-auto": {
|
||||
"os": "linux",
|
||||
"packages": [{
|
||||
"name": "docker",
|
||||
"allow-sudo": True,
|
||||
"post-install": "sudo groupadd docker || true",
|
||||
}],
|
||||
},
|
||||
},
|
||||
"packages": [{"name": "docker", "type": "pkg", "sources": {"apt": "docker-ce"}}],
|
||||
}
|
||||
ctx = _make_ctx(manifest)
|
||||
BootstrapService(ctx).run("linux-auto")
|
||||
|
||||
assert captured["packages"][0].allow_sudo is True
|
||||
assert captured["packages"][0].post_install == "sudo groupadd docker || true"
|
||||
|
||||
def test_run_uses_dotfiles_profile_override(self, monkeypatch):
|
||||
captured = {}
|
||||
|
||||
monkeypatch.setattr("flow.services.packages.PackageService.install", lambda self, packages, dry_run=False: None)
|
||||
|
||||
class StubDotfilesService:
|
||||
def __init__(self, ctx):
|
||||
pass
|
||||
|
||||
def link(self, profile=None):
|
||||
captured["profile"] = profile
|
||||
|
||||
monkeypatch.setattr("flow.services.dotfiles.DotfilesService", StubDotfilesService)
|
||||
|
||||
manifest = {
|
||||
"profiles": {
|
||||
"linux-auto": {
|
||||
"os": "linux",
|
||||
"dotfiles-profile": "linux-work",
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx = _make_ctx(manifest)
|
||||
BootstrapService(ctx).run("linux-auto")
|
||||
assert captured["profile"] == "linux-work"
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
"""Tests for ContainerService."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
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, FileSystem, SystemRuntime
|
||||
from flow.core.runtime import CommandRunner, SystemRuntime
|
||||
from flow.core import paths
|
||||
from flow.services.containers import ContainerService
|
||||
|
||||
@@ -19,6 +17,11 @@ class FakeRunner(CommandRunner):
|
||||
|
||||
def run(self, argv, *, cwd=None, env=None, capture_output=True, check=False, timeout=None):
|
||||
self.calls.append(("run", list(argv)))
|
||||
command = list(argv)
|
||||
if command[:4] == ["docker", "container", "ls", "-a"]:
|
||||
return subprocess.CompletedProcess(argv, 0, stdout="dev-api\n", stderr="")
|
||||
if command[:3] == ["docker", "container", "ls"]:
|
||||
return subprocess.CompletedProcess(argv, 0, stdout="dev-api\n", stderr="")
|
||||
return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
|
||||
|
||||
def run_shell(self, command, *, cwd=None, env=None, capture_output=True, check=False, timeout=None):
|
||||
@@ -43,31 +46,35 @@ class TestContainerService:
|
||||
def test_create_dry_run(self, tmp_path, capsys, monkeypatch):
|
||||
monkeypatch.setattr(paths, "HOME", tmp_path)
|
||||
monkeypatch.setattr(paths, "DOTFILES_DIR", tmp_path / "dotfiles")
|
||||
monkeypatch.setattr("flow.services.containers.runtime", lambda: "docker")
|
||||
ctx = _make_ctx(tmp_path)
|
||||
svc = ContainerService(ctx)
|
||||
svc.create("devbox", "personal", dry_run=True)
|
||||
svc.create("api", "tm0/node", dry_run=True)
|
||||
output = capsys.readouterr().out
|
||||
assert "devbox" in output
|
||||
assert "dev-api" in output
|
||||
|
||||
def test_list_no_docker(self, tmp_path, capsys):
|
||||
def test_list_no_containers(self, tmp_path, capsys, monkeypatch):
|
||||
runner = FakeRunner()
|
||||
monkeypatch.setattr("flow.services.containers.runtime", lambda: "docker")
|
||||
runner.run = lambda argv, **kwargs: subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
|
||||
ctx = _make_ctx(tmp_path, runner=runner)
|
||||
svc = ContainerService(ctx)
|
||||
svc.list()
|
||||
# FakeRunner returns empty stdout -> "No flow containers"
|
||||
output = capsys.readouterr().out
|
||||
assert "No flow containers" in output
|
||||
|
||||
def test_stop_calls_docker(self, tmp_path):
|
||||
def test_stop_calls_docker(self, tmp_path, monkeypatch):
|
||||
runner = FakeRunner()
|
||||
monkeypatch.setattr("flow.services.containers.runtime", lambda: "docker")
|
||||
ctx = _make_ctx(tmp_path, runner=runner)
|
||||
svc = ContainerService(ctx)
|
||||
svc.stop("flow-personal-devbox")
|
||||
svc.stop("api")
|
||||
assert any("docker" in str(c) and "stop" in str(c) for c in runner.calls)
|
||||
|
||||
def test_remove_calls_docker(self, tmp_path):
|
||||
def test_remove_calls_docker(self, tmp_path, monkeypatch):
|
||||
runner = FakeRunner()
|
||||
monkeypatch.setattr("flow.services.containers.runtime", lambda: "docker")
|
||||
ctx = _make_ctx(tmp_path, runner=runner)
|
||||
svc = ContainerService(ctx)
|
||||
svc.remove("flow-personal-devbox")
|
||||
svc.remove("api")
|
||||
assert any("docker" in str(c) and "rm" in str(c) for c in runner.calls)
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
"""Tests for DotfilesService."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
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 SystemRuntime
|
||||
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(
|
||||
@@ -170,3 +180,59 @@ class TestDotfilesServiceLink:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for PackageService."""
|
||||
|
||||
import io
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -63,3 +65,68 @@ class TestPackageService:
|
||||
svc = PackageService(ctx)
|
||||
with pytest.raises(FlowError, match="Specify"):
|
||||
svc.install()
|
||||
|
||||
def test_list_all_known_packages(self, tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setattr(paths, "INSTALLED_STATE", tmp_path / "installed.json")
|
||||
manifest = {"packages": [{"name": "fd", "type": "pkg"}]}
|
||||
ctx = _make_ctx(tmp_path, manifest)
|
||||
svc = PackageService(ctx)
|
||||
svc.list_packages(show_all=True)
|
||||
assert "fd" in capsys.readouterr().out
|
||||
|
||||
def test_install_binary_honors_declared_install_map(self, tmp_path, monkeypatch):
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HOME", str(home))
|
||||
monkeypatch.setattr(paths, "DATA_DIR", tmp_path / "data")
|
||||
monkeypatch.setattr(paths, "INSTALLED_STATE", tmp_path / "installed.json")
|
||||
|
||||
archive = io.BytesIO()
|
||||
with tarfile.open(fileobj=archive, mode="w:gz") as tar:
|
||||
files = {
|
||||
"nvim-linux64/bin/nvim": b"#!/bin/sh\n",
|
||||
"nvim-linux64/share/nvim/runtime.txt": b"runtime\n",
|
||||
"nvim-linux64/share/man/man1/nvim.1": b"manpage\n",
|
||||
}
|
||||
for name, content in files.items():
|
||||
info = tarfile.TarInfo(name=name)
|
||||
info.size = len(content)
|
||||
tar.addfile(info, io.BytesIO(content))
|
||||
archive_bytes = archive.getvalue()
|
||||
|
||||
class FakeResponse:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return archive_bytes
|
||||
|
||||
monkeypatch.setattr("flow.services.packages.urllib.request.urlopen", lambda *args, **kwargs: FakeResponse())
|
||||
|
||||
manifest = {
|
||||
"packages": [{
|
||||
"name": "neovim",
|
||||
"type": "binary",
|
||||
"source": "github:neovim/neovim",
|
||||
"version": "0.10.4",
|
||||
"asset-pattern": "nvim-{{os}}-{{arch}}.tar.gz",
|
||||
"platform-map": {"linux-x64": {"os": "linux", "arch": "x64"}},
|
||||
"extract-dir": "nvim-{{os}}64",
|
||||
"install": {
|
||||
"bin": ["bin/nvim"],
|
||||
"share": ["share/nvim"],
|
||||
"man": ["share/man/man1/nvim.1"],
|
||||
},
|
||||
}],
|
||||
}
|
||||
ctx = _make_ctx(tmp_path, manifest)
|
||||
svc = PackageService(ctx)
|
||||
packages = svc.resolve_install_packages(package_names=["neovim"])
|
||||
svc.install(packages)
|
||||
|
||||
assert (home / ".local" / "bin" / "nvim").exists()
|
||||
assert (home / ".local" / "share" / "nvim" / "runtime.txt").exists()
|
||||
assert (home / ".local" / "share" / "man" / "man1" / "nvim.1").exists()
|
||||
|
||||
@@ -29,6 +29,8 @@ class TestRemoteService:
|
||||
output = capsys.readouterr().out
|
||||
assert "personal@orb" in output
|
||||
assert "ssh" in output
|
||||
assert "tmux" in output
|
||||
assert "DF_NAMESPACE=personal" in output
|
||||
|
||||
def test_enter_unknown_target(self):
|
||||
ctx = _make_ctx()
|
||||
|
||||
Reference in New Issue
Block a user