fd18b547e5
Adds the `user.rust` UPM to workshop-engine for managing Rust toolchains, components, cross-compilation targets, and cargo operations: - `rustup default <toolchain>` / `rustup component add` / `rustup target add` - `cargo fetch` for dependency pre-fetch with optional `--locked` - `cargo install` for global crate tools with registry support Includes 275-line test suite (`tests/upm/test_rust.py`) covering the full lifecycle with optional network tests.
203 lines
8.9 KiB
Python
203 lines
8.9 KiB
Python
import os
|
|
import json
|
|
import pytest
|
|
|
|
from probes import user, file, package
|
|
|
|
FIXTURES = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures", "prebake")
|
|
|
|
@pytest.mark.prebake
|
|
class TestPrebakeConfigValidation:
|
|
|
|
def test_invalid_version_fails(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "invalid_version.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert not result["success"], f"Expected prebake to fail with invalid version"
|
|
assert "version" in result["stderr"].lower() or "Version" in result["stderr"]
|
|
|
|
def test_missing_required_field_fails(self, run_baker, work_dir):
|
|
missing_env = os.path.join(work_dir, "missing_env.yml")
|
|
with open(missing_env, "w") as f:
|
|
f.write('version: "1.0"\n')
|
|
result = run_baker("prebake", missing_env, cwd=work_dir, timeout=60)
|
|
assert not result["success"]
|
|
|
|
def test_minimal_config_succeeds(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "minimal.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"], f"Minimal prebake failed: {result['stderr']}"
|
|
|
|
@pytest.mark.prebake
|
|
class TestPrebakeBootstrap:
|
|
|
|
def test_bootstrap_creates_vulcan_user(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "bootstrap_only.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"], f"prebake failed: {result['stderr']}"
|
|
assert user.user_exists("vulcan")
|
|
|
|
def test_bootstrap_creates_workspace(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "bootstrap_only.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"]
|
|
assert file.dir_exists("/home/vulcan/workspace")
|
|
|
|
def test_bootstrap_creates_symlink(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "bootstrap_only.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"]
|
|
assert os.path.islink("/workspace")
|
|
|
|
def test_bootstrap_generates_bootstrap_script(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "bootstrap_only.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"]
|
|
assert file.file_exists("/bootstrap.sh")
|
|
|
|
def test_bootstrap_script_permissions(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "bootstrap_only.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"]
|
|
mode = file.file_permissions("/bootstrap.sh")
|
|
assert mode & 0o111, f"/bootstrap.sh not executable, mode: {oct(mode)}"
|
|
|
|
def test_bootstrap_script_contains_user_creation(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "bootstrap_only.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"]
|
|
assert file.file_exists("/bootstrap.sh")
|
|
assert file.file_contains("/bootstrap.sh", "useradd")
|
|
|
|
def test_bootstrap_script_contains_workspace_setup(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "bootstrap_only.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"]
|
|
assert file.file_contains("/bootstrap.sh", "mkdir -p /home/vulcan/workspace")
|
|
|
|
def test_bootstrap_with_pacman_creates_sudoers(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "with_pacman.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=120)
|
|
assert result["success"], f"prebake failed: {result['stderr']}"
|
|
assert file.file_exists("/etc/sudoers.d/workshop")
|
|
|
|
def test_custom_bootstrap_executes(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "with_custom_bootstrap.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"], f"Custom bootstrap failed: {result['stderr']}"
|
|
assert file.dir_exists("/custom-workspace")
|
|
|
|
@pytest.mark.prebake
|
|
class TestPrebakeDryRun:
|
|
|
|
def test_dry_run_does_not_create_user(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "bootstrap_only.yml")
|
|
env = {"HBW_DRY_RUN": "true"}
|
|
result = run_baker("prebake", config, cwd=work_dir, env=env, timeout=60)
|
|
assert result["success"], f"Dry-run failed: {result['stderr']}"
|
|
|
|
def test_dry_run_logs_script_content(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "bootstrap_only.yml")
|
|
env = {"HBW_DRY_RUN": "true", "RUST_LOG": "info"}
|
|
result = run_baker("prebake", config, cwd=work_dir, env=env, timeout=60)
|
|
assert result["success"]
|
|
combined = result["stdout"] + result["stderr"]
|
|
assert "DRY_RUN" in combined or "dry_run" in combined.lower()
|
|
|
|
@pytest.mark.prebake
|
|
class TestPrebakeHooks:
|
|
|
|
#@pytest.mark.xfail(reason="Hook execution fails with Permission denied in container environment", strict=False)
|
|
def test_early_hook_executes(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "with_hooks.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"], f"prebake with hooks failed: {result['stderr']}"
|
|
combined = result["stdout"] + result["stderr"]
|
|
assert "early hook executed" in combined
|
|
|
|
#@pytest.mark.xfail(reason="Hook execution fails with Permission denied in container environment", strict=False)
|
|
def test_late_hook_executes(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "with_hooks.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"], f"prebake with hooks failed: {result['stderr']}"
|
|
combined = result["stdout"] + result["stderr"]
|
|
assert "late hook executed" in combined
|
|
|
|
#@pytest.mark.xfail(reason="Hook execution fails with Permission denied in container environment", strict=False)
|
|
def test_hooks_run_in_order(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "with_hooks.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"]
|
|
combined = result["stdout"] + result["stderr"]
|
|
early_pos = combined.find("early hook executed")
|
|
late_pos = combined.find("late hook executed")
|
|
assert early_pos != -1 and late_pos != -1
|
|
assert early_pos < late_pos, "Early hook should execute before late hook"
|
|
|
|
def test_empty_hooks_succeed(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "minimal.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"], f"prebake with no hooks failed: {result['stderr']}"
|
|
|
|
@pytest.mark.prebake
|
|
class TestPrebakeDepsSystem:
|
|
|
|
def test_pacman_packages_installed(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "with_pacman.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=120)
|
|
assert result["success"], f"prebake failed: {result['stderr']}"
|
|
assert package.binary_exists("/usr/bin/curl")
|
|
assert package.binary_exists("/usr/bin/wget")
|
|
|
|
def test_pacman_mirror_change(self, run_baker, work_dir):
|
|
mirror_config = os.path.join(work_dir, "mirror.yml")
|
|
with open(mirror_config, "w") as f:
|
|
f.write('''
|
|
version: "1.0"
|
|
environment:
|
|
builder: "baremetal"
|
|
bootstrap:
|
|
user: "vulcan"
|
|
dependencies:
|
|
system:
|
|
pacman:
|
|
packages: ["which"]
|
|
mirror: "https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch"
|
|
''')
|
|
result = run_baker("prebake", mirror_config, cwd=work_dir, timeout=120)
|
|
assert result["success"], f"prebake with mirror failed: {result['stderr']}"
|
|
|
|
@pytest.mark.prebake
|
|
class TestPrebakeEnvVars:
|
|
|
|
def test_prebake_envvars_injected(self, run_baker, work_dir):
|
|
hook_config = os.path.join(work_dir, "envvar_hook.yml")
|
|
with open(hook_config, "w") as f:
|
|
f.write('''
|
|
version: "1.0"
|
|
environment:
|
|
builder: "baremetal"
|
|
bootstrap:
|
|
user: "vulcan"
|
|
envvars:
|
|
prebake:
|
|
TEST_INJECTED: "injected_value"
|
|
hooks:
|
|
early:
|
|
- name: "check-env"
|
|
command: "echo $TEST_INJECTED"
|
|
''')
|
|
result = run_baker("prebake", hook_config, cwd=work_dir, timeout=60)
|
|
assert result["success"], f"prebake with envvars failed: {result['stderr']}"
|
|
combined = result["stdout"] + result["stderr"]
|
|
assert "injected_value" in combined
|
|
|
|
@pytest.mark.prebake
|
|
class TestPrebakeSecurity:
|
|
|
|
#@pytest.mark.xfail(reason="Privilege drop may fail in container without proper setup", strict=False)
|
|
def test_privilege_drop_after_bootstrap(self, run_baker, work_dir):
|
|
config = os.path.join(FIXTURES, "with_security.yml")
|
|
result = run_baker("prebake", config, cwd=work_dir, timeout=60)
|
|
assert result["success"], f"prebake with security failed: {result['stderr']}"
|
|
|