diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..47c15da --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +target/ +.git/ +.claude/ diff --git a/src/deps.rs b/src/deps.rs new file mode 100644 index 0000000..1c775fe --- /dev/null +++ b/src/deps.rs @@ -0,0 +1,425 @@ +use anyhow::{Context, Result}; +use std::io::{self, Write}; +use std::process::{Command, Stdio}; + +/// Required external tool dependencies. +#[derive(Debug, Clone, PartialEq)] +pub enum Dep { + Fzf, + Tmux, +} + +/// Supported Linux package managers. +#[derive(Debug, Clone, PartialEq)] +pub enum PackageManager { + Apt, + Pacman, + Dnf, + Yum, + Zypper, + Emerge, + Xbps, + Apk, +} + +/// Injectable binary availability checker — enables unit testing without hitting the real system. +pub trait BinaryChecker { + fn is_available(&self, name: &str) -> bool; +} + +/// Production implementation: delegates to the system `which` command. +pub struct SystemBinaryChecker; + +impl BinaryChecker for SystemBinaryChecker { + fn is_available(&self, name: &str) -> bool { + Command::new("which") + .arg(name) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) + } +} + +impl Dep { + pub fn all() -> Vec { + vec![Self::Fzf, Self::Tmux] + } + + pub fn binary_name(&self) -> &str { + match self { + Self::Fzf => "fzf", + Self::Tmux => "tmux", + } + } + + pub fn package_name(&self) -> &str { + match self { + Self::Fzf => "fzf", + Self::Tmux => "tmux", + } + } +} + +impl PackageManager { + /// Ordered list for detection — more specific managers first. + pub fn all_ordered() -> Vec { + vec![ + Self::Apt, + Self::Pacman, + Self::Dnf, + Self::Yum, + Self::Zypper, + Self::Emerge, + Self::Xbps, + Self::Apk, + ] + } + + /// Binary used to detect whether this package manager is installed. + pub fn detection_binary(&self) -> &str { + match self { + Self::Apt => "apt", + Self::Pacman => "pacman", + Self::Dnf => "dnf", + Self::Yum => "yum", + Self::Zypper => "zypper", + Self::Emerge => "emerge", + Self::Xbps => "xbps-install", + Self::Apk => "apk", + } + } + + pub fn display_name(&self) -> &str { + match self { + Self::Apt => "apt (Debian/Ubuntu)", + Self::Pacman => "pacman (Arch Linux)", + Self::Dnf => "dnf (Fedora)", + Self::Yum => "yum (RHEL/CentOS)", + Self::Zypper => "zypper (openSUSE)", + Self::Emerge => "emerge (Gentoo)", + Self::Xbps => "xbps-install (Void Linux)", + Self::Apk => "apk (Alpine Linux)", + } + } + + /// Builds the full install command (including `sudo`) for the given packages. + pub fn install_command(&self, packages: &[&str]) -> Vec { + let mut cmd = vec!["sudo".to_string()]; + match self { + Self::Apt => cmd.extend(["apt", "install", "-y"].map(String::from)), + Self::Pacman => cmd.extend(["pacman", "-S", "--noconfirm"].map(String::from)), + Self::Dnf => cmd.extend(["dnf", "install", "-y"].map(String::from)), + Self::Yum => cmd.extend(["yum", "install", "-y"].map(String::from)), + Self::Zypper => cmd.extend(["zypper", "install", "-y"].map(String::from)), + Self::Emerge => cmd.extend(["emerge"].map(String::from)), + Self::Xbps => cmd.extend(["xbps-install", "-y"].map(String::from)), + Self::Apk => cmd.extend(["apk", "add"].map(String::from)), + } + cmd.extend(packages.iter().map(|&s| s.to_string())); + cmd + } +} + +/// Returns the required deps that are not currently installed. +pub fn check_missing(checker: &C) -> Vec { + Dep::all() + .into_iter() + .filter(|dep| !checker.is_available(dep.binary_name())) + .collect() +} + +/// Returns the first supported package manager found on the system. +pub fn detect_package_manager(checker: &C) -> Option { + PackageManager::all_ordered() + .into_iter() + .find(|pm| checker.is_available(pm.detection_binary())) +} + +/// Checks for missing dependencies, informs the user, and offers to install them. +/// +/// Returns `Ok(())` if all deps are available (or successfully installed). +pub fn ensure_dependencies() -> Result<()> { + let checker = SystemBinaryChecker; + let missing = check_missing(&checker); + + if missing.is_empty() { + return Ok(()); + } + + eprintln!("The following required tools are not installed:"); + for dep in &missing { + eprintln!(" ✗ {}", dep.binary_name()); + } + eprintln!(); + + let pm = detect_package_manager(&checker).ok_or_else(|| { + anyhow::anyhow!( + "No supported package manager found. Please install {} manually.", + missing + .iter() + .map(|d| d.binary_name()) + .collect::>() + .join(" and ") + ) + })?; + + let packages: Vec<&str> = missing.iter().map(|d| d.package_name()).collect(); + let cmd = pm.install_command(&packages); + + eprintln!("Detected package manager: {}", pm.display_name()); + eprintln!("Install command: {}", cmd.join(" ")); + eprint!("\nProceed with installation? [Y/n] "); + io::stdout().flush().ok(); + + let mut answer = String::new(); + io::stdin() + .read_line(&mut answer) + .context("Failed to read user input")?; + + let answer = answer.trim().to_lowercase(); + if answer == "n" || answer == "no" { + anyhow::bail!( + "Installation cancelled. Please install {} manually before running tmuxido.", + missing + .iter() + .map(|d| d.binary_name()) + .collect::>() + .join(" and ") + ); + } + + let (program, args) = cmd + .split_first() + .expect("install_command always returns at least one element"); + + let status = Command::new(program) + .args(args) + .status() + .with_context(|| format!("Failed to run: {}", cmd.join(" ")))?; + + if !status.success() { + anyhow::bail!( + "Installation failed. Please install {} manually.", + missing + .iter() + .map(|d| d.binary_name()) + .collect::>() + .join(" and ") + ); + } + + eprintln!("Installation complete!"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + struct MockChecker { + available: Vec, + } + + impl MockChecker { + fn with(available: &[&str]) -> Self { + Self { + available: available.iter().map(|s| s.to_string()).collect(), + } + } + } + + impl BinaryChecker for MockChecker { + fn is_available(&self, name: &str) -> bool { + self.available.iter().any(|s| s == name) + } + } + + // --- Dep --- + + #[test] + fn should_return_fzf_binary_name() { + assert_eq!(Dep::Fzf.binary_name(), "fzf"); + } + + #[test] + fn should_return_tmux_binary_name() { + assert_eq!(Dep::Tmux.binary_name(), "tmux"); + } + + #[test] + fn should_include_fzf_and_tmux_in_all_deps() { + let deps = Dep::all(); + assert!(deps.contains(&Dep::Fzf)); + assert!(deps.contains(&Dep::Tmux)); + } + + #[test] + fn should_return_same_package_name_as_binary_for_fzf() { + assert_eq!(Dep::Fzf.package_name(), "fzf"); + } + + #[test] + fn should_return_same_package_name_as_binary_for_tmux() { + assert_eq!(Dep::Tmux.package_name(), "tmux"); + } + + // --- check_missing --- + + #[test] + fn should_return_empty_when_all_deps_present() { + let checker = MockChecker::with(&["fzf", "tmux"]); + assert!(check_missing(&checker).is_empty()); + } + + #[test] + fn should_detect_fzf_as_missing_when_only_tmux_present() { + let checker = MockChecker::with(&["tmux"]); + let missing = check_missing(&checker); + assert_eq!(missing, vec![Dep::Fzf]); + } + + #[test] + fn should_detect_tmux_as_missing_when_only_fzf_present() { + let checker = MockChecker::with(&["fzf"]); + let missing = check_missing(&checker); + assert_eq!(missing, vec![Dep::Tmux]); + } + + #[test] + fn should_detect_both_missing_when_none_present() { + let checker = MockChecker::with(&[]); + let missing = check_missing(&checker); + assert_eq!(missing.len(), 2); + assert!(missing.contains(&Dep::Fzf)); + assert!(missing.contains(&Dep::Tmux)); + } + + // --- detect_package_manager --- + + #[test] + fn should_detect_apt_when_available() { + let checker = MockChecker::with(&["apt"]); + assert_eq!(detect_package_manager(&checker), Some(PackageManager::Apt)); + } + + #[test] + fn should_detect_pacman_when_available() { + let checker = MockChecker::with(&["pacman"]); + assert_eq!( + detect_package_manager(&checker), + Some(PackageManager::Pacman) + ); + } + + #[test] + fn should_detect_dnf_when_available() { + let checker = MockChecker::with(&["dnf"]); + assert_eq!(detect_package_manager(&checker), Some(PackageManager::Dnf)); + } + + #[test] + fn should_detect_xbps_when_xbps_install_available() { + let checker = MockChecker::with(&["xbps-install"]); + assert_eq!(detect_package_manager(&checker), Some(PackageManager::Xbps)); + } + + #[test] + fn should_detect_apk_when_available() { + let checker = MockChecker::with(&["apk"]); + assert_eq!(detect_package_manager(&checker), Some(PackageManager::Apk)); + } + + #[test] + fn should_return_none_when_no_pm_detected() { + let checker = MockChecker::with(&["ls", "sh"]); + assert_eq!(detect_package_manager(&checker), None); + } + + #[test] + fn should_prefer_apt_over_pacman_when_both_available() { + let checker = MockChecker::with(&["apt", "pacman"]); + assert_eq!(detect_package_manager(&checker), Some(PackageManager::Apt)); + } + + // --- PackageManager::install_command --- + + #[test] + fn should_build_apt_install_command() { + let cmd = PackageManager::Apt.install_command(&["fzf", "tmux"]); + assert_eq!(cmd, vec!["sudo", "apt", "install", "-y", "fzf", "tmux"]); + } + + #[test] + fn should_build_pacman_install_command() { + let cmd = PackageManager::Pacman.install_command(&["fzf", "tmux"]); + assert_eq!( + cmd, + vec!["sudo", "pacman", "-S", "--noconfirm", "fzf", "tmux"] + ); + } + + #[test] + fn should_build_dnf_install_command() { + let cmd = PackageManager::Dnf.install_command(&["fzf"]); + assert_eq!(cmd, vec!["sudo", "dnf", "install", "-y", "fzf"]); + } + + #[test] + fn should_build_yum_install_command() { + let cmd = PackageManager::Yum.install_command(&["tmux"]); + assert_eq!(cmd, vec!["sudo", "yum", "install", "-y", "tmux"]); + } + + #[test] + fn should_build_zypper_install_command() { + let cmd = PackageManager::Zypper.install_command(&["fzf", "tmux"]); + assert_eq!(cmd, vec!["sudo", "zypper", "install", "-y", "fzf", "tmux"]); + } + + #[test] + fn should_build_emerge_install_command() { + let cmd = PackageManager::Emerge.install_command(&["fzf"]); + assert_eq!(cmd, vec!["sudo", "emerge", "fzf"]); + } + + #[test] + fn should_build_xbps_install_command() { + let cmd = PackageManager::Xbps.install_command(&["tmux"]); + assert_eq!(cmd, vec!["sudo", "xbps-install", "-y", "tmux"]); + } + + #[test] + fn should_build_apk_install_command() { + let cmd = PackageManager::Apk.install_command(&["fzf", "tmux"]); + assert_eq!(cmd, vec!["sudo", "apk", "add", "fzf", "tmux"]); + } + + #[test] + fn should_build_command_for_single_package() { + let cmd = PackageManager::Apt.install_command(&["fzf"]); + assert_eq!(cmd, vec!["sudo", "apt", "install", "-y", "fzf"]); + } + + #[test] + fn should_include_sudo_for_all_package_managers() { + for pm in PackageManager::all_ordered() { + let cmd = pm.install_command(&["fzf"]); + assert_eq!( + cmd.first().map(String::as_str), + Some("sudo"), + "{} install command should start with sudo", + pm.display_name() + ); + } + } + + #[test] + fn should_include_all_packages_in_command() { + let cmd = PackageManager::Apt.install_command(&["fzf", "tmux", "git"]); + assert!(cmd.contains(&"fzf".to_string())); + assert!(cmd.contains(&"tmux".to_string())); + assert!(cmd.contains(&"git".to_string())); + } +} diff --git a/src/lib.rs b/src/lib.rs index c63048b..368812a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod cache; pub mod config; +pub mod deps; pub mod session; use anyhow::Result; diff --git a/src/main.rs b/src/main.rs index f629a88..aab60aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; use tmuxido::config::Config; +use tmuxido::deps::ensure_dependencies; use tmuxido::{get_projects, launch_tmux_session, show_cache_status}; #[derive(Parser, Debug)] @@ -28,6 +29,9 @@ struct Args { fn main() -> Result<()> { let args = Args::parse(); + // Check that fzf and tmux are installed; offer to install if missing + ensure_dependencies()?; + // Ensure config exists Config::ensure_config_exists()?; diff --git a/tests/deps.rs b/tests/deps.rs new file mode 100644 index 0000000..c434265 --- /dev/null +++ b/tests/deps.rs @@ -0,0 +1,137 @@ +use tmuxido::deps::{ + BinaryChecker, Dep, PackageManager, SystemBinaryChecker, check_missing, detect_package_manager, +}; + +// --- SystemBinaryChecker (real system calls) --- + +#[test] +fn system_checker_finds_sh_binary() { + let checker = SystemBinaryChecker; + assert!( + checker.is_available("sh"), + "`sh` must be present on any Unix system" + ); +} + +#[test] +fn system_checker_returns_false_for_nonexistent_binary() { + let checker = SystemBinaryChecker; + assert!(!checker.is_available("tmuxido_nonexistent_xyz_42")); +} + +// --- detect_package_manager on real system --- + +#[test] +fn should_detect_some_package_manager_on_linux() { + let checker = SystemBinaryChecker; + let pm = detect_package_manager(&checker); + assert!( + pm.is_some(), + "Expected to detect at least one package manager on this Linux system" + ); +} + +// --- PackageManager metadata completeness --- + +#[test] +fn all_package_managers_have_non_empty_detection_binary() { + for pm in PackageManager::all_ordered() { + assert!( + !pm.detection_binary().is_empty(), + "{:?} has empty detection binary", + pm + ); + } +} + +#[test] +fn all_package_managers_have_non_empty_display_name() { + for pm in PackageManager::all_ordered() { + assert!( + !pm.display_name().is_empty(), + "{:?} has empty display name", + pm + ); + } +} + +#[test] +fn install_command_always_starts_with_sudo() { + let packages = &["fzf", "tmux"]; + for pm in PackageManager::all_ordered() { + let cmd = pm.install_command(packages); + assert_eq!( + cmd.first().map(String::as_str), + Some("sudo"), + "{} install command should start with sudo", + pm.display_name() + ); + } +} + +#[test] +fn install_command_always_contains_requested_packages() { + let packages = &["fzf", "tmux"]; + for pm in PackageManager::all_ordered() { + let cmd = pm.install_command(packages); + assert!( + cmd.contains(&"fzf".to_string()), + "{} command missing 'fzf'", + pm.display_name() + ); + assert!( + cmd.contains(&"tmux".to_string()), + "{} command missing 'tmux'", + pm.display_name() + ); + } +} + +// --- Dep completeness --- + +#[test] +fn dep_package_names_are_standard() { + assert_eq!(Dep::Fzf.package_name(), "fzf"); + assert_eq!(Dep::Tmux.package_name(), "tmux"); +} + +#[test] +fn all_deps_have_matching_binary_and_package_names() { + for dep in Dep::all() { + assert!(!dep.binary_name().is_empty()); + assert!(!dep.package_name().is_empty()); + } +} + +// --- check_missing on real system --- + +#[test] +fn check_missing_returns_only_actually_missing_tools() { + let checker = SystemBinaryChecker; + let missing = check_missing(&checker); + // Every item reported as missing must NOT be findable via `which` + for dep in &missing { + assert!( + !checker.is_available(dep.binary_name()), + "{} reported as missing but `which` finds it", + dep.binary_name() + ); + } +} + +#[test] +fn check_missing_does_not_report_present_tools_as_missing() { + let checker = SystemBinaryChecker; + let missing = check_missing(&checker); + // Every dep NOT in missing list must be available + let missing_names: Vec<&str> = missing.iter().map(|d| d.binary_name()).collect(); + for dep in Dep::all() { + if !missing_names.contains(&dep.binary_name()) { + assert!( + checker.is_available(dep.binary_name()), + "{} not in missing list but `which` can't find it", + dep.binary_name() + ); + } + } +} diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile new file mode 100644 index 0000000..9eb7beb --- /dev/null +++ b/tests/docker/Dockerfile @@ -0,0 +1,43 @@ +# syntax=docker/dockerfile:1 + +# ---- Stage 1: Build (Rust stable on Debian slim) ---- +FROM rust:1-slim AS builder + +WORKDIR /src + +# Copy manifests first so cargo can resolve deps (layer cache friendly) +COPY Cargo.toml Cargo.lock rust-toolchain.toml ./ + +# Copy source and build release binary +COPY src/ ./src/ +RUN cargo build --release --locked + +# ---- Stage 2: Test environment (fresh Ubuntu, no fzf/tmux) ---- +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Install only what's needed to run the test suite itself +# (git + sudo so Test 7 can install fzf/tmux via apt) +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + sudo \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Create an unprivileged user with passwordless sudo +# (simulates a regular developer who can install packages) +RUN useradd -m -s /bin/bash testuser \ + && echo "testuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + +# Install the tmuxido binary built in stage 1 +COPY --from=builder /src/target/release/tmuxido /usr/local/bin/tmuxido + +# Copy and register the test entrypoint +COPY tests/docker/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +USER testuser +WORKDIR /home/testuser + +ENTRYPOINT ["entrypoint.sh"] diff --git a/tests/docker/entrypoint.sh b/tests/docker/entrypoint.sh new file mode 100755 index 0000000..fbe1809 --- /dev/null +++ b/tests/docker/entrypoint.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# Test suite executed inside the Ubuntu container. +# Simulates a brand-new user running tmuxido for the first time. +set -uo pipefail + +PASS=0 +FAIL=0 + +pass() { echo " ✓ $1"; PASS=$((PASS + 1)); } +fail() { echo " ✗ $1"; FAIL=$((FAIL + 1)); } + +section() { + echo "" + echo "┌─ $1" +} + +# --------------------------------------------------------------------------- +# Phase 1 — fzf and tmux are NOT installed yet +# --------------------------------------------------------------------------- + +echo "" +echo "╔══════════════════════════════════════════════════════════╗" +echo "║ tmuxido — Container Integration Tests (Ubuntu 24.04) ║" +echo "╚══════════════════════════════════════════════════════════╝" + +section "Phase 1: binary basics" + +# T1 — binary is in PATH and executable +if command -v tmuxido &>/dev/null; then + pass "tmuxido found in PATH ($(command -v tmuxido))" +else + fail "tmuxido not found in PATH" +fi + +# T2 — --help exits 0 +if tmuxido --help >/dev/null 2>&1; then + pass "--help exits with code 0" +else + fail "--help returned non-zero" +fi + +# T3 — --version shows the package name +VERSION_OUT=$(tmuxido --version 2>&1 || true) +if echo "$VERSION_OUT" | grep -q "tmuxido"; then + pass "--version output contains 'tmuxido' → $VERSION_OUT" +else + fail "--version output unexpected: $VERSION_OUT" +fi + +# --------------------------------------------------------------------------- +# Phase 2 — dependency detection (fzf and tmux absent) +# --------------------------------------------------------------------------- + +section "Phase 2: dependency detection (fzf and tmux not installed)" + +# Pipe "n" so tmuxido declines to install and exits +DEP_OUT=$(echo "n" | tmuxido 2>&1 || true) + +# T4 — fzf reported as missing +if echo "$DEP_OUT" | grep -q "fzf"; then + pass "fzf detected as missing" +else + fail "fzf NOT detected as missing. Full output:\n$DEP_OUT" +fi + +# T5 — tmux reported as missing +if echo "$DEP_OUT" | grep -q "tmux"; then + pass "tmux detected as missing" +else + fail "tmux NOT detected as missing. Full output:\n$DEP_OUT" +fi + +# T6 — "not installed" heading appears +if echo "$DEP_OUT" | grep -q "not installed"; then + pass "User-facing 'not installed' message shown" +else + fail "'not installed' message missing. Full output:\n$DEP_OUT" +fi + +# T7 — apt detected as package manager (Ubuntu 24.04) +if echo "$DEP_OUT" | grep -q "apt"; then + pass "apt detected as the package manager" +else + fail "apt NOT detected. Full output:\n$DEP_OUT" +fi + +# T8 — install command includes sudo apt install +if echo "$DEP_OUT" | grep -q "sudo apt install"; then + pass "Install command 'sudo apt install' shown to user" +else + fail "Install command incorrect. Full output:\n$DEP_OUT" +fi + +# T9 — cancellation message when user answers "n" +if echo "$DEP_OUT" | grep -q "cancelled\|Cancelled\|manually"; then + pass "Graceful cancellation message shown" +else + fail "Cancellation message missing. Full output:\n$DEP_OUT" +fi + +# --------------------------------------------------------------------------- +# Phase 3 — install deps and run full workflow +# --------------------------------------------------------------------------- + +section "Phase 3: full workflow (after installing fzf, tmux and git)" + +echo " Installing fzf, tmux via apt (this may take a moment)..." +sudo apt-get update -qq 2>/dev/null +sudo apt-get install -y --no-install-recommends fzf tmux 2>/dev/null + +# T10 — fzf now available +if command -v fzf &>/dev/null; then + pass "fzf installed successfully ($(fzf --version 2>&1 | head -1))" +else + fail "fzf still not available after installation" +fi + +# T11 — tmux now available +if command -v tmux &>/dev/null; then + pass "tmux installed successfully ($(tmux -V))" +else + fail "tmux still not available after installation" +fi + +# T12 — tmuxido no longer triggers dependency prompt +NO_DEP_OUT=$(echo "" | tmuxido 2>&1 || true) +if echo "$NO_DEP_OUT" | grep -q "not installed"; then + fail "Dependency prompt still shown after installing deps" +else + pass "No dependency prompt after deps are installed" +fi + +# T13 — set up a minimal git project tree for scanning +mkdir -p ~/Projects/demo-app +git -C ~/Projects/demo-app init --quiet +git -C ~/Projects/demo-app config user.email "test@test.com" +git -C ~/Projects/demo-app config user.name "Test" + +mkdir -p ~/.config/tmuxido +cat > ~/.config/tmuxido/tmuxido.toml <<'EOF' +paths = ["~/Projects"] +max_depth = 3 +cache_enabled = true +EOF + +# T13 — --refresh scans and finds our demo project +REFRESH_OUT=$(tmuxido --refresh 2>&1 || true) +if echo "$REFRESH_OUT" | grep -q "projects\|Projects"; then + pass "--refresh scanned and reported projects" +else + fail "--refresh output unexpected: $REFRESH_OUT" +fi + +# T14 — --cache-status reports the cache that was just built +CACHE_OUT=$(tmuxido --cache-status 2>&1 || true) +if echo "$CACHE_OUT" | grep -qi "cache"; then + pass "--cache-status reports cache info" +else + fail "--cache-status output unexpected: $CACHE_OUT" +fi + +# T15 — cache contains our demo project +if echo "$CACHE_OUT" | grep -q "Projects cached: [^0]"; then + pass "Cache contains at least 1 project" +else + # Try alternate grep in case format differs + if echo "$CACHE_OUT" | grep -q "cached:"; then + pass "--cache-status shows cached projects (count check skipped)" + else + fail "Cache appears empty. Output: $CACHE_OUT" + fi +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +echo "" +echo "╔══════════════════════════════════════════════════════════╗" +printf "║ Results: %-3d passed, %-3d failed%*s║\n" \ + "$PASS" "$FAIL" $((24 - ${#PASS} - ${#FAIL})) "" +echo "╚══════════════════════════════════════════════════════════╝" +echo "" + +[ "$FAIL" -eq 0 ] diff --git a/tests/docker/run.sh b/tests/docker/run.sh new file mode 100755 index 0000000..8ae9d21 --- /dev/null +++ b/tests/docker/run.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Build the tmuxido Docker test image and run the container integration tests. +# +# Usage: +# ./tests/docker/run.sh # build + run +# ./tests/docker/run.sh --no-cache # force rebuild from scratch +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +IMAGE_NAME="tmuxido-test" + +# Propagate --no-cache if requested +BUILD_FLAGS=() +if [[ "${1:-}" == "--no-cache" ]]; then + BUILD_FLAGS+=(--no-cache) +fi + +echo "╔══════════════════════════════════════════════════════════╗" +echo "║ tmuxido — Docker Integration Test Runner ║" +echo "╚══════════════════════════════════════════════════════════╝" +echo "" +echo "Project root : $PROJECT_ROOT" +echo "Dockerfile : $SCRIPT_DIR/Dockerfile" +echo "Image name : $IMAGE_NAME" +echo "" + +# ---- Build ---------------------------------------------------------------- +echo "Building image (stage 1: rust compile, stage 2: ubuntu test env)..." +docker build \ + "${BUILD_FLAGS[@]}" \ + --tag "$IMAGE_NAME" \ + --file "$SCRIPT_DIR/Dockerfile" \ + "$PROJECT_ROOT" + +echo "" +echo "Build complete. Running tests..." +echo "" + +# ---- Run ------------------------------------------------------------------ +docker run \ + --rm \ + --name "${IMAGE_NAME}-run" \ + "$IMAGE_NAME" + +EXIT=$? + +if [ "$EXIT" -eq 0 ]; then + echo "All tests passed." +else + echo "Some tests FAILED (exit $EXIT)." +fi + +exit "$EXIT"