diff --git a/Cargo.toml b/Cargo.toml index 956ac4a..562a763 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "star-setup" -version = "0.3.4" +version = "0.3.5" edition = "2021" repository = "https://github.com/star-setup/core" description = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems" diff --git a/src/cli/build/detect.rs b/src/cli/build/detect.rs index 265aed5..d98fe36 100644 --- a/src/cli/build/detect.rs +++ b/src/cli/build/detect.rs @@ -5,7 +5,7 @@ fn pick_build_system( has_cmake: bool, has_meson: bool, none_err: &str, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result { match (has_cmake, has_meson) { (true, false) => Ok(BuildSystem::Cmake), @@ -26,7 +26,7 @@ fn pick_build_system( /// Detects the build system in use by inspecting the given directory. /// # Errors /// Returns an error on EOF during prompt, or if no supported build system is found. -pub fn detect_build_system(dir: &Path, ctx: &mut RunCtx<'_>) -> Result { +pub fn detect_build_system(dir: &Path, ctx: &mut RunCtx<'_, '_>) -> Result { crate::time!(ctx.io.timing, ctx.io.output, "Detect", { let has_cmake = dir.join("CMakeLists.txt").exists(); let has_meson = dir.join("meson.build").exists(); @@ -39,7 +39,7 @@ pub fn detect_build_system(dir: &Path, ctx: &mut RunCtx<'_>) -> Result, + ctx: &mut RunCtx<'_, '_>, ) -> Result { writeln!(ctx.io.output, "Detecting build system\n").ok(); crate::time!(ctx.io.timing, ctx.io.output, "Detect", { diff --git a/src/cli/build/types.rs b/src/cli/build/types.rs index 27fd7f3..03e6c58 100644 --- a/src/cli/build/types.rs +++ b/src/cli/build/types.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; +use std::str::FromStr; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum BuildSystem { /// `CMake` build system (`CMakeLists.txt`). Cmake, @@ -8,7 +10,26 @@ pub enum BuildSystem { Meson, } -#[derive(Default, Clone, Serialize, Deserialize, PartialEq, Debug)] +impl FromStr for BuildSystem { + type Err = String; + + /// Parses a build system string. + /// # Errors + /// Returns an error if the string does not match `cmake` or `meson`. + fn from_str(s: &str) -> Result { + let systems = [("cmake", Self::Cmake), ("meson", Self::Meson)]; + + for (name, variant) in systems { + if s.eq_ignore_ascii_case(name) { + return Ok(variant); + } + } + + Err(format!("Unknown build system '{s}'. Valid: cmake, meson")) + } +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum BuildType { /// Debug build with no optimizations. @@ -17,8 +38,10 @@ pub enum BuildType { /// Optimized release build. Release, /// Release build with debug info. + #[serde(alias = "relwithdebinfo", alias = "debugoptimized")] RelWithDebInfo, /// Minimized binary size release build. + #[serde(alias = "minsizerel", alias = "minsize")] MinSizeRel, } @@ -44,36 +67,33 @@ impl BuildType { } } -impl std::str::FromStr for BuildSystem { - type Err = String; - - /// Parses a build system string. - /// # Errors - /// Returns an error if the string does not match `cmake` or `meson`. - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "cmake" => Ok(Self::Cmake), - "meson" => Ok(Self::Meson), - _ => Err(format!("Unknown build system '{s}'. Valid: cmake, meson")), - } - } -} - -impl std::str::FromStr for BuildType { +impl FromStr for BuildType { type Err = String; /// Parses a build type string, accepting canonical and system-specific aliases. /// # Errors /// Returns an error if the string does not match any known build type. fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "debug" => Ok(Self::Debug), - "release" => Ok(Self::Release), - "rel-with-deb-info" | "relwithdebinfo" | "debugoptimized" => Ok(Self::RelWithDebInfo), - "min-size-rel" | "minsizerel" | "minsize" => Ok(Self::MinSizeRel), - _ => Err(format!( - "Unknown build type '{s}'. Canonical: debug, release, rel-with-deb-info, min-size-rel" - )), + let types: &[(&[&str], Self)] = &[ + (&["debug"], Self::Debug), + (&["release"], Self::Release), + ( + &["rel-with-deb-info", "relwithdebinfo", "debugoptimized"], + Self::RelWithDebInfo, + ), + (&["min-size-rel", "minsizerel", "minsize"], Self::MinSizeRel), + ]; + + for (aliases, variant) in types { + for alias in *aliases { + if s.eq_ignore_ascii_case(alias) { + return Ok(*variant); + } + } } + + Err(format!( + "Unknown build type '{s}'. Canonical: debug, release, rel-with-deb-info, min-size-rel" + )) } } diff --git a/src/cli/resolve.rs b/src/cli/resolve.rs index bb316f7..7c35c5d 100644 --- a/src/cli/resolve.rs +++ b/src/cli/resolve.rs @@ -16,7 +16,10 @@ pub fn resolve_bool(positive: bool, negative: bool, config: Option, defaul if positive { return true; } - config.unwrap_or(default) + match config { + Some(val) => val, + None => default, + } } /// Resolves raw `Args` into `ResolvedArgs` by applying config defaults and CLI overrides. @@ -24,14 +27,12 @@ pub fn resolve_bool(positive: bool, negative: bool, config: Option, defaul /// Returns an error if the named config does not exist in the provided `SetupConfig`. pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result { let config_name = args.config_name.as_deref().unwrap_or("default"); - if let Some(name) = &args.config_name { - if !config.configs.contains_key(name.as_str()) { - return Err(format!("Configuration '{name}' not found")); - } - } - let default = config.configs.get(config_name); + if args.config_name.is_some() && default.is_none() { + return Err(format!("Configuration '{config_name}' not found")); + } + let ssh = resolve_bool( args.connection.ssh, args.connection.https, @@ -68,12 +69,14 @@ pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result Result()? - } else { - default.map(|e| e.build_type.clone()).unwrap_or_default() + build_type: match args.build.build_type { + Some(s) => s.parse::()?, + None => default.map(|e| e.build_type).unwrap_or_default(), }, build_dir: args .build @@ -98,8 +100,8 @@ pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result Result<&str, String> { + path + .to_str() + .ok_or_else(|| format!("Invalid path: {}", path.display())) +} + /// Runs `CMake` configuration and optionally builds the project in `build_path`. /// # Errors /// Returns an error if any `CMake` command fails. @@ -13,7 +19,7 @@ pub fn cmake_build( args: &ResolvedArgs, build_path: &Path, mono: bool, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { let build_type_flag = format!("-DCMAKE_BUILD_TYPE={}", args.build.build_type.to_cmake()); let mut cmake_cmd = if mono { @@ -53,13 +59,13 @@ pub fn meson_build( args: &ResolvedArgs, build_path: &Path, source_path: &Path, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { let buildtype_flag = format!("--buildtype={}", args.build.build_type.to_meson()); let mut meson_cmd = vec!["meson", "setup"]; meson_cmd.push(&buildtype_flag); - meson_cmd.push(build_path.to_str().ok_or("Invalid build path")?); - meson_cmd.push(source_path.to_str().ok_or("Invalid source path")?); + meson_cmd.push(to_str(build_path)?); + meson_cmd.push(to_str(source_path)?); meson_cmd.extend(args.build.meson_flags.iter().map(String::as_str)); crate::time!(ctx.io.timing, ctx.io.output, "Meson setup", { @@ -69,12 +75,7 @@ pub fn meson_build( writeln!(ctx.io.output, "Building project\n").ok(); crate::time!(ctx.io.timing, ctx.io.output, "Meson compile", { ctx.runner.run( - &[ - "meson", - "compile", - "-C", - build_path.to_str().ok_or("Invalid build path")?, - ], + &["meson", "compile", "-C", to_str(build_path)?], None, &mut ctx.io, )?; @@ -92,7 +93,7 @@ pub fn build_project( source_path: &Path, build_system: BuildSystem, mono: bool, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { match build_system { BuildSystem::Cmake => cmake_build(args, build_path, mono, ctx), diff --git a/src/commands/handlers.rs b/src/commands/handlers.rs new file mode 100644 index 0000000..9b8e16e --- /dev/null +++ b/src/commands/handlers.rs @@ -0,0 +1,94 @@ +use crate::{ + cli::{ConfigAction, ProfileAction, WorkspaceAction}, + config::{ + add_config, create_default_config, list_configs, remove_config, ConfigEntry, SetupConfig, + }, + ctx::{with_runner, IoCtx}, + profile::{add_profile, list_profiles, remove_profile}, + workspace::resolve_workspace, +}; +use std::{error::Error, path::PathBuf}; + +/// Handles configuration-related subcommands. +/// # Errors +/// Returns an error if configuration initializing, addition, or removal fails. +pub fn handle_config_cmd( + action: ConfigAction, + config: &mut SetupConfig, + config_path: PathBuf, + yes: bool, + io: &mut IoCtx, +) -> Result<(), Box> { + match action { + ConfigAction::Init => create_default_config(config_path, yes, io)?, + ConfigAction::List => list_configs(config, io), + ConfigAction::Remove { name } => remove_config(config, &name, yes, io)?, + ConfigAction::Add { + name, + connection, + build, + mono, + diagnostic, + } => { + let entry = ConfigEntry::from_flags(&connection, &build, &mono, &diagnostic); + add_config(config, &name, entry, yes, io)?; + } + } + Ok(()) +} + +/// Handles profile-related subcommands. +/// # Errors +/// Returns an error if adding or removing profiles encounters an I/O or validation failure. +pub fn handle_profile_cmd( + action: ProfileAction, + config: &mut SetupConfig, + yes: bool, + io: &mut IoCtx, +) -> Result<(), Box> { + match action { + ProfileAction::List => list_profiles(config, io), + ProfileAction::Remove { name } => remove_profile(config, &name, yes, io)?, + ProfileAction::Add { name, repos } => { + let vals = std::iter::once(name).chain(repos).collect::>(); + add_profile(config, &vals, yes, io)?; + } + } + Ok(()) +} + +/// Handles workspace-related subcommands. +/// # Errors +/// Returns an error if resolving, updating, cleaning, or fetching status for the workspace fails. +pub fn handle_workspace_cmd(action: WorkspaceAction, io: IoCtx) -> Result<(), Box> { + match action { + WorkspaceAction::Update { + path, + mono_dir, + build_dir, + } => { + let ws = resolve_workspace(path.as_deref(), mono_dir.as_deref(), build_dir.as_deref())?; + with_runner(io, |ctx| ws.update(ctx).map_err(Into::into))?; + } + WorkspaceAction::Status { + path, + mono_dir, + build_dir, + fetch, + } => { + let ws = resolve_workspace(path.as_deref(), mono_dir.as_deref(), build_dir.as_deref())?; + let mut status_io = io; + status_io.dry_run = false; + with_runner(status_io, |ctx| ws.status(fetch, ctx).map_err(Into::into))?; + } + WorkspaceAction::Clean { + path, + mono_dir, + build_dir, + } => { + let ws = resolve_workspace(path.as_deref(), mono_dir.as_deref(), build_dir.as_deref())?; + with_runner(io, |ctx| ws.clean(ctx).map_err(Into::into))?; + } + } + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6194719..aa6b573 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -12,3 +12,5 @@ pub mod single; pub use single::single_repo_mode; pub mod setup; pub use setup::{configure_and_build, extract_repo_input, prepare_build_dir}; +pub mod handlers; +pub use handlers::{handle_config_cmd, handle_profile_cmd, handle_workspace_cmd}; diff --git a/src/commands/mono/clone.rs b/src/commands/mono/clone.rs index c156fd7..8d48336 100644 --- a/src/commands/mono/clone.rs +++ b/src/commands/mono/clone.rs @@ -7,7 +7,7 @@ pub fn clone_mono_repos( repos: &[String], repos_path: &std::path::Path, ssh: bool, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { writeln!(ctx.io.output, "Cloning repositories").ok(); crate::time!(ctx.io.timing, ctx.io.output, "Clone", { diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 5cc3e64..986cfab 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -25,7 +25,7 @@ pub fn mono_repo_mode( args: &ResolvedArgs, config: &SetupConfig, base_dir: &Path, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { let total = std::time::Instant::now(); diff --git a/src/commands/mono/setup.rs b/src/commands/mono/setup.rs index 30d0379..8a832dc 100644 --- a/src/commands/mono/setup.rs +++ b/src/commands/mono/setup.rs @@ -15,7 +15,7 @@ pub fn generate_mono_config( repos_path: &std::path::Path, repo_dirs: &[PathBuf], repos: &[String], - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result>, String> { writeln!(ctx.io.output, "Creating mono-repo configuration").ok(); match build_system { diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 369272b..758afc8 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -11,7 +11,7 @@ use std::{fs, path::Path}; pub fn prepare_build_dir( build_path: &std::path::Path, clean: bool, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { if clean && ctx.io.dry_run { writeln!(ctx.io.output, "Cleaning build directory\n").ok(); @@ -53,7 +53,7 @@ pub fn configure_and_build( build_path: &Path, build_system: BuildSystem, is_mono: bool, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { writeln!(ctx.io.output, "Configuring project\n").ok(); build_project(args, build_path, project_path, build_system, is_mono, ctx) diff --git a/src/commands/single.rs b/src/commands/single.rs index ed91519..d7965b5 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -5,7 +5,7 @@ use crate::{ }, ctx::RunCtx, prompts::confirm, - repository::{repo_dir_name, resolve_repo_url}, + repository::{clone_repository, pull_repository, repo_dir_name}, }; use std::path::Path; @@ -15,12 +15,11 @@ use std::path::Path; pub fn single_repo_mode( args: &ResolvedArgs, base_dir: &Path, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { let total = std::time::Instant::now(); let repo = extract_repo_input(args)?; - let repo_url = resolve_repo_url(repo, args.connection.ssh); let dir_name = repo_dir_name(repo); print_mode_header( @@ -42,18 +41,11 @@ pub fn single_repo_mode( if confirm("Update existing repository?", args.yes, &mut ctx.io)? { writeln!(ctx.io.output, "Updating {dir_name}\n").ok(); crate::time!(ctx.io.timing, ctx.io.output, "Update", { - ctx - .runner - .run(&["git", "pull"], Some(&repo_path), &mut ctx.io)?; + pull_repository(&repo_path, ctx)?; }); } } else { - writeln!(ctx.io.output, "Cloning {dir_name}\n").ok(); - crate::time!(ctx.io.timing, ctx.io.output, "Clone", { - ctx - .runner - .run(&["git", "clone", &repo_url, &dir_name], None, &mut ctx.io)?; - }); + clone_repository(repo, base_dir, args.connection.ssh, ctx)?; } let build_path = repo_path.join(&args.build.build_dir); diff --git a/src/config/crud.rs b/src/config/crud.rs index 621befb..349a8d8 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -2,7 +2,7 @@ use crate::{ cli::BuildType, config::{format_entry, save_config, ConfigEntry, SetupConfig}, ctx::IoCtx, - prompts::confirm, + prompts::confirm_abort, }; use std::path::PathBuf; @@ -27,13 +27,12 @@ pub fn has_config(config: &SetupConfig, name: &str) -> bool { /// Returns an error if the config file cannot be written. pub fn create_default_config(path: PathBuf, yes: bool, io: &mut IoCtx<'_>) -> Result<(), String> { if path.exists() - && !confirm( + && !confirm_abort( &format!("{} already exists. Overwrite?", path.display()), yes, io, )? { - writeln!(io.output, "Aborted.").ok(); return Ok(()); } @@ -85,13 +84,12 @@ pub fn add_config( io: &mut IoCtx<'_>, ) -> Result<(), String> { if has_config(config, name) - && !confirm( + && !confirm_abort( &format!("Warning: Configuration '{name}' already exists. Overwrite?"), yes, io, )? { - writeln!(io.output, "Aborted.").ok(); return Ok(()); } @@ -135,8 +133,7 @@ pub fn remove_config( writeln!(io.output, "Configuration details:").ok(); write!(io.output, "{}", format_entry(e)).ok(); - if !confirm("\nAre you sure you want to remove this config?", yes, io)? { - writeln!(io.output, "Aborted.").ok(); + if !confirm_abort("\nAre you sure you want to remove this config?", yes, io)? { return Ok(()); } diff --git a/src/config/types.rs b/src/config/types.rs index e6e1615..56a76f8 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -74,7 +74,7 @@ impl From<&ResolvedArgs> for ConfigEntry { fn from(args: &ResolvedArgs) -> Self { Self { ssh: args.connection.ssh, - build_type: args.build.build_type.clone(), + build_type: args.build.build_type, build_dir: args.build.build_dir.clone(), mono_dir: args.mono.mono_dir.clone(), no_build: args.build.no_build, diff --git a/src/ctx.rs b/src/ctx.rs index 520ff47..d6c1c06 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -1,5 +1,6 @@ use crate::utils::process::run_command; use std::{ + error::Error, io::{BufRead, Write}, path::Path, }; @@ -75,7 +76,21 @@ pub struct IoCtx<'a> { } /// Full execution context combining IO and a command runner. -pub struct RunCtx<'a> { - pub io: IoCtx<'a>, - pub runner: &'a mut dyn Runner, +pub struct RunCtx<'io, 'run> { + pub io: IoCtx<'io>, + pub runner: &'run mut dyn Runner, +} + +/// Helper to quickly execute a workspace/repo task with the correct runner. +/// # Errors +/// Returns an error if the closure function `f` execution returns an error block. +pub fn with_runner(io: IoCtx, f: F) -> Result<(), Box> +where + F: FnOnce(&mut RunCtx) -> Result<(), Box>, +{ + let mut dry = DryRunRunner; + let mut real = ProcessRunner; + let runner: &mut dyn Runner = if io.dry_run { &mut dry } else { &mut real }; + let mut ctx = RunCtx { io, runner }; + f(&mut ctx) } diff --git a/src/interactive.rs b/src/interactive.rs index 5701b1c..80220ed 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -3,7 +3,7 @@ use crate::{ cli::{BuildType, ResolvedArgs}, ctx::IoCtx, - prompts::{ask, ask_default, ask_yesno}, + prompts::{ask, ask_bool_if, ask_default, ask_required}, }; /// Interactive CLI mode — prompts for any unset arguments. @@ -13,34 +13,22 @@ pub fn interactive_mode(args: &mut ResolvedArgs, io: &mut IoCtx<'_>) -> Result<( writeln!(io.output, "Star Setup Interactive Mode").ok(); if args.repo.is_none() { - loop { - let repo = ask("Enter repository (user/repo or URL)", io)?; - if !repo.is_empty() { - args.repo = Some(repo); - break; - } - } + args.repo = Some(ask_required("Enter repository (user/repo or URL)", io)?); } - if !args.connection.ssh { - args.connection.ssh = ask_yesno("Use SSH?", false, io)?; - } - if !args.connection.verbose { - args.connection.verbose = ask_yesno("Verbose?", false, io)?; - io.verbose = args.connection.verbose; - } - if !args.diagnostic.timing { - args.diagnostic.timing = ask_yesno("Show timing?", false, io)?; - io.timing = args.diagnostic.timing; - } - if !args.build.clean { - args.build.clean = ask_yesno("Clean build directory if exists?", false, io)?; - } + args.connection.ssh = ask_bool_if("Use SSH?", args.connection.ssh, io)?; + + args.connection.verbose = ask_bool_if("Verbose?", args.connection.verbose, io)?; + io.verbose = args.connection.verbose; + + args.diagnostic.timing = ask_bool_if("Show timing?", args.diagnostic.timing, io)?; + io.timing = args.diagnostic.timing; + + args.build.clean = ask_bool_if("Clean build directory if exists?", args.build.clean, io)?; if !args.mono.mono_repo { loop { - let mode = ask("Select mode: (1) Single Repo (2) Mono-Repo", io)?; - match mode.as_str() { + match ask("Select mode: (1) Single Repo (2) Mono-Repo", io)?.as_str() { "1" => break, "2" => { args.mono.mono_repo = true; @@ -53,29 +41,17 @@ pub fn interactive_mode(args: &mut ResolvedArgs, io: &mut IoCtx<'_>) -> Result<( if args.mono.mono_repo && args.mono.profile.is_none() && args.mono.repos.is_none() { loop { - let choice = ask("Mono-repo: (1) Use profile (2) Manual repo list", io)?; - match choice.as_str() { + match ask("Mono-repo: (1) Use profile (2) Manual repo list", io)?.as_str() { "1" => { - loop { - let profile = ask("Profile name", io)?; - if !profile.is_empty() { - args.mono.profile = Some(profile); - break; - } - } + args.mono.profile = Some(ask_required("Profile name", io)?); break; } "2" => { - loop { - let repo_list = ask( - "Enter repos (space separated 'username/lib1 username/lib2')", - io, - )?; - if !repo_list.is_empty() { - args.mono.repos = Some(repo_list.split_whitespace().map(String::from).collect()); - break; - } - } + let repo_list = ask_required( + "Enter repos (space separated 'username/lib1 username/lib2')", + io, + )?; + args.mono.repos = Some(repo_list.split_whitespace().map(String::from).collect()); break; } _ => {} @@ -87,9 +63,7 @@ pub fn interactive_mode(args: &mut ResolvedArgs, io: &mut IoCtx<'_>) -> Result<( args.build.build_type = build_type_str.parse::()?; args.build.build_dir = ask_default("Build directory", &args.build.build_dir, io)?; - if !args.build.no_build { - args.build.no_build = ask_yesno("Configure only (skip build)?", false, io)?; - } + args.build.no_build = ask_bool_if("Configure only (skip build)?", args.build.no_build, io)?; writeln!(io.output, "\nInteractive mode complete").ok(); Ok(()) diff --git a/src/main.rs b/src/main.rs index 4e4d1ca..4aeb93d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,12 @@ //! Entry point. Parses arguments, loads config, and dispatches to the appropriate command handler. use star_setup::run::run; +use std::path::PathBuf; fn main() { - if let Err(e) = run() { + let config_path = PathBuf::from(".star-setup.json"); + + if let Err(e) = run(config_path) { eprintln!("Error: {e}"); std::process::exit(1); } diff --git a/src/profile/crud.rs b/src/profile/crud.rs index 747ffec..00ad319 100644 --- a/src/profile/crud.rs +++ b/src/profile/crud.rs @@ -2,7 +2,7 @@ use crate::{ config::{save_config, SetupConfig}, ctx::IoCtx, profile::print_profile_details, - prompts::confirm, + prompts::confirm_abort, }; /// Inserts or overwrites a named profile. @@ -39,13 +39,12 @@ pub fn add_profile( let repos = args[1..].to_vec(); if has_profile(config, &name) - && !confirm( + && !confirm_abort( &format!("Warning: Profile '{name}' already exists. Overwrite?"), yes, io, )? { - writeln!(io.output, "Aborted.").ok(); return Ok(()); } @@ -90,12 +89,11 @@ pub fn remove_profile( &repos, ); - if !confirm( + if !confirm_abort( &format!("Are you sure you want to remove profile '{name}'?"), yes, io, )? { - writeln!(io.output, "Aborted.").ok(); return Ok(()); } diff --git a/src/prompts.rs b/src/prompts.rs index 73a3de0..524ce13 100644 --- a/src/prompts.rs +++ b/src/prompts.rs @@ -2,12 +2,11 @@ use crate::ctx::IoCtx; -/// Prompts the user for a required string value. -/// # Errors -/// Returns an error if stdin reaches EOF unexpectedly. -pub fn ask(prompt: &str, io: &mut IoCtx<'_>) -> Result { - write!(io.output, "{prompt}: ").ok(); +/// Internal helper to print a prompt, flush, and read a trimmed line of input. +fn read_input_line(prompt: &str, io: &mut IoCtx<'_>) -> Result { + write!(io.output, "{prompt}").ok(); io.output.flush().ok(); + let mut line = String::new(); if io.input.read_line(&mut line).unwrap_or(0) == 0 { return Err("unexpected end of input".to_string()); @@ -15,21 +14,22 @@ pub fn ask(prompt: &str, io: &mut IoCtx<'_>) -> Result { Ok(line.trim().to_string()) } +/// Prompts the user for a required string value. +/// # Errors +/// Returns an error if stdin reaches EOF unexpectedly. +pub fn ask(prompt: &str, io: &mut IoCtx<'_>) -> Result { + read_input_line(&format!("{prompt}: "), io) +} + /// Prompts the user for a string value, returning `default` if the input is empty. /// # Errors /// Returns an error if stdin reaches EOF unexpectedly. pub fn ask_default(prompt: &str, default: &str, io: &mut IoCtx<'_>) -> Result { - write!(io.output, "{prompt} [{default}]: ").ok(); - io.output.flush().ok(); - let mut line = String::new(); - if io.input.read_line(&mut line).unwrap_or(0) == 0 { - return Err("unexpected end of input".to_string()); - } - let val = line.trim().to_string(); - Ok(if val.is_empty() { + let input = read_input_line(&format!("{prompt} [{default}]: "), io)?; + Ok(if input.is_empty() { default.to_string() } else { - val + input }) } @@ -38,14 +38,12 @@ pub fn ask_default(prompt: &str, default: &str, io: &mut IoCtx<'_>) -> Result) -> Result { let default_char = if default { "Y" } else { "N" }; - write!(io.output, "{prompt} (y/n) [{default_char}]: ").ok(); - io.output.flush().ok(); - let mut line = String::new(); - if io.input.read_line(&mut line).unwrap_or(0) == 0 { - return Err("unexpected end of input".to_string()); - } - let val = line.trim().to_lowercase(); - Ok(if val.is_empty() { default } else { val.eq("y") }) + let input = read_input_line(&format!("{prompt} (y/n) [{default_char}]: "), io)?; + Ok(if input.is_empty() { + default + } else { + input.eq_ignore_ascii_case("y") + }) } /// Prompts the user to select from a numbered list of options. @@ -58,14 +56,8 @@ pub fn ask_choice(prompt: &str, options: &[&str], io: &mut IoCtx<'_>) -> Result< writeln!(io.output, " {}) {opt}", i + 1).ok(); } loop { - write!(io.output, "Select: ").ok(); - io.output.flush().ok(); - let mut line = String::new(); - if io.input.read_line(&mut line).unwrap_or(0) == 0 { - return Err("unexpected end of input".to_string()); - } - let val = line.trim(); - if let Ok(n) = val.parse::() { + let input = read_input_line("Select: ", io)?; + if let Ok(n) = input.parse::() { if n >= 1 && n <= options.len() { return Ok(n - 1); } @@ -73,6 +65,29 @@ pub fn ask_choice(prompt: &str, options: &[&str], io: &mut IoCtx<'_>) -> Result< } } +/// Prompts `ask_yesno` only if the condition isn't already met. +/// # Errors +/// Returns an error on EOF or if the selection is out of range. +pub fn ask_bool_if(prompt: &str, current_val: bool, io: &mut IoCtx<'_>) -> Result { + if current_val { + Ok(current_val) + } else { + ask_yesno(prompt, false, io) + } +} + +/// Repeatedly ask until a non-empty string is provided. +/// # Errors +/// Returns an error on EOF or if the selection is out of range. +pub fn ask_required(prompt: &str, io: &mut IoCtx<'_>) -> Result { + loop { + let response = ask(prompt, io)?; + if !response.is_empty() { + return Ok(response); + } + } +} + /// Returns `true` if `yes` is set or the user enters `y`/`Y`. /// # Errors /// Returns an error if stdin reaches EOF unexpectedly. @@ -80,12 +95,17 @@ pub fn confirm(prompt: &str, yes: bool, io: &mut IoCtx<'_>) -> Result) -> Result { + if !confirm(warning_msg, yes, io)? { + writeln!(io.output, "Aborted.").ok(); + return Ok(false); } - Ok(line.trim().eq_ignore_ascii_case("y")) + Ok(true) } diff --git a/src/repository.rs b/src/repository.rs index be4d0b1..1674bea 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -41,7 +41,7 @@ pub fn clone_repository( repo_path: &str, target_dir: &Path, use_ssh: bool, - ctx: &mut RunCtx<'_>, + ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { let repo_name = repo_dir_name(repo_path); let repo_dir = target_dir.join(&repo_name); @@ -63,3 +63,12 @@ pub fn clone_repository( ) .map_err(|e| format!("Failed to clone {repo_path}: {e}")) } + +/// Pulls the latest changes for an existing repository. +/// # Errors +/// Returns an error if the `git pull` command fails. +pub fn pull_repository(repo_path: &Path, ctx: &mut RunCtx<'_, '_>) -> Result<(), String> { + ctx + .runner + .run(&["git", "pull"], Some(repo_path), &mut ctx.io) +} diff --git a/src/run.rs b/src/run.rs index 5544120..e0ccd30 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,14 +1,12 @@ use crate::{ - cli::{args::Command, resolve_with_config, Args, ConfigAction, ProfileAction, WorkspaceAction}, - commands::{mono_repo_mode, single_repo_mode}, - config::{ - add_config, create_default_config, list_configs, load_config, remove_config, ConfigEntry, + cli::{args::Command, resolve_with_config, Args}, + commands::{ + handle_config_cmd, handle_profile_cmd, handle_workspace_cmd, mono_repo_mode, single_repo_mode, }, - ctx::{DryRunRunner, IoCtx, ProcessRunner, RunCtx, Runner}, + config::load_config, + ctx::{with_runner, IoCtx}, interactive::interactive_mode, - profile::{add_profile, list_profiles, remove_profile}, utils::check_prerequisites, - workspace::{clean_workspace, resolve_workspace, status_workspace, update_workspace}, }; use clap::Parser; use std::{ @@ -17,23 +15,25 @@ use std::{ path::{Path, PathBuf}, }; -const CONFIG_FILE_NAME: &str = ".star-setup.json"; - /// Runs the setup process. /// # Errors /// Returns an error if the configuration file is missing or corrupted. -#[allow(clippy::too_many_lines)] -pub fn run() -> Result<(), Box> { +pub fn run(config_path: PathBuf) -> Result<(), Box> { let mut stdin = io::stdin().lock(); let mut stdout = io::stdout(); let is_terminal = stdin.is_terminal() && stdout.is_terminal(); - let locations = vec![ - PathBuf::from(CONFIG_FILE_NAME), + let locations = [ + Some(config_path.as_path()), dirs::home_dir() - .map(|h| h.join(CONFIG_FILE_NAME)) - .unwrap_or_default(), - ]; + .as_deref() + .map(|h| h.join(&config_path)) + .as_deref(), + ] + .into_iter() + .flatten() + .map(Path::to_path_buf) + .collect::>(); let mut config = load_config(&locations, &mut stdout); let raw = Args::parse(); @@ -46,88 +46,13 @@ pub fn run() -> Result<(), Box> { dry_run: raw.diagnostic.dry_run, }; - if let Some(command) = raw.command { - match command { - Command::Config(cmd) => match cmd.action { - ConfigAction::Init => { - create_default_config(PathBuf::from(CONFIG_FILE_NAME), raw.yes, &mut io)?; - } - ConfigAction::List => list_configs(&config, &mut io), - ConfigAction::Remove { name } => { - remove_config(&mut config, &name, raw.yes, &mut io)?; - } - ConfigAction::Add { - name, - connection, - build, - mono, - diagnostic, - } => { - let entry = ConfigEntry::from_flags(&connection, &build, &mono, &diagnostic); - add_config(&mut config, &name, entry, raw.yes, &mut io)?; - } - }, - Command::Profile(cmd) => match cmd.action { - ProfileAction::List => list_profiles(&config, &mut io), - ProfileAction::Remove { name } => { - remove_profile(&mut config, &name, raw.yes, &mut io)?; - } - ProfileAction::Add { name, repos } => { - let vals = std::iter::once(name).chain(repos).collect::>(); - add_profile(&mut config, &vals, raw.yes, &mut io)?; - } - }, - Command::Workspace(cmd) => match cmd.action { - WorkspaceAction::Update { - path, - mono_dir, - build_dir, - } => { - let workspace = - resolve_workspace(path.as_deref(), mono_dir.as_deref(), build_dir.as_deref())?; - let mut dry = DryRunRunner; - let mut real = ProcessRunner; - let runner: &mut dyn Runner = if raw.diagnostic.dry_run { - &mut dry - } else { - &mut real - }; - let mut ctx = RunCtx { io, runner }; - update_workspace(&workspace, &mut ctx)?; - } - WorkspaceAction::Status { - path, - mono_dir, - build_dir, - fetch, - } => { - let workspace = - resolve_workspace(path.as_deref(), mono_dir.as_deref(), build_dir.as_deref())?; - let mut real = ProcessRunner; - let mut ctx = RunCtx { - io, - runner: &mut real, - }; - status_workspace(&workspace, fetch, &mut ctx)?; - } - WorkspaceAction::Clean { - path, - mono_dir, - build_dir, - } => { - let workspace = - resolve_workspace(path.as_deref(), mono_dir.as_deref(), build_dir.as_deref())?; - let mut dry = DryRunRunner; - let mut real = ProcessRunner; - let runner: &mut dyn Runner = if raw.diagnostic.dry_run { - &mut dry - } else { - &mut real - }; - let mut ctx = RunCtx { io, runner }; - clean_workspace(&workspace, &mut ctx)?; - } - }, + if let Some(cmd) = raw.command { + match cmd { + Command::Config(c) => { + handle_config_cmd(c.action, &mut config, config_path, raw.yes, &mut io)?; + } + Command::Profile(p) => handle_profile_cmd(p.action, &mut config, raw.yes, &mut io)?, + Command::Workspace(w) => handle_workspace_cmd(w.action, io)?, } return Ok(()); } @@ -143,16 +68,14 @@ pub fn run() -> Result<(), Box> { check_prerequisites(&mut io)?; - let mut dry = DryRunRunner; - let mut real = ProcessRunner; - let runner: &mut dyn Runner = if io.dry_run { &mut dry } else { &mut real }; - let mut ctx = RunCtx { io, runner }; - - if args.mono.mono_repo { - mono_repo_mode(&args, &config, Path::new("."), &mut ctx)?; - } else { - single_repo_mode(&args, Path::new("."), &mut ctx)?; - } + with_runner(io, |ctx| { + if args.mono.mono_repo { + mono_repo_mode(&args, &config, Path::new("."), ctx)?; + } else { + single_repo_mode(&args, Path::new("."), ctx)?; + } + Ok(()) + })?; Ok(()) } diff --git a/src/utils/prerequisites.rs b/src/utils/prerequisites.rs index 23f3fe9..4349e8a 100644 --- a/src/utils/prerequisites.rs +++ b/src/utils/prerequisites.rs @@ -7,22 +7,25 @@ use std::process::Command; /// Returns an error if any required tool is missing from PATH. pub fn check_prerequisites(io: &mut IoCtx<'_>) -> Result<(), String> { crate::time!(io.timing, io.output, "Check prerequisites", { - let mut missing: Vec<&str> = Vec::new(); + let missing: Vec<&str> = ["git", "cmake", "meson"] + .into_iter() + .filter(|&tool| { + let is_missing = Command::new(tool) + .arg("--version") + .output() + .map_or(true, |o| !o.status.success()); + + if !is_missing && io.verbose { + let _ = writeln!(io.output, " Found {tool}"); + } + is_missing + }) + .collect(); - for tool in &["git", "cmake", "meson"] { - if Command::new(tool) - .arg("--version") - .output() - .map_or(true, |o| !o.status.success()) - { - missing.push(tool); - } else if io.verbose { - writeln!(io.output, " Found {tool}").ok(); - } - } if !missing.is_empty() { return Err(format!("Missing required tools: {}", missing.join(", "))); } + Ok(()) }) } diff --git a/src/utils/process.rs b/src/utils/process.rs index bc8474f..a8e034b 100644 --- a/src/utils/process.rs +++ b/src/utils/process.rs @@ -1,18 +1,22 @@ use std::{ - io::{Read, Write}, + io::Write, path::Path, process::{Command, Stdio}, - thread, +}; + +#[cfg(target_os = "windows")] +use std::{ + collections::HashMap, + path::PathBuf, }; /// Finds vcvars64.bat using vswhere.exe. /// Returns None if vswhere is not found or no VS installation exists. #[cfg(target_os = "windows")] -fn find_vcvars() -> Option { - let vswhere = std::path::PathBuf::from( - std::env::var("ProgramFiles(x86)").unwrap_or_else(|_| r"C:\Program Files (x86)".to_string()), - ) - .join(r"Microsoft Visual Studio\Installer\vswhere.exe"); +fn find_vcvars() -> Option { + let program_files = + std::env::var("ProgramFiles(x86)").unwrap_or_else(|_| r"C:\Program Files (x86)".to_string()); + let vswhere = PathBuf::from(program_files).join(r"Microsoft Visual Studio\Installer\vswhere.exe"); if !vswhere.exists() { return None; @@ -24,8 +28,7 @@ fn find_vcvars() -> Option { .ok()?; let install_path = String::from_utf8(output.stdout).ok()?; - let vcvars = - std::path::PathBuf::from(install_path.trim()).join(r"VC\Auxiliary\Build\vcvars64.bat"); + let vcvars = PathBuf::from(install_path.trim()).join(r"VC\Auxiliary\Build\vcvars64.bat"); vcvars.exists().then_some(vcvars) } @@ -34,15 +37,12 @@ fn find_vcvars() -> Option { /// # Errors /// Returns an error if vcvars64.bat cannot be found or run. #[cfg(target_os = "windows")] -fn get_msvc_env() -> Result, String> { +fn get_msvc_env() -> Result, String> { let vcvars = find_vcvars().ok_or("Could not find vcvars64.bat via vswhere")?; + let vcvars_str = vcvars.to_str().ok_or("Invalid vcvars path")?; + let output = Command::new("cmd") - .args([ - "/c", - vcvars.to_str().ok_or("Invalid vcvars path")?, - "&&", - "set", - ]) + .args(["/c", vcvars_str, "&&", "set"]) .output() .map_err(|e| format!("Failed to run vcvars64.bat: {e}"))?; @@ -51,8 +51,8 @@ fn get_msvc_env() -> Result, String> { stdout .lines() .filter_map(|line| { - let mut parts = line.splitn(2, '='); - Some((parts.next()?.to_string(), parts.next()?.to_string())) + let (key, val) = line.split_once('=')?; + Some((key.to_string(), val.to_string())) }) .collect(), ) @@ -68,9 +68,10 @@ pub fn run_command( verbose: bool, output: &mut (impl Write + ?Sized), ) -> Result<(), String> { - if cmd.is_empty() { - return Err("No command provided".to_string()); - } + let (exe, args) = match cmd { + [] => return Err("No command provided".to_string()), + [exe, args @ ..] => (exe, args), + }; if verbose { writeln!(output, "Running: {}", cmd.join(" ")).ok(); @@ -79,9 +80,15 @@ pub fn run_command( } } - let mut command = Command::new(cmd[0]); + let mut command = Command::new(exe); command.stdin(Stdio::null()); - if cmd[0] == "git" { + command.args(args); + + if let Some(dir) = cwd { + command.current_dir(dir); + } + + if *exe == "git" { command.env("GIT_TERMINAL_PROMPT", "0"); if std::env::var("GIT_SSH_COMMAND").is_err() { command.env("GIT_SSH_COMMAND", "ssh -o BatchMode=yes"); @@ -89,65 +96,46 @@ pub fn run_command( } #[cfg(target_os = "windows")] - if std::path::Path::new(&cmd[0]) - .file_stem() - .is_some_and(|s| s.to_string_lossy().eq_ignore_ascii_case("meson")) - && std::env::var("VSINSTALLDIR").is_err() + if std::env::var("VSINSTALLDIR").is_err() + && Path::new(exe) + .file_stem() + .is_some_and(|s| s.to_string_lossy().eq_ignore_ascii_case("meson")) { if let Ok(env) = get_msvc_env() { - for (k, v) in env { - command.env(k, v); - } + command.envs(env); } } if verbose { - command.stdout(Stdio::inherit()); - command.stderr(Stdio::inherit()); - } else { - command.stdout(Stdio::null()); - command.stderr(Stdio::piped()); - } - - command.args(&cmd[1..]); - if let Some(dir) = cwd { - command.current_dir(dir); - } - - if verbose { + command.stdout(Stdio::inherit()).stderr(Stdio::inherit()); let status = command .status() .map_err(|e| format!("Failed to run command: {e}"))?; - if status.success() { - return Ok(()); - } - return Err(format!("Command failed with exit code: {status}")); - } - let mut child = command - .spawn() - .map_err(|e| format!("Failed to start command: {e}"))?; - let stderr_handle = child.stderr.take(); - let stderr_thread = thread::spawn(move || { - let mut s = String::new(); - if let Some(mut h) = stderr_handle { - h.read_to_string(&mut s).ok(); + if !status.success() { + return Err(format!("Command failed with exit code: {status}")); } - s - }); - - let status = child - .wait() - .map_err(|e| format!("Failed to wait for command: {e}"))?; - let stderr = stderr_thread.join().unwrap_or_default(); - if status.success() { - Ok(()) } else { - let msg: &str = stderr.trim(); - if msg.is_empty() { - Err(format!("Command failed with exit code: {status}")) - } else { - Err(format!("Command failed with exit code: {status}\n{msg}")) + command.stdout(Stdio::null()).stderr(Stdio::piped()); + let child = command + .spawn() + .map_err(|e| format!("Failed to start command: {e}"))?; + + let execution_output = child + .wait_with_output() + .map_err(|e| format!("Failed to wait for command: {e}"))?; + + if !execution_output.status.success() { + let msg = String::from_utf8_lossy(&execution_output.stderr); + let msg = msg.trim(); + + let mut err_msg = format!("Command failed with exit code: {}", execution_output.status); + if !msg.is_empty() { + err_msg = format!("{err_msg}\n{msg}"); + } + return Err(err_msg); } } + + Ok(()) } diff --git a/src/workspace/clean.rs b/src/workspace/clean.rs index cc1de27..37d8493 100644 --- a/src/workspace/clean.rs +++ b/src/workspace/clean.rs @@ -1,39 +1,41 @@ -use crate::{ctx::RunCtx, workspace::resolve::Workspace}; +use crate::{ctx::RunCtx, workspace::Workspace}; use std::fs; -/// Removes the build directory from the workspace. -/// # Errors -/// Returns an error if the build directory cannot be removed. -pub fn clean_workspace(workspace: &Workspace, ctx: &mut RunCtx<'_>) -> Result<(), String> { - if !workspace.build_path.exists() { +impl Workspace { + /// Removes the build directory. + /// # Errors + /// Returns an error if the build directory cannot be removed. + pub fn clean(&self, ctx: &mut RunCtx<'_, '_>) -> Result<(), String> { + if !self.build_path.exists() { + writeln!( + ctx.io.output, + "Build directory does not exist: {}", + self.build_path.display() + ) + .ok(); + return Ok(()); + } + writeln!( ctx.io.output, - "Build directory does not exist: {}", - workspace.build_path.display() + "Removing build directory: {}", + self.build_path.display() ) .ok(); - return Ok(()); - } - writeln!( - ctx.io.output, - "Removing build directory: {}", - workspace.build_path.display() - ) - .ok(); + if ctx.io.dry_run { + writeln!( + ctx.io.output, + "Would remove directory: {}", + self.build_path.display() + ) + .ok(); + } else { + fs::remove_dir_all(&self.build_path) + .map_err(|e| format!("Failed to remove build directory: {e}"))?; + writeln!(ctx.io.output, "Done").ok(); + } - if ctx.io.dry_run { - writeln!( - ctx.io.output, - "Would remove directory: {}", - workspace.build_path.display() - ) - .ok(); - } else { - fs::remove_dir_all(&workspace.build_path) - .map_err(|e| format!("Failed to remove build directory: {e}"))?; - writeln!(ctx.io.output, "Done").ok(); + Ok(()) } - - Ok(()) } diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index 1558217..04fcc01 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -1,8 +1,7 @@ -pub mod clean; +pub mod types; +pub use types::Workspace; pub mod resolve; +pub use resolve::resolve_workspace; +pub mod clean; pub mod status; pub mod update; -pub use clean::clean_workspace; -pub use resolve::{resolve_workspace, Workspace}; -pub use status::status_workspace; -pub use update::update_workspace; diff --git a/src/workspace/resolve.rs b/src/workspace/resolve.rs index 5836afc..44821d4 100644 --- a/src/workspace/resolve.rs +++ b/src/workspace/resolve.rs @@ -1,20 +1,5 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; - -/// A resolved workspace with paths to key directories and cloned repos. -#[derive(Debug)] -pub struct Workspace { - /// Workspace root directory (e.g. `./build-mono`). - pub root: PathBuf, - /// Path to the cloned repositories directory. - pub repos_path: PathBuf, - /// Path to the build output directory. - pub build_path: PathBuf, - /// Paths to each cloned repository directory. - pub repo_dirs: Vec, -} +use crate::workspace::Workspace; +use std::{fs, path::Path}; /// Resolves a workspace from optional path overrides. /// # Errors diff --git a/src/workspace/status.rs b/src/workspace/status.rs index 0b209aa..0b24b84 100644 --- a/src/workspace/status.rs +++ b/src/workspace/status.rs @@ -1,68 +1,66 @@ -use crate::{ctx::RunCtx, workspace::resolve::Workspace}; +use crate::{ctx::RunCtx, workspace::Workspace}; -/// Shows the status of all repositories in the workspace. -/// # Errors -/// Returns an error if any git command fails. -pub fn status_workspace( - workspace: &Workspace, - fetch: bool, - ctx: &mut RunCtx<'_>, -) -> Result<(), String> { - writeln!(ctx.io.output, "Workspace status:\n").ok(); +impl Workspace { + /// Shows the status of all repositories. + /// # Errors + /// Returns an error if any git command fails. + pub fn status(&self, fetch: bool, ctx: &mut RunCtx<'_, '_>) -> Result<(), String> { + writeln!(ctx.io.output, "Workspace status:\n").ok(); - for repo_dir in &workspace.repo_dirs { - let name = repo_dir - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_default(); + for repo_dir in &self.repo_dirs { + let name = repo_dir + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); - if fetch { - ctx - .runner - .run(&["git", "fetch"], Some(repo_dir), &mut ctx.io)?; - } - - let branch = ctx - .runner - .run_capture( - &["git", "rev-parse", "--abbrev-ref", "HEAD"], - Some(repo_dir), - ) - .unwrap_or_else(|_| "(unknown)".to_string()); + if fetch { + ctx + .runner + .run(&["git", "fetch"], Some(repo_dir), &mut ctx.io)?; + } - let dirty = !ctx - .runner - .run_capture(&["git", "status", "--porcelain"], Some(repo_dir))? - .is_empty(); - - let status_str = if dirty { "dirty" } else { "clean" }; - - let ahead_behind = if fetch { - let ahead = ctx + let branch = ctx .runner .run_capture( - &["git", "rev-list", "--count", "@{u}..HEAD"], + &["git", "rev-parse", "--abbrev-ref", "HEAD"], Some(repo_dir), ) - .unwrap_or_else(|_| "?".to_string()); - let behind = ctx + .unwrap_or_else(|_| "(unknown)".to_string()); + + let dirty = !ctx .runner - .run_capture( - &["git", "rev-list", "--count", "HEAD..@{u}"], - Some(repo_dir), - ) - .unwrap_or_else(|_| "?".to_string()); - format!(" ↑{ahead} ↓{behind}") - } else { - String::new() - }; + .run_capture(&["git", "status", "--porcelain"], Some(repo_dir))? + .is_empty(); - writeln!( - ctx.io.output, - " {name:<20} {branch:<12} {status_str}{ahead_behind}" - ) - .ok(); - } + let status_str = if dirty { "dirty" } else { "clean" }; + + let ahead_behind = if fetch { + let ahead = ctx + .runner + .run_capture( + &["git", "rev-list", "--count", "@{u}..HEAD"], + Some(repo_dir), + ) + .unwrap_or_else(|_| "?".to_string()); + let behind = ctx + .runner + .run_capture( + &["git", "rev-list", "--count", "HEAD..@{u}"], + Some(repo_dir), + ) + .unwrap_or_else(|_| "?".to_string()); + format!(" ↑{ahead} ↓{behind}") + } else { + String::new() + }; - Ok(()) + writeln!( + ctx.io.output, + " {name:<20} {branch:<12} {status_str}{ahead_behind}" + ) + .ok(); + } + + Ok(()) + } } diff --git a/src/workspace/types.rs b/src/workspace/types.rs new file mode 100644 index 0000000..ca99003 --- /dev/null +++ b/src/workspace/types.rs @@ -0,0 +1,14 @@ +use std::path::PathBuf; + +/// A resolved workspace with paths to key directories and cloned repos. +#[derive(Debug)] +pub struct Workspace { + /// Workspace root directory (e.g. `./build-mono`). + pub root: PathBuf, + /// Path to the cloned repositories directory. + pub repos_path: PathBuf, + /// Path to the build output directory. + pub build_path: PathBuf, + /// Paths to each cloned repository directory. + pub repo_dirs: Vec, +} diff --git a/src/workspace/update.rs b/src/workspace/update.rs index c71c575..c49f749 100644 --- a/src/workspace/update.rs +++ b/src/workspace/update.rs @@ -1,42 +1,41 @@ -use crate::{ctx::RunCtx, workspace::resolve::Workspace}; +use crate::{ctx::RunCtx, repository::pull_repository, workspace::Workspace}; -/// Pulls latest changes for all repositories in the workspace. -/// # Errors -/// Returns an error if any `git pull` command fails. -pub fn update_workspace(workspace: &Workspace, ctx: &mut RunCtx<'_>) -> Result<(), String> { - writeln!( - ctx.io.output, - "Updating {} repositories\n", - workspace.repo_dirs.len() - ) - .ok(); +impl Workspace { + /// Pulls latest changes for all repositories. + /// # Errors + /// Returns an error if any `git pull` command fails. + pub fn update(&self, ctx: &mut RunCtx<'_, '_>) -> Result<(), String> { + writeln!( + ctx.io.output, + "Updating {} repositories\n", + self.repo_dirs.len() + ) + .ok(); - let mut errors: Vec = Vec::new(); + let mut errors: Vec = Vec::new(); - for repo_dir in &workspace.repo_dirs { - let name = repo_dir - .file_name() - .map(|n| n.to_string_lossy()) - .unwrap_or_default(); + for repo_dir in &self.repo_dirs { + let name = repo_dir + .file_name() + .map(|n| n.to_string_lossy()) + .unwrap_or_default(); - writeln!(ctx.io.output, " Updating {name}").ok(); - if let Err(e) = ctx - .runner - .run(&["git", "pull"], Some(repo_dir), &mut ctx.io) - { - writeln!(ctx.io.output, " Failed to update {name}: {e}").ok(); - errors.push(format!("{name}: {e}")); + writeln!(ctx.io.output, " Updating {name}").ok(); + if let Err(e) = pull_repository(repo_dir, ctx) { + writeln!(ctx.io.output, " Failed to update {name}: {e}").ok(); + errors.push(format!("{name}: {e}")); + } } - } - if errors.is_empty() { - writeln!(ctx.io.output, "\nDone").ok(); - Ok(()) - } else { - Err(format!( - "{} repository(s) failed to update:\n{}", - errors.len(), - errors.join("\n") - )) + if errors.is_empty() { + writeln!(ctx.io.output, "\nDone").ok(); + Ok(()) + } else { + Err(format!( + "{} repository(s) failed to update:\n{}", + errors.len(), + errors.join("\n") + )) + } } } diff --git a/tests/cli.rs b/tests/cli.rs index e7b2e01..6003801 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,6 +1,7 @@ -#[path = "cli/build.rs"] -mod build; #[path = "common/mod.rs"] mod common; + +#[path = "cli/build.rs"] +mod build; #[path = "cli/resolve.rs"] mod resolve; diff --git a/tests/cli/build/detect.rs b/tests/cli/build/detect.rs index 54d0001..7f71470 100644 --- a/tests/cli/build/detect.rs +++ b/tests/cli/build/detect.rs @@ -2,240 +2,145 @@ use star_setup::{ cli::{detect_build_system, detect_mono_build_system, BuildSystem}, ctx::{IoCtx, ProcessRunner, RunCtx}, }; -use tempfile::TempDir; -fn cmake_dir() -> TempDir { - let tmp = TempDir::new().unwrap(); - std::fs::write(tmp.path().join("CMakeLists.txt"), "").unwrap(); - tmp +fn create_cmake_fixture(path: &std::path::Path) { + std::fs::write(path.join("CMakeLists.txt"), "").unwrap(); } -fn meson_dir() -> TempDir { - let tmp = TempDir::new().unwrap(); - std::fs::write(tmp.path().join("meson.build"), "").unwrap(); - tmp +fn create_meson_fixture(path: &std::path::Path) { + std::fs::write(path.join("meson.build"), "").unwrap(); } -#[test] -fn test_detect_build_system_none() { - let dir = TempDir::new().unwrap(); +fn with_detect_ctx(input: &[u8], timing: bool, test_logic: F) -> (T, String) +where + F: FnOnce(&std::path::Path, &mut RunCtx) -> T, +{ + let tmp = tempfile::TempDir::new().unwrap(); let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, + let mut output = Vec::new(); + let mut input_slice = input; + + let result = { + let mut ctx = RunCtx { + io: IoCtx { + input: &mut input_slice, + output: &mut output, + verbose: false, + timing, + dry_run: false, + }, + runner: &mut runner, + }; + test_logic(tmp.path(), &mut ctx) }; - let result = detect_build_system(dir.path(), &mut ctx); + + (result, String::from_utf8(output).unwrap()) +} + +#[test] +fn test_detect_build_system_none() { + let (result, _) = with_detect_ctx(b"", false, detect_build_system); assert!(result.is_err()); } #[test] fn test_detect_build_system_cmake() { - let dir = cmake_dir(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = detect_build_system(dir.path(), &mut ctx).unwrap(); - assert!(matches!(result, BuildSystem::Cmake)); + let (result, _) = with_detect_ctx(b"", false, |path, ctx| { + create_cmake_fixture(path); + detect_build_system(path, ctx) + }); + assert!(matches!(result.unwrap(), BuildSystem::Cmake)); } #[test] fn test_detect_build_system_meson() { - let dir = meson_dir(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = detect_build_system(dir.path(), &mut ctx).unwrap(); - assert!(matches!(result, BuildSystem::Meson)); + let (result, _) = with_detect_ctx(b"", false, |path, ctx| { + create_meson_fixture(path); + detect_build_system(path, ctx) + }); + assert!(matches!(result.unwrap(), BuildSystem::Meson)); } #[test] fn test_detect_build_system_both_picks_cmake() { - let dir = cmake_dir(); - std::fs::write(dir.path().join("meson.build"), "").unwrap(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"1\n".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = detect_build_system(dir.path(), &mut ctx).unwrap(); - assert!(matches!(result, BuildSystem::Cmake)); + let (result, _) = with_detect_ctx(b"1\n", false, |path, ctx| { + create_cmake_fixture(path); + create_meson_fixture(path); + detect_build_system(path, ctx) + }); + assert!(matches!(result.unwrap(), BuildSystem::Cmake)); } #[test] fn test_detect_build_system_both_picks_meson() { - let dir = cmake_dir(); - std::fs::write(dir.path().join("meson.build"), "").unwrap(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"2\n".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = detect_build_system(dir.path(), &mut ctx).unwrap(); - assert!(matches!(result, BuildSystem::Meson)); + let (result, _) = with_detect_ctx(b"2\n", false, |path, ctx| { + create_cmake_fixture(path); + create_meson_fixture(path); + detect_build_system(path, ctx) + }); + assert!(matches!(result.unwrap(), BuildSystem::Meson)); } #[test] fn test_detect_build_system_timing_output() { - let dir = cmake_dir(); - let mut output = Vec::new(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"".as_ref(), - output: &mut output, - verbose: false, - timing: true, - dry_run: false, - }, - runner: &mut runner, - }; - detect_build_system(dir.path(), &mut ctx).unwrap(); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_detect_ctx(b"", true, |path, ctx| { + create_cmake_fixture(path); + detect_build_system(path, ctx).unwrap(); + }); assert!(out.contains("[timing] Detect:")); } #[test] -fn test_detect_mono_build_system_cmake() { - let dir = cmake_dir(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = detect_mono_build_system(&[dir.path().to_path_buf()], &mut ctx).unwrap(); - assert!(matches!(result, BuildSystem::Cmake)); +fn test_detect_mono_build_system_none() { + let (result, _) = with_detect_ctx(b"", false, |path, ctx| { + detect_mono_build_system(&[path.to_path_buf()], ctx) + }); + assert!(result.is_err()); } #[test] -fn test_detect_mono_build_system_meson() { - let dir = meson_dir(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = detect_mono_build_system(&[dir.path().to_path_buf()], &mut ctx).unwrap(); - assert!(matches!(result, BuildSystem::Meson)); +fn test_detect_mono_build_system_cmake() { + let (result, _) = with_detect_ctx(b"", false, |path, ctx| { + create_cmake_fixture(path); + detect_mono_build_system(&[path.to_path_buf()], ctx) + }); + assert!(matches!(result.unwrap(), BuildSystem::Cmake)); } #[test] -fn test_detect_mono_build_system_none() { - let dir = TempDir::new().unwrap(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = detect_mono_build_system(&[dir.path().to_path_buf()], &mut ctx); - assert!(result.is_err()); +fn test_detect_mono_build_system_meson() { + let (result, _) = with_detect_ctx(b"", false, |path, ctx| { + create_meson_fixture(path); + detect_mono_build_system(&[path.to_path_buf()], ctx) + }); + assert!(matches!(result.unwrap(), BuildSystem::Meson)); } #[test] fn test_detect_mono_build_system_both_picks_cmake() { - let dir = cmake_dir(); - std::fs::write(dir.path().join("meson.build"), "").unwrap(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"1\n".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = detect_mono_build_system(&[dir.path().to_path_buf()], &mut ctx).unwrap(); - assert!(matches!(result, BuildSystem::Cmake)); + let (result, _) = with_detect_ctx(b"1\n", false, |path, ctx| { + create_cmake_fixture(path); + create_meson_fixture(path); + detect_mono_build_system(&[path.to_path_buf()], ctx) + }); + assert!(matches!(result.unwrap(), BuildSystem::Cmake)); } #[test] fn test_detect_mono_build_system_both_picks_meson() { - let dir = cmake_dir(); - std::fs::write(dir.path().join("meson.build"), "").unwrap(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"2\n".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = detect_mono_build_system(&[dir.path().to_path_buf()], &mut ctx).unwrap(); - assert!(matches!(result, BuildSystem::Meson)); + let (result, _) = with_detect_ctx(b"2\n", false, |path, ctx| { + create_cmake_fixture(path); + create_meson_fixture(path); + detect_mono_build_system(&[path.to_path_buf()], ctx) + }); + assert!(matches!(result.unwrap(), BuildSystem::Meson)); } #[test] fn test_detect_mono_build_system_timing_output() { - let dir = cmake_dir(); - let mut output = Vec::new(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"".as_ref(), - output: &mut output, - verbose: false, - timing: true, - dry_run: false, - }, - runner: &mut runner, - }; - detect_mono_build_system(&[dir.path().to_path_buf()], &mut ctx).unwrap(); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_detect_ctx(b"", true, |path, ctx| { + create_cmake_fixture(path); + detect_mono_build_system(&[path.to_path_buf()], ctx).unwrap(); + }); assert!(out.contains("[timing] Detect:")); } diff --git a/tests/cli/build/types.rs b/tests/cli/build/types.rs index bd366c6..e722134 100644 --- a/tests/cli/build/types.rs +++ b/tests/cli/build/types.rs @@ -1,4 +1,5 @@ -use star_setup::cli::BuildType; +use star_setup::cli::{BuildSystem, BuildType}; +use std::str::FromStr; #[test] fn test_to_cmake_all_variants() { @@ -18,48 +19,50 @@ fn test_to_meson_all_variants() { #[test] fn test_from_str_all_variants() { - use std::str::FromStr; - assert_eq!(BuildType::from_str("debug").unwrap(), BuildType::Debug); - assert_eq!(BuildType::from_str("release").unwrap(), BuildType::Release); - assert_eq!( - BuildType::from_str("rel-with-deb-info").unwrap(), - BuildType::RelWithDebInfo - ); - assert_eq!( - BuildType::from_str("relwithdebinfo").unwrap(), - BuildType::RelWithDebInfo - ); - assert_eq!( - BuildType::from_str("debugoptimized").unwrap(), - BuildType::RelWithDebInfo - ); - assert_eq!( - BuildType::from_str("min-size-rel").unwrap(), - BuildType::MinSizeRel - ); - assert_eq!( - BuildType::from_str("minsizerel").unwrap(), - BuildType::MinSizeRel - ); - assert_eq!( - BuildType::from_str("minsize").unwrap(), - BuildType::MinSizeRel - ); + let cases = [ + ("debug", BuildType::Debug), + ("release", BuildType::Release), + ("rel-with-deb-info", BuildType::RelWithDebInfo), + ("relwithdebinfo", BuildType::RelWithDebInfo), + ("debugoptimized", BuildType::RelWithDebInfo), + ("min-size-rel", BuildType::MinSizeRel), + ("minsizerel", BuildType::MinSizeRel), + ("minsize", BuildType::MinSizeRel), + ]; + + for (input, expected) in cases { + assert_eq!( + BuildType::from_str(input).unwrap(), + expected, + "Failed on input: {input}" + ); + } } #[test] fn test_from_str_error() { - use std::str::FromStr; assert!(BuildType::from_str("unknown").is_err()); } #[test] fn test_build_system_from_str() { - use star_setup::cli::BuildSystem; - use std::str::FromStr; - assert_eq!(BuildSystem::from_str("cmake").unwrap(), BuildSystem::Cmake); - assert_eq!(BuildSystem::from_str("CMAKE").unwrap(), BuildSystem::Cmake); - assert_eq!(BuildSystem::from_str("meson").unwrap(), BuildSystem::Meson); - assert_eq!(BuildSystem::from_str("MESON").unwrap(), BuildSystem::Meson); - assert!(BuildSystem::from_str("ninja").is_err()); + let success_cases = [ + ("cmake", BuildSystem::Cmake), + ("CMAKE", BuildSystem::Cmake), + ("meson", BuildSystem::Meson), + ("MESON", BuildSystem::Meson), + ]; + + for (input, expected) in success_cases { + assert_eq!( + BuildSystem::from_str(input).unwrap(), + expected, + "Failed on valid input: {input}" + ); + } + + assert!( + BuildSystem::from_str("ninja").is_err(), + "Expected error for invalid input: ninja" + ); } diff --git a/tests/cli/resolve.rs b/tests/cli/resolve.rs index 708e1a5..a182764 100644 --- a/tests/cli/resolve.rs +++ b/tests/cli/resolve.rs @@ -4,6 +4,30 @@ use star_setup::{ config::{ConfigEntry, SetupConfig}, }; +/// Generates a base `ConfigEntry` with defaults. +fn create_test_config_entry() -> ConfigEntry { + ConfigEntry { + ssh: false, + verbose: false, + build_type: BuildType::Debug, + build_dir: "build".to_string(), + mono_dir: "build-mono".to_string(), + no_build: false, + clean: false, + timing: false, + dry_run: false, + cmake_flags: vec![], + meson_flags: vec![], + } +} + +/// Helper to quickly build a `SetupConfig` with a populated profile entry. +fn config_with_entry(name: &str, entry: ConfigEntry) -> SetupConfig { + let mut config = SetupConfig::new(); + config.configs.insert(name.to_string(), entry); + config +} + #[test] fn test_resolve_bool() { #[allow(clippy::struct_excessive_bools)] @@ -85,23 +109,20 @@ fn test_resolve_with_config_defaults_when_no_config() { #[test] fn test_resolve_with_config_applies_config_defaults() { - let mut config = SetupConfig::new(); - config.configs.insert( - "default".to_string(), + let config = config_with_entry( + "default", ConfigEntry { ssh: true, verbose: true, build_type: BuildType::Release, build_dir: "out".to_string(), - mono_dir: "mono".to_string(), no_build: true, clean: true, - timing: false, - dry_run: false, cmake_flags: vec!["-DTEST=ON".to_string()], - meson_flags: vec![], + ..create_test_config_entry() }, ); + let resolved = resolve_with_config(default_args(), &config).unwrap(); assert!(resolved.connection.ssh); assert!(resolved.connection.verbose); @@ -114,26 +135,12 @@ fn test_resolve_with_config_applies_config_defaults() { #[test] fn test_resolve_with_config_cli_overrides_config() { - let mut config = SetupConfig::new(); - config.configs.insert( - "default".to_string(), - ConfigEntry { - ssh: false, - verbose: false, - build_type: BuildType::Debug, - build_dir: "build".to_string(), - mono_dir: "build-mono".to_string(), - no_build: false, - clean: false, - timing: false, - dry_run: false, - cmake_flags: vec![], - meson_flags: vec![], - }, - ); + let config = config_with_entry("default", create_test_config_entry()); + let mut args = default_args(); args.connection.ssh = true; args.build.build_type = Some("Release".to_string()); + let resolved = resolve_with_config(args, &config).unwrap(); assert!(resolved.connection.ssh); assert_eq!(resolved.build.build_type, BuildType::Release); @@ -144,8 +151,8 @@ fn test_resolve_with_config_errors_on_missing_config_name() { let config = SetupConfig::new(); let mut args = default_args(); args.config_name = Some("nonexistent".to_string()); - let result = resolve_with_config(args, &config); - assert!(result.is_err()); + + assert!(resolve_with_config(args, &config).is_err()); } #[test] @@ -153,6 +160,7 @@ fn test_resolve_with_config_mono_repo_from_repos() { let config = SetupConfig::new(); let mut args = default_args(); args.mono.repos = Some(vec!["user/lib1".to_string()]); + let resolved = resolve_with_config(args, &config).unwrap(); assert!(resolved.mono.mono_repo); } @@ -162,31 +170,27 @@ fn test_resolve_with_config_mono_repo_from_profile() { let config = SetupConfig::new(); let mut args = default_args(); args.mono.profile = Some("myprofile".to_string()); + let resolved = resolve_with_config(args, &config).unwrap(); assert!(resolved.mono.mono_repo); } #[test] fn test_resolve_with_config_named_config_pulls_correct_values() { - let mut config = SetupConfig::new(); - config.configs.insert( - "myconfig".to_string(), + let config = config_with_entry( + "myconfig", ConfigEntry { ssh: true, - verbose: false, build_type: BuildType::RelWithDebInfo, build_dir: "out".to_string(), - mono_dir: "mono".to_string(), - no_build: false, clean: true, - timing: false, - dry_run: false, - cmake_flags: vec![], - meson_flags: vec![], + ..create_test_config_entry() }, ); + let mut args = default_args(); args.config_name = Some("myconfig".to_string()); + let resolved = resolve_with_config(args, &config).unwrap(); assert!(resolved.connection.ssh); assert_eq!(resolved.build.build_type, BuildType::RelWithDebInfo); @@ -196,54 +200,39 @@ fn test_resolve_with_config_named_config_pulls_correct_values() { #[test] fn test_resolve_with_config_cli_cmake_flags_not_overwritten_by_config() { - let mut config = SetupConfig::new(); - config.configs.insert( - "default".to_string(), + let config = config_with_entry( + "default", ConfigEntry { - ssh: false, - verbose: false, - build_type: BuildType::Debug, - build_dir: "build".to_string(), - mono_dir: "build-mono".to_string(), - no_build: false, - clean: false, - timing: false, - dry_run: false, cmake_flags: vec!["-DCONFIG_FLAG=ON".to_string()], - meson_flags: vec![], + ..create_test_config_entry() }, ); + let mut args = default_args(); args.build.cmake_flags = vec!["-DCLI_FLAG=ON".to_string()]; + let resolved = resolve_with_config(args, &config).unwrap(); assert_eq!(resolved.build.cmake_flags, vec!["-DCLI_FLAG=ON"]); } #[test] fn test_resolve_with_config_negative_flags_override_config() { - let mut config = SetupConfig::new(); - config.configs.insert( - "default".to_string(), + let config = config_with_entry( + "default", ConfigEntry { ssh: true, verbose: true, - build_type: BuildType::Debug, - build_dir: "build".to_string(), - mono_dir: "build-mono".to_string(), no_build: true, clean: true, - timing: false, - dry_run: false, - cmake_flags: vec![], - meson_flags: vec![], + ..create_test_config_entry() }, ); let mut args = default_args(); - args.connection.https = true; // negates ssh - args.connection.no_verbose = true; // negates verbose - args.build.build = true; // negates no_build - args.build.no_clean = true; // negates clean + args.connection.https = true; + args.connection.no_verbose = true; + args.build.build = true; + args.build.no_clean = true; let resolved = resolve_with_config(args, &config).unwrap(); assert!( diff --git a/tests/commands.rs b/tests/commands.rs index 23d9dbd..9aa7a0e 100644 --- a/tests/commands.rs +++ b/tests/commands.rs @@ -1,7 +1,8 @@ -#[path = "commands/build.rs"] -mod build; #[path = "common/mod.rs"] mod common; + +#[path = "commands/build.rs"] +mod build; #[path = "commands/display.rs"] mod display; #[path = "commands/header.rs"] diff --git a/tests/commands/build.rs b/tests/commands/build.rs index 9c7b591..89fd791 100644 --- a/tests/commands/build.rs +++ b/tests/commands/build.rs @@ -1,78 +1,44 @@ -use super::common::{default_resolved_with_no_build, empty_input, make_io, sink, MockRunner}; +use super::common::{default_resolved_with_no_build, with_runner_ctx, MockRunner}; use star_setup::{ cli::BuildSystem, commands::{build_project, cmake_build, meson_build}, - ctx::RunCtx, }; -use tempfile::TempDir; #[test] fn test_cmake_build_configure_only() { - let tmp = TempDir::new().unwrap(); let args = default_resolved_with_no_build(true); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - cmake_build(&args, tmp.path(), false, &mut ctx).unwrap(); - + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + cmake_build(&args, tmp_path, false, ctx).unwrap(); + }); assert_eq!(runner.calls.len(), 1); assert!(runner.calls[0].0.contains(&"cmake".to_string())); } #[test] fn test_cmake_build_with_build_step() { - let tmp = TempDir::new().unwrap(); let args = default_resolved_with_no_build(false); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - cmake_build(&args, tmp.path(), false, &mut ctx).unwrap(); - + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + cmake_build(&args, tmp_path, false, ctx).unwrap(); + }); assert_eq!(runner.calls.len(), 2); assert!(runner.calls[1].0.contains(&"--build".to_string())); } #[test] fn test_cmake_build_mono_flag() { - let tmp = TempDir::new().unwrap(); let args = default_resolved_with_no_build(true); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - cmake_build(&args, tmp.path(), true, &mut ctx).unwrap(); - + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + cmake_build(&args, tmp_path, true, ctx).unwrap(); + }); assert!(runner.calls[0].0.contains(&"-DBUILD_LOCAL=ON".to_string())); } #[test] fn test_meson_build_configure_only() { - let tmp = TempDir::new().unwrap(); let args = default_resolved_with_no_build(true); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - meson_build(&args, tmp.path(), tmp.path(), &mut ctx).unwrap(); - + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + meson_build(&args, tmp_path, tmp_path, ctx).unwrap(); + }); assert_eq!(runner.calls.len(), 1); assert!(runner.calls[0].0.contains(&"meson".to_string())); assert!(runner.calls[0].0.contains(&"setup".to_string())); @@ -80,68 +46,28 @@ fn test_meson_build_configure_only() { #[test] fn test_meson_build_with_build_step() { - let tmp = TempDir::new().unwrap(); let args = default_resolved_with_no_build(false); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - meson_build(&args, tmp.path(), tmp.path(), &mut ctx).unwrap(); - + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + meson_build(&args, tmp_path, tmp_path, ctx).unwrap(); + }); assert_eq!(runner.calls.len(), 2); assert!(runner.calls[1].0.contains(&"compile".to_string())); } #[test] fn test_build_project_dispatches_cmake() { - let tmp = TempDir::new().unwrap(); let args = default_resolved_with_no_build(true); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - build_project( - &args, - tmp.path(), - tmp.path(), - BuildSystem::Cmake, - false, - &mut ctx, - ) - .unwrap(); - + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + build_project(&args, tmp_path, tmp_path, BuildSystem::Cmake, false, ctx).unwrap(); + }); assert!(runner.calls[0].0.contains(&"cmake".to_string())); } #[test] fn test_build_project_dispatches_meson() { - let tmp = TempDir::new().unwrap(); let args = default_resolved_with_no_build(true); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - build_project( - &args, - tmp.path(), - tmp.path(), - BuildSystem::Meson, - false, - &mut ctx, - ) - .unwrap(); - + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + build_project(&args, tmp_path, tmp_path, BuildSystem::Meson, false, ctx).unwrap(); + }); assert!(runner.calls[0].0.contains(&"meson".to_string())); } diff --git a/tests/commands/display.rs b/tests/commands/display.rs index 8c02962..6712288 100644 --- a/tests/commands/display.rs +++ b/tests/commands/display.rs @@ -1,65 +1,56 @@ -use super::common::{empty_input, make_io, sink}; +use crate::common::{with_io_dir, with_io_input_output}; use star_setup::commands::mono::display::{print_setup_complete, resolve_setup_paths}; use std::collections::HashMap; -use tempfile::TempDir; #[test] fn test_print_setup_complete_no_map() { - let tmp = TempDir::new().unwrap(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - let paths = resolve_setup_paths( - None::<&HashMap>, - tmp.path(), - &tmp.path().join("build"), - "user/repo", - ); - print_setup_complete(&paths, std::time::Instant::now(), &mut io); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_input_output(b"", |io| { + with_io_dir(|tmp_path, _| { + let paths = resolve_setup_paths( + None::<&HashMap>, + tmp_path, + &tmp_path.join("build"), + "user/repo", + ); + print_setup_complete(&paths, std::time::Instant::now(), io); + }); + }); + assert!(out.contains("Setup complete")); assert!(out.contains("Build output in:")); } #[test] fn test_print_setup_complete_with_map() { - let tmp = TempDir::new().unwrap(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - let mut map = HashMap::new(); - map.insert("my_lib".to_string(), "user-repo".to_string()); - let paths = resolve_setup_paths( - Some(&map), - tmp.path(), - &tmp.path().join("build"), - "user/repo", - ); - print_setup_complete(&paths, std::time::Instant::now(), &mut io); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_input_output(b"", |io| { + with_io_dir(|tmp_path, _| { + let mut map = HashMap::new(); + map.insert("my_lib".to_string(), "user-repo".to_string()); + + let paths = resolve_setup_paths(Some(&map), tmp_path, &tmp_path.join("build"), "user/repo"); + print_setup_complete(&paths, std::time::Instant::now(), io); + }); + }); + assert!(out.contains("Setup complete")); assert!(out.contains("Executable:")); } #[test] fn test_print_setup_complete_timing() { - let tmp = TempDir::new().unwrap(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = star_setup::ctx::IoCtx { - input: &mut input, - output: &mut output, - verbose: false, - timing: true, - dry_run: false, - }; - let paths = resolve_setup_paths( - None::<&HashMap>, - tmp.path(), - &tmp.path().join("build"), - "user/repo", - ); - print_setup_complete(&paths, std::time::Instant::now(), &mut io); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_input_output(b"", |io| { + io.timing = true; + + with_io_dir(|tmp_path, _| { + let paths = resolve_setup_paths( + None::<&HashMap>, + tmp_path, + &tmp_path.join("build"), + "user/repo", + ); + print_setup_complete(&paths, std::time::Instant::now(), io); + }); + }); + assert!(out.contains("[timing] Total:")); } diff --git a/tests/commands/header.rs b/tests/commands/header.rs index d2809a1..8250845 100644 --- a/tests/commands/header.rs +++ b/tests/commands/header.rs @@ -1,23 +1,22 @@ -use super::common::{empty_input, make_io, sink}; +use super::common::with_io_input_output; use star_setup::commands::{print_mode_header, ModeHeader}; #[test] fn test_print_mode_header_repo_name_without_test_repo() { - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - print_mode_header( - &ModeHeader { - mode: "Single Repository Mode", - test_repo: None, - repo_name: Some("myrepo"), - use_ssh: false, - mono_dir: None, - profile: None, - lib_count: None, - }, - &mut io, - ); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_input_output(b"", |io| { + print_mode_header( + &ModeHeader { + mode: "Single Repository Mode", + test_repo: None, + repo_name: Some("myrepo"), + use_ssh: false, + mono_dir: None, + profile: None, + lib_count: None, + }, + io, + ); + }); + assert!(out.contains("Repository: myrepo")); } diff --git a/tests/commands/mono/clone.rs b/tests/commands/mono/clone.rs index c64908b..e91a02e 100644 --- a/tests/commands/mono/clone.rs +++ b/tests/commands/mono/clone.rs @@ -1,21 +1,12 @@ -use super::super::common::{empty_input, make_io, sink, MockRunner}; +use super::super::common::{with_runner_ctx, MockRunner}; use star_setup::commands::mono::clone::clone_mono_repos; -use star_setup::ctx::RunCtx; -use tempfile::TempDir; #[test] fn test_clone_mono_repos_calls_clone_for_each_repo() { - let tmp = TempDir::new().unwrap(); - let repos = vec!["user/repo1".to_string(), "user/repo2".to_string()]; - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - clone_mono_repos(&repos, tmp.path(), false, &mut ctx).unwrap(); + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + let repos = vec!["user/repo1".to_string(), "user/repo2".to_string()]; + clone_mono_repos(&repos, tmp_path, false, ctx).unwrap(); + }); assert_eq!(runner.calls.len(), 2); assert!(runner @@ -26,16 +17,9 @@ fn test_clone_mono_repos_calls_clone_for_each_repo() { #[test] fn test_clone_mono_repos_empty() { - let tmp = TempDir::new().unwrap(); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - clone_mono_repos(&[], tmp.path(), false, &mut ctx).unwrap(); + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + clone_mono_repos(&[], tmp_path, false, ctx).unwrap(); + }); assert!(runner.calls.is_empty()); } diff --git a/tests/commands/mono/config.rs b/tests/commands/mono/config.rs index a5bfbd5..aac2886 100644 --- a/tests/commands/mono/config.rs +++ b/tests/commands/mono/config.rs @@ -1,69 +1,62 @@ -use super::super::common::{empty_input, make_io, sink}; +use crate::common::with_io_dir; use star_setup::commands::{create_mono_repo_cmakelists, create_mono_repo_mesonbuild}; // create_mono_repo_cmakelists tests #[test] fn test_create_mono_repo_cmakelists_creates_file() { - let tmp = tempfile::TempDir::new().unwrap(); + with_io_dir(|tmp_path, io| { + let repos = vec![ + "user-testrepo".to_string(), + "user/lib1".to_string(), + "user/lib2".to_string(), + ]; + create_mono_repo_cmakelists(tmp_path, &repos, io).unwrap(); - let repos = vec![ - "user-testrepo".to_string(), - "user/lib1".to_string(), - "user/lib2".to_string(), - ]; - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - create_mono_repo_cmakelists(tmp.path(), &repos, &mut io).unwrap(); + let cmake_file = tmp_path.join("CMakeLists.txt"); + assert!(cmake_file.exists()); - let cmake_file = tmp.path().join("CMakeLists.txt"); - assert!(cmake_file.exists()); - - let content = std::fs::read_to_string(&cmake_file).unwrap(); - assert!(content.contains("user-testrepo")); - assert!(content.contains("user-lib1")); - assert!(content.contains("user-lib2")); + let content = std::fs::read_to_string(&cmake_file).unwrap(); + assert!(content.contains("user-testrepo")); + assert!(content.contains("user-lib1")); + assert!(content.contains("user-lib2")); + }); } #[test] fn test_create_mono_repo_cmakelists_empty_repos() { - let tmp = tempfile::TempDir::new().unwrap(); - let repos = vec!["user-testrepo".to_string()]; - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - create_mono_repo_cmakelists(tmp.path(), &repos, &mut io).unwrap(); - assert!(tmp.path().join("CMakeLists.txt").exists()); + with_io_dir(|tmp_path, io| { + let repos = vec!["user-testrepo".to_string()]; + create_mono_repo_cmakelists(tmp_path, &repos, io).unwrap(); + assert!(tmp_path.join("CMakeLists.txt").exists()); + }); } // create_mono_repo_mesonbuild tests #[test] fn test_create_mono_repo_mesonbuild_creates_file() { - let tmp = tempfile::TempDir::new().unwrap(); - let repos = vec![ - "user-testrepo".to_string(), - "user/lib1".to_string(), - "user/lib2".to_string(), - ]; - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - create_mono_repo_mesonbuild(tmp.path(), &repos, &mut io).unwrap(); - let meson_file = tmp.path().join("meson.build"); - assert!(meson_file.exists()); - let content = std::fs::read_to_string(&meson_file).unwrap(); - assert!(content.contains("user-testrepo")); - assert!(content.contains("user-lib1")); - assert!(content.contains("user-lib2")); + with_io_dir(|tmp_path, io| { + let repos = vec![ + "user-testrepo".to_string(), + "user/lib1".to_string(), + "user/lib2".to_string(), + ]; + create_mono_repo_mesonbuild(tmp_path, &repos, io).unwrap(); + + let meson_file = tmp_path.join("meson.build"); + assert!(meson_file.exists()); + + let content = std::fs::read_to_string(&meson_file).unwrap(); + assert!(content.contains("user-testrepo")); + assert!(content.contains("user-lib1")); + assert!(content.contains("user-lib2")); + }); } #[test] fn test_create_mono_repo_mesonbuild_empty_repos() { - let tmp = tempfile::TempDir::new().unwrap(); - let repos = vec!["user-testrepo".to_string()]; - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - create_mono_repo_mesonbuild(tmp.path(), &repos, &mut io).unwrap(); - assert!(tmp.path().join("meson.build").exists()); + with_io_dir(|tmp_path, io| { + let repos = vec!["user-testrepo".to_string()]; + create_mono_repo_mesonbuild(tmp_path, &repos, io).unwrap(); + assert!(tmp_path.join("meson.build").exists()); + }); } diff --git a/tests/commands/mono/mode.rs b/tests/commands/mono/mode.rs index db00f31..b2fc868 100644 --- a/tests/commands/mono/mode.rs +++ b/tests/commands/mono/mode.rs @@ -1,10 +1,5 @@ -use crate::common::{default_resolved_mono, empty_input, make_io, sink, MockRunner}; -use star_setup::{ - commands::mono_repo_mode, - config::SetupConfig, - ctx::{DryRunRunner, RunCtx}, -}; -use tempfile::TempDir; +use crate::common::{default_resolved_mono, with_ctx, with_runner_ctx, MockRunner}; +use star_setup::{commands::mono_repo_mode, config::SetupConfig, ctx::DryRunRunner}; fn make_cmake_repo(repos_path: &std::path::Path, name: &str) { let dir = repos_path.join(name); @@ -14,23 +9,16 @@ fn make_cmake_repo(repos_path: &std::path::Path, name: &str) { #[test] fn test_mono_repo_mode_clones_and_configures() { - let tmp = TempDir::new().unwrap(); let args = default_resolved_mono(vec!["user/lib1".to_string()]); - let repos_path = tmp.path().join(&args.mono.mono_dir).join("repos"); - std::fs::create_dir_all(&repos_path).unwrap(); - make_cmake_repo(&repos_path, "user-lib1"); - make_cmake_repo(&repos_path, "user-test-repo"); + let (_, output) = with_ctx(MockRunner::new(), |tmp_path, ctx| { + let repos_path = tmp_path.join(&args.mono.mono_dir).join("repos"); + std::fs::create_dir_all(&repos_path).unwrap(); + make_cmake_repo(&repos_path, "user-lib1"); + make_cmake_repo(&repos_path, "user-test-repo"); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - mono_repo_mode(&args, &SetupConfig::new(), tmp.path(), &mut ctx).unwrap(); + mono_repo_mode(&args, &SetupConfig::new(), tmp_path, ctx).unwrap(); + }); let out = String::from_utf8(output).unwrap(); assert!(out.contains("Setup complete")); @@ -39,49 +27,31 @@ fn test_mono_repo_mode_clones_and_configures() { #[test] fn test_mono_repo_mode_dry_run_makes_no_fs_changes() { - let tmp = TempDir::new().unwrap(); let mut args = default_resolved_mono(vec!["user/lib1".to_string()]); args.diagnostic.dry_run = true; - let mut input = empty_input(); - let mut output = sink(); - let mut runner = DryRunRunner; - let mut ctx = RunCtx { - io: star_setup::ctx::IoCtx { - input: &mut input, - output: &mut output, - verbose: false, - timing: false, - dry_run: true, - }, - runner: &mut runner, - }; + with_ctx(DryRunRunner, |tmp_path, ctx| { + ctx.io.dry_run = true; - mono_repo_mode(&args, &SetupConfig::new(), tmp.path(), &mut ctx).unwrap(); + mono_repo_mode(&args, &SetupConfig::new(), tmp_path, ctx).unwrap(); - assert!(std::fs::read_dir(tmp.path()).unwrap().next().is_none()); + assert!(std::fs::read_dir(tmp_path).unwrap().next().is_none()); + }); } #[test] fn test_mono_repo_mode_with_build_system_flag() { - let tmp = TempDir::new().unwrap(); let mut args = default_resolved_mono(vec!["user/lib1".to_string()]); args.build.build_system = Some(star_setup::cli::BuildSystem::Cmake); - let repos_path = tmp.path().join(&args.mono.mono_dir).join("repos"); - std::fs::create_dir_all(&repos_path).unwrap(); - make_cmake_repo(&repos_path, "user-lib1"); - make_cmake_repo(&repos_path, "user-test-repo"); - - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; + let runner = with_runner_ctx(MockRunner::new(), |tmp_path, ctx| { + let repos_path = tmp_path.join(&args.mono.mono_dir).join("repos"); + std::fs::create_dir_all(&repos_path).unwrap(); + make_cmake_repo(&repos_path, "user-lib1"); + make_cmake_repo(&repos_path, "user-test-repo"); - mono_repo_mode(&args, &SetupConfig::new(), tmp.path(), &mut ctx).unwrap(); + mono_repo_mode(&args, &SetupConfig::new(), tmp_path, ctx).unwrap(); + }); assert!(runner.calls.iter().any(|(cmd, _)| cmd[0] == "cmake")); } diff --git a/tests/commands/mono/resolve.rs b/tests/commands/mono/resolve.rs index 2500d3c..e129fd9 100644 --- a/tests/commands/mono/resolve.rs +++ b/tests/commands/mono/resolve.rs @@ -1,10 +1,8 @@ -use crate::common::{default_resolved, empty_input, make_io, sink, MockRunner}; +use crate::common::{default_resolved, with_ctx, with_io, MockRunner}; use star_setup::{ commands::{mono::generate_mono_config, resolve_repos_for_mono, resolve_test_repo}, config::SetupConfig, - ctx::RunCtx, }; -use tempfile::TempDir; // resolve_test_repo tests #[test] @@ -54,13 +52,11 @@ fn test_resolve_repos_for_mono_empty_profile_errors() { let mut args = default_resolved(); args.mono.profile = Some("emptyprofile".to_string()); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - let result = resolve_repos_for_mono(&args, &config, "user/repo", &mut io); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("has no repositories")); + with_io(|io| { + let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("has no repositories")); + }); } #[test] @@ -73,13 +69,11 @@ fn test_resolve_repos_for_mono_with_profile() { let mut args = default_resolved(); args.mono.profile = Some("myprofile".to_string()); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - let result = resolve_repos_for_mono(&args, &config, "user/repo", &mut io); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), vec!["user/lib1", "user/lib2"]); + with_io(|io| { + let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["user/lib1", "user/lib2"]); + }); } #[test] @@ -88,13 +82,11 @@ fn test_resolve_repos_for_mono_with_explicit_repos() { let mut args = default_resolved(); args.mono.repos = Some(vec!["user/lib1".to_string(), "user/lib2".to_string()]); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - let result = resolve_repos_for_mono(&args, &config, "user/repo", &mut io); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), vec!["user/lib1", "user/lib2"]); + with_io(|io| { + let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["user/lib1", "user/lib2"]); + }); } #[test] @@ -102,15 +94,13 @@ fn test_resolve_repos_for_mono_no_repos_or_profile_errors() { let config = SetupConfig::new(); let args = default_resolved(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - let result = resolve_repos_for_mono(&args, &config, "user/repo", &mut io); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .contains("No repos or profile specified")); + with_io(|io| { + let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("No repos or profile specified")); + }); } #[test] @@ -119,46 +109,39 @@ fn test_resolve_repos_for_mono_profile_not_found_errors() { let mut args = default_resolved(); args.mono.profile = Some("nonexistent".to_string()); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - let result = resolve_repos_for_mono(&args, &config, "user/repo", &mut io); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("not found")); + with_io(|io| { + let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not found")); + }); } #[test] fn test_generate_mono_config_meson() { - let tmp = TempDir::new().unwrap(); - let repos_path = tmp.path().join("repos"); - std::fs::create_dir_all(&repos_path).unwrap(); - - let repo_dir = repos_path.join("user-lib1"); - std::fs::create_dir_all(&repo_dir).unwrap(); - std::fs::write(repo_dir.join("meson.build"), "project('user-lib1', 'cpp')").unwrap(); - - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - let result = generate_mono_config( - star_setup::cli::BuildSystem::Meson, - tmp.path(), - &repos_path, - &[repo_dir], - &["user/lib1".to_string()], - &mut ctx, - ); + with_ctx(MockRunner::new(), |tmp_path, ctx| { + let repos_path = tmp_path.join("repos"); + std::fs::create_dir_all(&repos_path).unwrap(); + + let repo_dir = repos_path.join("user-lib1"); + std::fs::create_dir_all(&repo_dir).unwrap(); + std::fs::write(repo_dir.join("meson.build"), "project('user-lib1', 'cpp')").unwrap(); + + let result = generate_mono_config( + star_setup::cli::BuildSystem::Meson, + tmp_path, + &repos_path, + std::slice::from_ref(&repo_dir), + &["user/lib1".to_string()], + ctx, + ); + + assert!(result.is_ok()); + assert!(result.unwrap().is_some()); + + let meson_build = tmp_path.join("meson.build"); + assert!(meson_build.exists()); - assert!(result.is_ok()); - assert!(result.unwrap().is_some()); - let meson_build = tmp.path().join("meson.build"); - assert!(meson_build.exists()); - let content = std::fs::read_to_string(&meson_build).unwrap(); - assert!(content.contains("user_lib1") || content.contains("user-lib1")); + let content = std::fs::read_to_string(&meson_build).unwrap(); + assert!(content.contains("user_lib1") || content.contains("user-lib1")); + }); } diff --git a/tests/commands/mono/setup.rs b/tests/commands/mono/setup.rs index 502aff4..6e89724 100644 --- a/tests/commands/mono/setup.rs +++ b/tests/commands/mono/setup.rs @@ -1,36 +1,39 @@ use star_setup::commands::build_repo_list; +fn to_string_vec(slice: &[&str]) -> Vec { + slice.iter().map(ToString::to_string).collect() +} + #[test] fn test_build_repo_list_test_repo_first() { - let deps = vec!["user/lib1".to_string(), "user/lib2".to_string()]; + let deps = to_string_vec(&["user/lib1", "user/lib2"]); let result = build_repo_list("user/testrepo", &deps); assert_eq!(result[0], "user/testrepo"); } #[test] fn test_build_repo_list_includes_deps() { - let deps = vec!["user/lib1".to_string(), "user/lib2".to_string()]; + let deps = to_string_vec(&["user/lib1", "user/lib2"]); let result = build_repo_list("user/testrepo", &deps); assert_eq!(result.len(), 3); } #[test] fn test_build_repo_list_dedupes_test_repo_in_deps() { - let deps = vec!["user/lib1".to_string(), "user/testrepo".to_string()]; + let deps = to_string_vec(&["user/lib1", "user/testrepo"]); let result = build_repo_list("user/testrepo", &deps); - assert_eq!(result.len(), 2); - assert_eq!(result[0], "user/testrepo"); + assert_eq!(result, to_string_vec(&["user/testrepo", "user/lib1"])); } #[test] fn test_build_repo_list_dedupes_duplicate_deps() { - let deps = vec!["user/lib1".to_string(), "user/lib1".to_string()]; + let deps = to_string_vec(&["user/lib1", "user/lib1"]); let result = build_repo_list("user/testrepo", &deps); - assert_eq!(result.len(), 2); + assert_eq!(result, to_string_vec(&["user/testrepo", "user/lib1"])); } #[test] fn test_build_repo_list_no_deps() { let result = build_repo_list("user/testrepo", &[]); - assert_eq!(result, vec!["user/testrepo"]); + assert_eq!(result, to_string_vec(&["user/testrepo"])); } diff --git a/tests/commands/mono/wraps.rs b/tests/commands/mono/wraps.rs index 11d2225..b43e0a1 100644 --- a/tests/commands/mono/wraps.rs +++ b/tests/commands/mono/wraps.rs @@ -1,47 +1,25 @@ -use super::super::common::{empty_input, make_io, sink}; +use crate::common::with_io_dir; use star_setup::commands::{hoist_wraps, parse_project_name, parse_provide_pairs}; use tempfile::TempDir; #[test] -fn test_parse_project_name_single_quoted() { - assert_eq!( - parse_project_name("project('my-lib', 'cpp')"), - Some("my_lib".to_string()) - ); -} - -#[test] -fn test_parse_project_name_double_quoted() { - assert_eq!( - parse_project_name(r#"project("my-lib", "cpp")"#), - Some("my_lib".to_string()) - ); -} - -#[test] -fn test_parse_project_name_no_hyphens() { - assert_eq!( - parse_project_name("project('mylib', 'cpp')"), - Some("mylib".to_string()) - ); -} - -#[test] -fn test_parse_project_name_prefix_guard() { - assert_eq!(parse_project_name("myproject('mylib', 'cpp')"), None); -} - -#[test] -fn test_parse_project_name_missing() { - assert_eq!( - parse_project_name("cmake_minimum_required(VERSION 3.20)"), - None - ); -} - -#[test] -fn test_parse_project_name_no_quotes() { - assert_eq!(parse_project_name("project(mylib, cpp)"), None); +fn test_parse_project_name() { + let cases = [ + ("project('my-lib', 'cpp')", Some("my_lib")), + (r#"project("my-lib", "cpp")"#, Some("my_lib")), + ("project('mylib', 'cpp')", Some("mylib")), + ("myproject('mylib', 'cpp')", None), + ("cmake_minimum_required(VERSION 3.20)", None), + ("project(mylib, cpp)", None), + ]; + + for (input, expected) in cases { + assert_eq!( + parse_project_name(input), + expected.map(String::from), + "Failed on input: {input}" + ); + } } #[test] @@ -91,60 +69,56 @@ fn make_repo(project_name: &str) -> TempDir { #[test] fn test_hoist_wraps_empty_repos() { - let repos_dir = TempDir::new().unwrap(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - let result = hoist_wraps(repos_dir.path(), &[], &mut io).unwrap(); - assert!(result.is_empty()); + with_io_dir(|repos_dir, io| { + let result = hoist_wraps(repos_dir, &[], io).unwrap(); + assert!(result.is_empty()); + }); } #[test] fn test_hoist_wraps_skips_repo_without_meson_build() { - let repos_dir = TempDir::new().unwrap(); - let repo = TempDir::new().unwrap(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut io).unwrap(); - assert!(result.is_empty()); + with_io_dir(|repos_dir, io| { + let repo = TempDir::new().unwrap(); + let result = hoist_wraps(repos_dir, &[repo.path().to_path_buf()], io).unwrap(); + assert!(result.is_empty()); + }); } #[test] fn test_hoist_wraps_emits_wrap_without_provide() { - let repos_dir = TempDir::new().unwrap(); - let repo = make_repo("my-lib"); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut io).unwrap(); - assert!(result.contains_key("my_lib")); - let wrap = repos_dir.path().join("my_lib.wrap"); - assert!(wrap.exists()); - let content = std::fs::read_to_string(&wrap).unwrap(); - assert!(content.contains("directory =")); - assert!(!content.contains("[provide]")); + with_io_dir(|repos_dir, io| { + let repo = make_repo("my-lib"); + let result = hoist_wraps(repos_dir, &[repo.path().to_path_buf()], io).unwrap(); + + assert!(result.contains_key("my_lib")); + let wrap = repos_dir.join("my_lib.wrap"); + assert!(wrap.exists()); + + let content = std::fs::read_to_string(&wrap).unwrap(); + assert!(content.contains("directory =")); + assert!(!content.contains("[provide]")); + }); } #[test] fn test_hoist_wraps_emits_wrap_with_provide() { - let repos_dir = TempDir::new().unwrap(); - let repo = make_repo("my-lib"); - let subprojects = repo.path().join("subprojects"); - std::fs::create_dir(&subprojects).unwrap(); - std::fs::write( - subprojects.join("my_lib.wrap"), - "[provide]\nmy_lib = my_lib_dep\n", - ) - .unwrap(); - std::fs::write(subprojects.join("readme.txt"), "ignore me").unwrap(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut io).unwrap(); - assert!(result.contains_key("my_lib")); - let wrap = repos_dir.path().join("my_lib.wrap"); - let content = std::fs::read_to_string(&wrap).unwrap(); - assert!(content.contains("[provide]")); - assert!(content.contains("my_lib = my_lib_dep")); + with_io_dir(|repos_dir, io| { + let repo = make_repo("my-lib"); + let subprojects = repo.path().join("subprojects"); + std::fs::create_dir(&subprojects).unwrap(); + std::fs::write( + subprojects.join("my_lib.wrap"), + "[provide]\nmy_lib = my_lib_dep\n", + ) + .unwrap(); + std::fs::write(subprojects.join("readme.txt"), "ignore me").unwrap(); + + let result = hoist_wraps(repos_dir, &[repo.path().to_path_buf()], io).unwrap(); + + assert!(result.contains_key("my_lib")); + let wrap = repos_dir.join("my_lib.wrap"); + let content = std::fs::read_to_string(&wrap).unwrap(); + assert!(content.contains("[provide]")); + assert!(content.contains("my_lib = my_lib_dep")); + }); } diff --git a/tests/commands/single.rs b/tests/commands/single.rs index 68167e9..e02f341 100644 --- a/tests/commands/single.rs +++ b/tests/commands/single.rs @@ -1,9 +1,8 @@ -use super::common::{default_resolved, make_io, sink, MockRunner}; +use super::common::{default_resolved, MockRunner}; use star_setup::{ commands::single_repo_mode, - ctx::{DryRunRunner, RunCtx}, + ctx::{DryRunRunner, IoCtx, RunCtx}, }; -use tempfile::TempDir; fn make_repo_fixture(base: &std::path::Path) { let repo_dir = base.join("user-repo"); @@ -11,21 +10,38 @@ fn make_repo_fixture(base: &std::path::Path) { std::fs::write(repo_dir.join("CMakeLists.txt"), "").unwrap(); } -#[test] -fn test_single_repo_mode_updates_existing_repo() { - let tmp = TempDir::new().unwrap(); - let args = default_resolved(); - make_repo_fixture(tmp.path()); - - let mut input = b"y\n".as_ref(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, +fn with_single_mode_ctx(input: &[u8], mut runner: R, test_logic: F) -> (T, String, R) +where + R: star_setup::ctx::Runner, + F: FnOnce(&std::path::Path, &mut RunCtx) -> T, +{ + let tmp = tempfile::TempDir::new().unwrap(); + let mut output = Vec::new(); + let mut input_slice = input; + + let result = { + let mut ctx = RunCtx { + io: IoCtx { + input: &mut input_slice, + output: &mut output, + verbose: false, + timing: false, + dry_run: false, + }, + runner: &mut runner, + }; + test_logic(tmp.path(), &mut ctx) }; - single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); + (result, String::from_utf8(output).unwrap(), runner) +} + +#[test] +fn test_single_repo_mode_updates_existing_repo() { + let ((), _, runner) = with_single_mode_ctx(b"y\n", MockRunner::new(), |tmp_path, ctx| { + make_repo_fixture(tmp_path); + single_repo_mode(&default_resolved(), tmp_path, ctx).unwrap(); + }); assert!(runner .calls @@ -35,19 +51,10 @@ fn test_single_repo_mode_updates_existing_repo() { #[test] fn test_single_repo_mode_skips_update_when_declined() { - let tmp = TempDir::new().unwrap(); - let args = default_resolved(); - make_repo_fixture(tmp.path()); - - let mut input = b"n\n".as_ref(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - - single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); + let ((), _, runner) = with_single_mode_ctx(b"n\n", MockRunner::new(), |tmp_path, ctx| { + make_repo_fixture(tmp_path); + single_repo_mode(&default_resolved(), tmp_path, ctx).unwrap(); + }); assert!(!runner .calls @@ -57,120 +64,68 @@ fn test_single_repo_mode_skips_update_when_declined() { #[test] fn test_single_repo_mode_cleans_build_dir() { - let tmp = TempDir::new().unwrap(); let mut args = default_resolved(); args.build.clean = true; - make_repo_fixture(tmp.path()); - let build_dir = tmp.path().join("user-repo").join(&args.build.build_dir); - std::fs::create_dir_all(&build_dir).unwrap(); - std::fs::write(build_dir.join("dummy.txt"), "old content").unwrap(); - - let mut input = b"n\n".as_ref(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); - assert!(!build_dir.join("dummy.txt").exists()); - assert!(build_dir.exists()); + let ((), _, _) = with_single_mode_ctx(b"n\n", MockRunner::new(), |tmp_path, ctx| { + make_repo_fixture(tmp_path); + let build_dir = tmp_path.join("user-repo").join(&args.build.build_dir); + std::fs::create_dir_all(&build_dir).unwrap(); + std::fs::write(build_dir.join("dummy.txt"), "old content").unwrap(); + + single_repo_mode(&args, tmp_path, ctx).unwrap(); + assert!(!build_dir.join("dummy.txt").exists()); + assert!(build_dir.exists()); + }); } #[test] fn test_single_repo_mode_outputs_timing() { - let tmp = TempDir::new().unwrap(); - let args = default_resolved(); - make_repo_fixture(tmp.path()); - - let mut input = b"n\n".as_ref(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: star_setup::ctx::IoCtx { - input: &mut input, - output: &mut output, - verbose: false, - timing: true, - dry_run: false, - }, - runner: &mut runner, - }; + let ((), out, _) = with_single_mode_ctx(b"n\n", MockRunner::new(), |tmp_path, ctx| { + make_repo_fixture(tmp_path); + ctx.io.timing = true; + single_repo_mode(&default_resolved(), tmp_path, ctx).unwrap(); + }); - single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); - let out = String::from_utf8(output).unwrap(); assert!(out.contains("[timing] Total:")); } #[test] fn test_single_repo_mode_dry_run_makes_no_fs_changes() { - let tmp = TempDir::new().unwrap(); let mut args = default_resolved(); args.diagnostic.dry_run = true; - let mut input = b"".as_ref(); - let mut output = sink(); - let mut runner = DryRunRunner; - let mut ctx = RunCtx { - io: star_setup::ctx::IoCtx { - input: &mut input, - output: &mut output, - verbose: false, - timing: false, - dry_run: true, - }, - runner: &mut runner, - }; - - single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); - assert!(std::fs::read_dir(tmp.path()).unwrap().next().is_none()); + let ((), _, _) = with_single_mode_ctx(b"", DryRunRunner, |tmp_path, ctx| { + ctx.io.dry_run = true; + single_repo_mode(&args, tmp_path, ctx).unwrap(); + assert!(std::fs::read_dir(tmp_path).unwrap().next().is_none()); + }); } #[test] fn test_single_repo_mode_dry_run_clean_prints_would_remove() { - let tmp = TempDir::new().unwrap(); let mut args = default_resolved(); args.diagnostic.dry_run = true; args.build.clean = true; - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut runner = DryRunRunner; - let mut ctx = RunCtx { - io: star_setup::ctx::IoCtx { - input: &mut input, - output: &mut output, - verbose: false, - timing: false, - dry_run: true, - }, - runner: &mut runner, - }; + let ((), out, _) = with_single_mode_ctx(b"", DryRunRunner, |tmp_path, ctx| { + ctx.io.dry_run = true; + single_repo_mode(&args, tmp_path, ctx).unwrap(); + assert!(std::fs::read_dir(tmp_path).unwrap().next().is_none()); + }); - single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); - - let out = String::from_utf8(output).unwrap(); assert!(out.contains("Would remove directory:")); - assert!(std::fs::read_dir(tmp.path()).unwrap().next().is_none()); } #[test] fn test_single_repo_mode_with_build_system_flag() { - let tmp = TempDir::new().unwrap(); let mut args = default_resolved(); args.build.build_system = Some(star_setup::cli::BuildSystem::Cmake); - make_repo_fixture(tmp.path()); - - let mut input = b"n\n".as_ref(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); + let ((), _, runner) = with_single_mode_ctx(b"n\n", MockRunner::new(), |tmp_path, ctx| { + make_repo_fixture(tmp_path); + single_repo_mode(&args, tmp_path, ctx).unwrap(); + }); assert!(runner.calls.iter().any(|(cmd, _)| cmd[0] == "cmake")); } diff --git a/tests/common/harness.rs b/tests/common/harness.rs new file mode 100644 index 0000000..3391031 --- /dev/null +++ b/tests/common/harness.rs @@ -0,0 +1,70 @@ +use super::io::{empty_input, make_io, sink}; +use star_setup::ctx::{IoCtx, RunCtx, Runner}; +use std::path::Path; +use tempfile::TempDir; + +#[allow(dead_code)] +pub fn with_io(f: impl FnOnce(&mut IoCtx<'_>)) { + let mut input = empty_input(); + let mut output = sink(); + let mut io = make_io(&mut input, &mut output); + f(&mut io); +} + +#[allow(dead_code)] +pub fn with_io_dir(f: impl FnOnce(&Path, &mut IoCtx<'_>)) { + let tmp = TempDir::new().unwrap(); + let mut input = empty_input(); + let mut output = sink(); + let mut io = make_io(&mut input, &mut output); + f(tmp.path(), &mut io); +} + +#[allow(dead_code)] +pub fn with_io_input(input: &[u8], f: impl FnOnce(&mut IoCtx<'_>) -> T) -> T { + let mut input_slice = input; + let mut output = sink(); + let mut io = make_io(&mut input_slice, &mut output); + f(&mut io) +} + +#[allow(dead_code)] +pub fn with_io_output(f: impl FnOnce(&mut IoCtx<'_>) -> T) -> (T, String) { + let mut input = empty_input(); + let mut output = Vec::new(); + let mut io = make_io(&mut input, &mut output); + let result = f(&mut io); + (result, String::from_utf8(output).unwrap_or_default()) +} + +#[allow(dead_code)] +pub fn with_io_input_output(input: &[u8], f: impl FnOnce(&mut IoCtx<'_>) -> T) -> (T, String) { + let mut input_slice = input; + let mut output = Vec::new(); + let mut io = make_io(&mut input_slice, &mut output); + let result = f(&mut io); + (result, String::from_utf8(output).unwrap_or_default()) +} + +#[allow(dead_code)] +pub fn with_ctx( + mut runner: R, + f: impl FnOnce(&Path, &mut RunCtx<'_, '_>), +) -> (R, Vec) { + let tmp = TempDir::new().unwrap(); + let mut input = empty_input(); + let mut output = Vec::new(); + { + let mut ctx = RunCtx { + io: make_io(&mut input, &mut output), + runner: &mut runner, + }; + f(tmp.path(), &mut ctx); + } + (runner, output) +} + +#[allow(dead_code)] +pub fn with_runner_ctx(runner: R, f: impl FnOnce(&Path, &mut RunCtx<'_, '_>)) -> R { + with_ctx(runner, f).0 +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 7930bb7..667b994 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -10,3 +10,9 @@ pub use args::{ default_args, default_resolved, default_resolved_interactive, default_resolved_mono, default_resolved_with_no_build, }; +pub mod harness; +#[allow(unused_imports)] +pub use harness::{ + with_ctx, with_io, with_io_dir, with_io_input, with_io_input_output, with_io_output, + with_runner_ctx, +}; diff --git a/tests/common/runner.rs b/tests/common/runner.rs index a265562..6a7718a 100644 --- a/tests/common/runner.rs +++ b/tests/common/runner.rs @@ -2,7 +2,6 @@ use star_setup::ctx::{IoCtx, Runner}; use std::collections::VecDeque; use std::path::Path; -#[allow(dead_code)] pub struct MockRunner { pub calls: Vec<(Vec, Option)>, pub fail_on: Option, diff --git a/tests/config.rs b/tests/config.rs index b0d5811..5d1498e 100644 --- a/tests/config.rs +++ b/tests/config.rs @@ -1,5 +1,6 @@ #[path = "common/mod.rs"] mod common; + #[path = "config/crud.rs"] mod crud; #[path = "config/display.rs"] diff --git a/tests/config/crud.rs b/tests/config/crud.rs index 17c2e48..728f47f 100644 --- a/tests/config/crud.rs +++ b/tests/config/crud.rs @@ -1,7 +1,5 @@ -use super::{ - common::{empty_input, make_io, sink}, - fixtures::sample_entry, -}; +use super::fixtures::sample_entry; +use crate::common::{with_io_dir, with_io_input_output, with_io_output}; use star_setup::{ cli::BuildType, config::{ @@ -25,52 +23,47 @@ fn test_has_config_false() { #[test] fn test_add_config_inserts_and_saves() { - let tmp = tempfile::TempDir::new().unwrap(); - let path = tmp.path().join(".star-setup.json"); - let mut config = SetupConfig::new(); - config.path = Some(path.clone()); - - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - add_config(&mut config, "myconfig", sample_entry(), true, &mut io).unwrap(); - assert!(has_config(&config, "myconfig")); - assert!(path.exists()); + with_io_dir(|tmp, io| { + let path = tmp.join(".star-setup.json"); + let mut config = SetupConfig::new(); + config.path = Some(path.clone()); + + add_config(&mut config, "myconfig", sample_entry(), true, io).unwrap(); + assert!(has_config(&config, "myconfig")); + assert!(path.exists()); + }); } #[test] fn test_add_config_aborts_when_exists_and_not_confirmed() { - let tmp = tempfile::TempDir::new().unwrap(); - let mut config = SetupConfig::new(); - config.path = Some(tmp.path().join(".star-setup.json")); - insert_config(&mut config, "myconfig", sample_entry()); - - let mut input = b"n\n".as_ref(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - add_config( - &mut config, - "myconfig", - ConfigEntry { - ssh: false, // different from sample_entry's ssh: true - build_type: BuildType::Debug, - build_dir: "build".to_string(), - mono_dir: "mono".to_string(), - no_build: false, - clean: false, - verbose: false, - timing: false, - dry_run: false, - cmake_flags: vec![], - meson_flags: vec![], - }, - false, - &mut io, - ) - .unwrap(); - assert!(config.configs["myconfig"].ssh); + with_io_input_output(b"n\n", |io| { + let tmp = tempfile::TempDir::new().unwrap(); + let mut config = SetupConfig::new(); + config.path = Some(tmp.path().join(".star-setup.json")); + insert_config(&mut config, "myconfig", sample_entry()); + + add_config( + &mut config, + "myconfig", + ConfigEntry { + ssh: false, + build_type: BuildType::Debug, + build_dir: "build".to_string(), + mono_dir: "mono".to_string(), + no_build: false, + clean: false, + verbose: false, + timing: false, + dry_run: false, + cmake_flags: vec![], + meson_flags: vec![], + }, + false, + io, + ) + .unwrap(); + assert!(config.configs["myconfig"].ssh); + }); } #[test] @@ -91,53 +84,41 @@ fn test_remove_config_entry_exists() { #[test] fn test_create_default_config_creates_file() { - let tmp = tempfile::TempDir::new().unwrap(); - let path = tmp.path().join(".star-setup.json"); - - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - create_default_config(path.clone(), true, &mut io).unwrap(); - assert!(path.exists()); + with_io_dir(|tmp, io| { + let path = tmp.join(".star-setup.json"); + create_default_config(path.clone(), true, io).unwrap(); + assert!(path.exists()); + }); } #[test] fn test_create_default_config_aborts_when_exists_and_not_confirmed() { - let tmp = tempfile::TempDir::new().unwrap(); - let path = tmp.path().join(".star-setup.json"); - std::fs::write(&path, "original").unwrap(); - - let mut input = b"n\n".as_ref(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - create_default_config(path.clone(), false, &mut io).unwrap(); - assert_eq!(std::fs::read_to_string(&path).unwrap(), "original"); + with_io_input_output(b"n\n", |io| { + let tmp = tempfile::TempDir::new().unwrap(); + let path = tmp.path().join(".star-setup.json"); + std::fs::write(&path, "original").unwrap(); + + create_default_config(path.clone(), false, io).unwrap(); + assert_eq!(std::fs::read_to_string(&path).unwrap(), "original"); + }); } #[test] fn test_list_configs_empty() { - let config = SetupConfig::new(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - list_configs(&config, &mut io); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_output(|io| { + let config = SetupConfig::new(); + list_configs(&config, io); + }); assert!(out.contains("No configurations created")); } #[test] fn test_list_configs_with_entries() { - let mut config = SetupConfig::new(); - insert_config(&mut config, "myconfig", sample_entry()); - - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - list_configs(&config, &mut io); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_output(|io| { + let mut config = SetupConfig::new(); + insert_config(&mut config, "myconfig", sample_entry()); + list_configs(&config, io); + }); assert!(out.contains("myconfig")); assert!(out.contains("Configurations:")); } @@ -150,43 +131,35 @@ fn test_remove_config_entry_missing() { #[test] fn test_remove_config_removes_and_saves() { - let tmp = tempfile::TempDir::new().unwrap(); - let path = tmp.path().join(".star-setup.json"); - let mut config = SetupConfig::new(); - config.path = Some(path.clone()); - insert_config(&mut config, "myconfig", sample_entry()); - save_config(&mut config).unwrap(); - - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - remove_config(&mut config, "myconfig", true, &mut io).unwrap(); - assert!(!has_config(&config, "myconfig")); + with_io_dir(|tmp, io| { + let path = tmp.join(".star-setup.json"); + let mut config = SetupConfig::new(); + config.path = Some(path.clone()); + insert_config(&mut config, "myconfig", sample_entry()); + save_config(&mut config).unwrap(); + + remove_config(&mut config, "myconfig", true, io).unwrap(); + assert!(!has_config(&config, "myconfig")); + }); } #[test] fn test_remove_config_not_found() { let mut config = SetupConfig::new(); - - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - remove_config(&mut config, "nonexistent", true, &mut io).unwrap(); + with_io_output(|io| { + remove_config(&mut config, "nonexistent", true, io).unwrap(); + }); } #[test] fn test_remove_config_aborts_when_not_confirmed() { - let tmp = tempfile::TempDir::new().unwrap(); - let mut config = SetupConfig::new(); - config.path = Some(tmp.path().join(".star-setup.json")); - insert_config(&mut config, "myconfig", sample_entry()); - - let mut input = b"n\n".as_ref(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - remove_config(&mut config, "myconfig", false, &mut io).unwrap(); - assert!(has_config(&config, "myconfig")); + with_io_input_output(b"n\n", |io| { + let tmp = tempfile::TempDir::new().unwrap(); + let mut config = SetupConfig::new(); + config.path = Some(tmp.path().join(".star-setup.json")); + insert_config(&mut config, "myconfig", sample_entry()); + + remove_config(&mut config, "myconfig", false, io).unwrap(); + assert!(has_config(&config, "myconfig")); + }); } diff --git a/tests/config/io.rs b/tests/config/io.rs index 0b83232..0baaccb 100644 --- a/tests/config/io.rs +++ b/tests/config/io.rs @@ -1,4 +1,6 @@ -use super::{common::sink, fixtures::sample_entry}; +use crate::common::with_io_output; + +use super::fixtures::sample_entry; use star_setup::{ cli::BuildType, config::{insert_config, load_config, save_config, ConfigEntry, SetupConfig}, @@ -30,18 +32,22 @@ fn test_save_and_load_roundtrip() { ); save_config(&mut config).unwrap(); - let loaded = load_config(&[path], &mut sink()); - assert!(loaded.configs.contains_key("default")); - assert!(loaded.configs["default"].ssh); - assert_eq!(loaded.configs["default"].build_type, BuildType::Release); - assert_eq!(loaded.configs["default"].mono_dir, "mono"); - assert_eq!(loaded.configs["default"].cmake_flags, Vec::::new()); + with_io_output(|io| { + let loaded = load_config(&[path], &mut io.output); + assert!(loaded.configs.contains_key("default")); + assert!(loaded.configs["default"].ssh); + assert_eq!(loaded.configs["default"].build_type, BuildType::Release); + assert_eq!(loaded.configs["default"].mono_dir, "mono"); + assert_eq!(loaded.configs["default"].cmake_flags, Vec::::new()); + }); } #[test] fn test_load_config_skips_missing_local_file() { - let config = load_config(&[], &mut sink()); - assert!(config.configs.is_empty()); + with_io_output(|io| { + let config = load_config(&[], &mut io.output); + assert!(config.configs.is_empty()); + }); } #[test] @@ -50,17 +56,21 @@ fn test_load_config_handles_invalid_json() { let path = tmp.path().join(".star-setup.json"); std::fs::write(&path, "{invalid json").unwrap(); - let config = load_config(&[path], &mut sink()); - assert!(config.configs.is_empty()); + with_io_output(|io| { + let config = load_config(&[path], &mut io.output); + assert!(config.configs.is_empty()); + }); } #[test] fn test_load_config_skips_nonexistent_path() { - let config = load_config( - &[PathBuf::from("/nonexistent/path/.star-setup.json")], - &mut sink(), - ); - assert!(config.configs.is_empty()); + with_io_output(|io| { + let config = load_config( + &[PathBuf::from("/nonexistent/path/.star-setup.json")], + &mut io.output, + ); + assert!(config.configs.is_empty()); + }); } #[test] @@ -80,9 +90,11 @@ fn test_load_config_first_valid_wins() { insert_config(&mut config2, "second", sample_entry()); save_config(&mut config2).unwrap(); - let loaded = load_config(&[path1, path2], &mut sink()); - assert!(loaded.configs.contains_key("first")); - assert!(!loaded.configs.contains_key("second")); + with_io_output(|io| { + let loaded = load_config(&[path1, path2], &mut io.output); + assert!(loaded.configs.contains_key("first")); + assert!(!loaded.configs.contains_key("second")); + }); } #[test] @@ -99,6 +111,8 @@ fn test_load_config_falls_through_invalid_to_valid() { insert_config(&mut config2, "second", sample_entry()); save_config(&mut config2).unwrap(); - let loaded = load_config(&[path1, path2], &mut sink()); - assert!(loaded.configs.contains_key("second")); + with_io_output(|io| { + let loaded = load_config(&[path1, path2], &mut io.output); + assert!(loaded.configs.contains_key("second")); + }); } diff --git a/tests/ctx.rs b/tests/ctx.rs index db891dd..cdce039 100644 --- a/tests/ctx.rs +++ b/tests/ctx.rs @@ -1,58 +1,36 @@ -use star_setup::ctx::{DryRunRunner, IoCtx, ProcessRunner, Runner}; +use star_setup::ctx::{DryRunRunner, ProcessRunner, Runner}; use std::path::Path; +mod common; +use common::{with_io, with_io_output}; #[test] fn test_process_runner_runs_command() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut runner = ProcessRunner; - let mut io = IoCtx { - input: &mut input, - output: &mut output, - verbose: false, - timing: false, - dry_run: false, - }; - assert!(runner.run(&["git", "--version"], None, &mut io).is_ok()); + with_io(|io| { + assert!(ProcessRunner.run(&["git", "--version"], None, io).is_ok()); + }); } #[test] fn test_dry_run_runner_prints_command() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut runner = DryRunRunner; - let mut io = IoCtx { - input: &mut input, - output: &mut output, - verbose: false, - timing: false, - dry_run: true, - }; - runner.run(&["git", "clone", "foo"], None, &mut io).unwrap(); - assert_eq!( - String::from_utf8(output).unwrap(), - "Would run: git clone foo\n" - ); + let ((), output) = with_io_output(|io| { + io.dry_run = true; + DryRunRunner + .run(&["git", "clone", "foo"], None, io) + .unwrap(); + }); + assert_eq!(output, "Would run: git clone foo\n"); } #[test] fn test_dry_run_runner_prints_cwd() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut runner = DryRunRunner; - let mut io = IoCtx { - input: &mut input, - output: &mut output, - verbose: false, - timing: false, - dry_run: true, - }; - runner - .run(&["cmake", ".."], Some(Path::new("/tmp/build")), &mut io) - .unwrap(); - let out = String::from_utf8(output).unwrap(); - assert!(out.contains("Would run: cmake ..")); - assert!(out.contains(" in directory: /tmp/build")); + let ((), output) = with_io_output(|io| { + io.dry_run = true; + DryRunRunner + .run(&["cmake", ".."], Some(Path::new("/tmp/build")), io) + .unwrap(); + }); + assert!(output.contains("Would run: cmake ..")); + assert!(output.contains(" in directory: /tmp/build")); } #[test] diff --git a/tests/interactive.rs b/tests/interactive.rs index 5f55bf7..571f1d9 100644 --- a/tests/interactive.rs +++ b/tests/interactive.rs @@ -1,6 +1,6 @@ use star_setup::interactive::interactive_mode; mod common; -use common::{default_resolved, default_resolved_interactive, make_io}; +use common::{default_resolved, default_resolved_interactive, with_io_input_output}; fn input_with_suffix(prefix: &[u8]) -> Vec { let mut v = prefix.to_vec(); @@ -11,11 +11,12 @@ fn input_with_suffix(prefix: &[u8]) -> Vec { #[test] fn test_interactive_mode_single_repo() { let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n1"); - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved(); - interactive_mode(&mut args, &mut io).unwrap(); + let (args, _) = with_io_input_output(&input, |io| { + let mut args = default_resolved(); + interactive_mode(&mut args, io).unwrap(); + args + }); + assert_eq!(args.repo, Some("user/repo".to_string())); assert!(!args.connection.ssh); assert!(!args.mono.mono_repo); @@ -24,22 +25,24 @@ fn test_interactive_mode_single_repo() { #[test] fn test_interactive_mode_ssh_enabled() { let input = input_with_suffix(b"user/repo\ny\nn\nn\nn\n1"); - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved_interactive(); - interactive_mode(&mut args, &mut io).unwrap(); + let (args, _) = with_io_input_output(&input, |io| { + let mut args = default_resolved_interactive(); + interactive_mode(&mut args, io).unwrap(); + args + }); + assert!(args.connection.ssh); } #[test] fn test_interactive_mode_mono_repo_with_profile() { let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n2\n1\nmyprofile"); - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved(); - interactive_mode(&mut args, &mut io).unwrap(); + let (args, _) = with_io_input_output(&input, |io| { + let mut args = default_resolved(); + interactive_mode(&mut args, io).unwrap(); + args + }); + assert!(args.mono.mono_repo); assert_eq!(args.mono.profile, Some("myprofile".to_string())); } @@ -47,11 +50,12 @@ fn test_interactive_mode_mono_repo_with_profile() { #[test] fn test_interactive_mode_mono_repo_with_manual_repos() { let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n2\n2\nuser/lib1 user/lib2"); - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved(); - interactive_mode(&mut args, &mut io).unwrap(); + let (args, _) = with_io_input_output(&input, |io| { + let mut args = default_resolved(); + interactive_mode(&mut args, io).unwrap(); + args + }); + assert!(args.mono.mono_repo); assert_eq!( args.mono.repos, @@ -62,24 +66,25 @@ fn test_interactive_mode_mono_repo_with_manual_repos() { #[test] fn test_interactive_mode_skips_repo_prompt_when_set() { let input = input_with_suffix(b"n\nn\nn\nn\n1"); - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved(); - args.repo = Some("already/set".to_string()); - interactive_mode(&mut args, &mut io).unwrap(); + let (args, _) = with_io_input_output(&input, |io| { + let mut args = default_resolved(); + args.repo = Some("already/set".to_string()); + interactive_mode(&mut args, io).unwrap(); + args + }); + assert_eq!(args.repo, Some("already/set".to_string())); } #[test] fn test_interactive_mode_output_contains_header() { let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n1"); - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved(); - interactive_mode(&mut args, &mut io).unwrap(); - let out_str = String::from_utf8(output).unwrap(); + let (_, out_str) = with_io_input_output(&input, |io| { + let mut args = default_resolved(); + interactive_mode(&mut args, io).unwrap(); + args + }); + assert!(out_str.contains("Star Setup Interactive Mode")); assert!(out_str.contains("Interactive mode complete")); } @@ -87,46 +92,48 @@ fn test_interactive_mode_output_contains_header() { #[test] fn test_interactive_mode_yes_word_not_accepted_for_ssh() { let input = input_with_suffix(b"user/repo\nyes\nn\nn\nn\n1"); - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved(); - interactive_mode(&mut args, &mut io).unwrap(); + let (args, _) = with_io_input_output(&input, |io| { + let mut args = default_resolved(); + interactive_mode(&mut args, io).unwrap(); + args + }); + assert!(!args.connection.ssh); } #[test] fn test_interactive_mode_invalid_mode_then_valid() { let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\nfoo\n1"); - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved(); - interactive_mode(&mut args, &mut io).unwrap(); + let (args, _) = with_io_input_output(&input, |io| { + let mut args = default_resolved(); + interactive_mode(&mut args, io).unwrap(); + args + }); + assert!(!args.mono.mono_repo); } #[test] fn test_interactive_mode_invalid_mono_choice_then_valid() { let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n2\nfoo\n1\nmyprofile"); - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved(); - interactive_mode(&mut args, &mut io).unwrap(); + let (args, _) = with_io_input_output(&input, |io| { + let mut args = default_resolved(); + interactive_mode(&mut args, io).unwrap(); + args + }); + assert!(args.mono.mono_repo); assert_eq!(args.mono.profile, Some("myprofile".to_string())); } #[test] fn test_interactive_mode_errors_on_eof() { - let input = b""; - let mut input = input.as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let mut args = default_resolved(); - args.repo = None; - let result = interactive_mode(&mut args, &mut io); + let (result, _) = with_io_input_output(b"", |io| { + let mut args = default_resolved(); + args.repo = None; + interactive_mode(&mut args, io) + }); + assert!(result.is_err()); assert!(result.unwrap_err().contains("unexpected end of input")); } diff --git a/tests/profile.rs b/tests/profile.rs index ba1b3ee..e08ad54 100644 --- a/tests/profile.rs +++ b/tests/profile.rs @@ -1,5 +1,6 @@ #[path = "common/mod.rs"] mod common; + #[path = "profile/crud.rs"] mod crud; #[path = "profile/display.rs"] diff --git a/tests/profile/crud.rs b/tests/profile/crud.rs index cc19431..06b3388 100644 --- a/tests/profile/crud.rs +++ b/tests/profile/crud.rs @@ -1,4 +1,4 @@ -use super::common::{empty_input, make_io, sink}; +use crate::common::{with_io_dir, with_io_input_output, with_io_output}; use star_setup::{ config::{load_config, save_config, SetupConfig}, profile::{add_profile, has_profile, insert_profile, remove_profile, remove_profile_entry}, @@ -40,131 +40,112 @@ fn test_has_profile_false() { #[test] fn test_add_profile_inserts_and_saves() { - let tmp = tempfile::TempDir::new().unwrap(); - let path = tmp.path().join(".star-setup.json"); - let mut config = SetupConfig::new(); - config.path = Some(path.clone()); - - let args = vec!["myprofile".to_string(), "user/repo1".to_string()]; - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - add_profile(&mut config, &args, true, &mut io).unwrap(); - assert!(has_profile(&config, "myprofile")); - assert!(path.exists()); + with_io_dir(|tmp, io| { + let path = tmp.join(".star-setup.json"); + let mut config = SetupConfig::new(); + config.path = Some(path.clone()); + + let args = vec!["myprofile".to_string(), "user/repo1".to_string()]; + add_profile(&mut config, &args, true, io).unwrap(); + assert!(has_profile(&config, "myprofile")); + assert!(path.exists()); + }); } #[test] fn test_add_profile_errors_on_insufficient_args() { let mut config = SetupConfig::new(); let args = vec!["myprofile".to_string()]; - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - let result = add_profile(&mut config, &args, true, &mut io); - assert!(result.is_err()); + with_io_output(|io| { + let result = add_profile(&mut config, &args, true, io); + assert!(result.is_err()); + }); } #[test] fn test_add_profile_errors_on_empty_args() { let mut config = SetupConfig::new(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - let result = add_profile(&mut config, &[], true, &mut io); - assert!(result.is_err()); + with_io_output(|io| { + let result = add_profile(&mut config, &[], true, io); + assert!(result.is_err()); + }); } #[test] fn test_add_profile_overwrites_existing() { - let tmp = tempfile::TempDir::new().unwrap(); - let mut config = SetupConfig::new(); - config.path = Some(tmp.path().join(".star-setup.json")); - insert_profile(&mut config, "myprofile", vec!["old/repo".to_string()]); - - let args = vec!["myprofile".to_string(), "new/repo".to_string()]; - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - add_profile(&mut config, &args, true, &mut io).unwrap(); - assert_eq!(config.profiles["myprofile"], vec!["new/repo"]); + with_io_dir(|tmp, io| { + let mut config = SetupConfig::new(); + config.path = Some(tmp.join(".star-setup.json")); + insert_profile(&mut config, "myprofile", vec!["old/repo".to_string()]); + + let args = vec!["myprofile".to_string(), "new/repo".to_string()]; + add_profile(&mut config, &args, true, io).unwrap(); + assert_eq!(config.profiles["myprofile"], vec!["new/repo"]); + }); } #[test] fn test_add_profile_multiple_repos() { - let tmp = tempfile::TempDir::new().unwrap(); - let mut config = SetupConfig::new(); - config.path = Some(tmp.path().join(".star-setup.json")); - - let args = vec![ - "myprofile".to_string(), - "user/repo1".to_string(), - "user/repo2".to_string(), - "user/repo3".to_string(), - ]; - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - add_profile(&mut config, &args, true, &mut io).unwrap(); - assert_eq!(config.profiles["myprofile"].len(), 3); + with_io_dir(|tmp, io| { + let mut config = SetupConfig::new(); + config.path = Some(tmp.join(".star-setup.json")); + + let args = vec![ + "myprofile".to_string(), + "user/repo1".to_string(), + "user/repo2".to_string(), + "user/repo3".to_string(), + ]; + add_profile(&mut config, &args, true, io).unwrap(); + assert_eq!(config.profiles["myprofile"].len(), 3); + }); } #[test] fn test_add_profile_aborts_when_exists_and_not_confirmed() { - let tmp = tempfile::TempDir::new().unwrap(); - let mut config = SetupConfig::new(); - config.path = Some(tmp.path().join(".star-setup.json")); - insert_profile(&mut config, "myprofile", vec!["old/repo".to_string()]); - - let args = vec!["myprofile".to_string(), "new/repo".to_string()]; - let mut input = b"n\n".as_ref(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - add_profile(&mut config, &args, false, &mut io).unwrap(); - assert_eq!(config.profiles["myprofile"], vec!["old/repo"]); + with_io_input_output(b"n\n", |io| { + let tmp = tempfile::TempDir::new().unwrap(); + let mut config = SetupConfig::new(); + config.path = Some(tmp.path().join(".star-setup.json")); + insert_profile(&mut config, "myprofile", vec!["old/repo".to_string()]); + + let args = vec!["myprofile".to_string(), "new/repo".to_string()]; + add_profile(&mut config, &args, false, io).unwrap(); + assert_eq!(config.profiles["myprofile"], vec!["old/repo"]); + }); } #[test] fn test_remove_profile_removes_and_saves() { - let tmp = tempfile::TempDir::new().unwrap(); - let path = tmp.path().join(".star-setup.json"); - let mut config = SetupConfig::new(); - config.path = Some(path.clone()); - insert_profile(&mut config, "myprofile", vec!["user/repo1".to_string()]); - save_config(&mut config).unwrap(); - - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - remove_profile(&mut config, "myprofile", true, &mut io).unwrap(); - assert!(!has_profile(&config, "myprofile")); + with_io_dir(|tmp, io| { + let path = tmp.join(".star-setup.json"); + let mut config = SetupConfig::new(); + config.path = Some(path.clone()); + insert_profile(&mut config, "myprofile", vec!["user/repo1".to_string()]); + save_config(&mut config).unwrap(); + + remove_profile(&mut config, "myprofile", true, io).unwrap(); + assert!(!has_profile(&config, "myprofile")); + }); } #[test] fn test_remove_profile_not_found() { let mut config = SetupConfig::new(); - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - remove_profile(&mut config, "nonexistent", true, &mut io).unwrap(); + with_io_output(|io| { + remove_profile(&mut config, "nonexistent", true, io).unwrap(); + }); } #[test] fn test_remove_profile_aborts_when_not_confirmed() { - let mut config = SetupConfig::new(); - insert_profile(&mut config, "myprofile", vec!["user/repo1".to_string()]); + with_io_input_output(b"n\n", |io| { + let mut config = SetupConfig::new(); + insert_profile(&mut config, "myprofile", vec!["user/repo1".to_string()]); - let mut input = b"n\n".as_ref(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - remove_profile(&mut config, "myprofile", false, &mut io).unwrap(); - assert!(has_profile(&config, "myprofile")); + remove_profile(&mut config, "myprofile", false, io).unwrap(); + assert!(has_profile(&config, "myprofile")); + }); } #[test] @@ -180,10 +161,12 @@ fn test_save_and_load_profile_roundtrip() { ); save_config(&mut config).unwrap(); - let loaded = load_config(&[path], &mut sink()); - assert!(loaded.profiles.contains_key("myprofile")); - assert_eq!( - loaded.profiles["myprofile"], - vec!["user/repo1", "user/repo2"] - ); + with_io_output(|io| { + let loaded = load_config(&[path], &mut io.output); + assert!(loaded.profiles.contains_key("myprofile")); + assert_eq!( + loaded.profiles["myprofile"], + vec!["user/repo1", "user/repo2"] + ); + }); } diff --git a/tests/profile/display.rs b/tests/profile/display.rs index 4424ab4..6b127d3 100644 --- a/tests/profile/display.rs +++ b/tests/profile/display.rs @@ -1,4 +1,4 @@ -use super::common::{empty_input, make_io, sink}; +use crate::common::with_io_output; use star_setup::{ config::SetupConfig, profile::{insert_profile, list_profiles}, @@ -6,28 +6,20 @@ use star_setup::{ #[test] fn test_list_profiles_empty() { - let config = SetupConfig::new(); - - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - list_profiles(&config, &mut io); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_output(|io| { + let config = SetupConfig::new(); + list_profiles(&config, io); + }); assert!(out.contains("No profiles configured")); } #[test] fn test_list_profiles_with_entries() { - let mut config = SetupConfig::new(); - insert_profile(&mut config, "myprofile", vec!["user/repo1".to_string()]); - - let mut input = empty_input(); - let mut output = sink(); - let mut io = make_io(&mut input, &mut output); - - list_profiles(&config, &mut io); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_output(|io| { + let mut config = SetupConfig::new(); + insert_profile(&mut config, "myprofile", vec!["user/repo1".to_string()]); + list_profiles(&config, io); + }); assert!(out.contains("myprofile")); assert!(out.contains("user/repo1")); } diff --git a/tests/prompts.rs b/tests/prompts.rs index 2782f21..1255811 100644 --- a/tests/prompts.rs +++ b/tests/prompts.rs @@ -1,37 +1,26 @@ use star_setup::prompts::{ask, ask_default, ask_yesno, confirm}; mod common; -use common::make_io; +use common::with_io_input; #[test] fn test_ask_errors_on_eof() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - assert!(ask("prompt", &mut io).is_err()); + assert!(with_io_input(b"", |io| ask("prompt", io)).is_err()); } #[test] fn test_ask_default_errors_on_eof() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - assert!(ask_default("prompt", "default", &mut io).is_err()); + assert!(with_io_input(b"", |io| ask_default("prompt", "default", io)).is_err()); } #[test] fn test_ask_yesno_errors_on_eof() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - assert!(ask_yesno("prompt", true, &mut io).is_err()); + assert!(with_io_input(b"", |io| ask_yesno("prompt", true, io)).is_err()); } #[test] fn test_ask_default_returns_input_when_not_empty() { - let mut input = b"custom\n".as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - assert_eq!(ask_default("prompt", "default", &mut io).unwrap(), "custom"); + let result = with_io_input(b"custom\n", |io| ask_default("prompt", "default", io)); + assert_eq!(result.unwrap(), "custom"); } #[test] @@ -43,32 +32,21 @@ fn test_confirm_input_cases() { (b"n\n", false, "n rejects"), (b"yes\n", false, "yes rejects"), ]; + for (input, expected, name) in cases { - let mut input = input; - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - assert_eq!( - confirm("prompt", false, &mut io).unwrap(), - expected, - "Failed: {name}" - ); + let result = with_io_input(input, |io| confirm("prompt", false, io)); + assert_eq!(result.unwrap(), expected, "Failed: {name}"); } } #[test] fn test_confirm_yes_flag_returns_true() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - assert!(confirm("prompt", true, &mut io).unwrap()); + assert!(with_io_input(b"", |io| confirm("prompt", true, io)).unwrap()); } #[test] fn test_confirm_errors_on_eof() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - let result = confirm("prompt", false, &mut io); + let result = with_io_input(b"", |io| confirm("prompt", false, io)); assert!(result.is_err()); assert!(result.unwrap_err().contains("unexpected end of input")); } diff --git a/tests/repository.rs b/tests/repository.rs index 0987076..091f323 100644 --- a/tests/repository.rs +++ b/tests/repository.rs @@ -1,7 +1,7 @@ mod common; -use common::{empty_input, make_io, sink, MockRunner}; +use common::{with_runner_ctx, MockRunner}; use star_setup::{ - ctx::{IoCtx, ProcessRunner, RunCtx}, + ctx::ProcessRunner, repository::{clone_repository, repo_dir_name, resolve_repo_url}, }; @@ -66,38 +66,23 @@ fn test_resolve_repo_url() { #[test] fn test_clone_skips_existing_directory() { - let tmp = tempfile::TempDir::new().unwrap(); - let repo_dir = tmp.path().join("owner-repo"); - std::fs::create_dir_all(&repo_dir).unwrap(); + with_runner_ctx(ProcessRunner, |tmp_path, ctx| { + let repo_dir = tmp_path.join("owner-repo"); + std::fs::create_dir_all(&repo_dir).unwrap(); - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io: IoCtx { - input: &mut b"".as_ref(), - output: &mut Vec::new(), - verbose: false, - timing: false, - dry_run: false, - }, - runner: &mut runner, - }; - let result = clone_repository("owner/repo", tmp.path(), false, &mut ctx); - assert!(result.is_ok()); - assert!(repo_dir.exists()); + let result = clone_repository("owner/repo", tmp_path, false, ctx); + assert!(result.is_ok()); + assert!(repo_dir.exists()); + }); } #[test] fn test_clone_repository_calls_git_clone() { let tmp = tempfile::TempDir::new().unwrap(); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - clone_repository("user/repo", tmp.path(), false, &mut ctx).unwrap(); + let runner = with_runner_ctx(MockRunner::new(), |_, ctx| { + clone_repository("user/repo", tmp.path(), false, ctx).unwrap(); + }); assert_eq!(runner.calls.len(), 1); let (cmd, cwd) = &runner.calls[0]; diff --git a/tests/utils.rs b/tests/utils.rs index 724e05b..14bc18f 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -1,5 +1,6 @@ #[path = "common/mod.rs"] mod common; + #[path = "utils/prerequisites.rs"] mod prerequisites; #[path = "utils/process.rs"] diff --git a/tests/utils/prerequisites.rs b/tests/utils/prerequisites.rs index 1b9f3b4..9aea47e 100644 --- a/tests/utils/prerequisites.rs +++ b/tests/utils/prerequisites.rs @@ -1,26 +1,21 @@ -use super::common::make_io; -use star_setup::{ctx::IoCtx, utils::check_prerequisites}; +use crate::common::{with_ctx, MockRunner}; +use star_setup::utils::check_prerequisites; #[test] fn test_check_prerequisites_succeeds_with_tools_present() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut io = make_io(&mut input, &mut output); - assert!(check_prerequisites(&mut io).is_ok()); + with_ctx(MockRunner::new(), |_, ctx| { + assert!(check_prerequisites(&mut ctx.io).is_ok()); + }); } #[test] fn test_check_prerequisites_verbose_outputs_found() { - let mut input = b"".as_ref(); - let mut output = Vec::new(); - let mut io = IoCtx { - input: &mut input, - output: &mut output, - verbose: true, - timing: false, - dry_run: false, - }; - check_prerequisites(&mut io).unwrap(); + let (_, output) = with_ctx(MockRunner::new(), |_, ctx| { + ctx.io.verbose = true; + + check_prerequisites(&mut ctx.io).unwrap(); + }); + let out = String::from_utf8(output).unwrap(); assert!(out.contains("Found git")); assert!(out.contains("Found cmake")); diff --git a/tests/utils/process.rs b/tests/utils/process.rs index ebb2173..9f5d0f8 100644 --- a/tests/utils/process.rs +++ b/tests/utils/process.rs @@ -1,61 +1,49 @@ -use super::common::sink; +use crate::common::with_io_output; use star_setup::utils::process::run_command; #[test] fn test_run_command_errors_on_empty() { - let mut output = sink(); - assert!(run_command(&[], None, false, &mut output).is_err()); + let (result, _) = with_io_output(|io| run_command(&[], None, false, io.output)); + assert!(result.is_err()); } #[test] fn test_run_command_verbose_outputs_command() { - let mut output = Vec::new(); - star_setup::utils::run_command(&["git", "--version"], None, true, &mut output).unwrap(); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_output(|io| { + run_command(&["git", "--version"], None, true, io.output).unwrap(); + }); assert!(out.contains("Running: git --version")); } #[test] fn test_run_command_verbose_outputs_cwd() { - let tmp = tempfile::TempDir::new().unwrap(); - let mut output = Vec::new(); - star_setup::utils::run_command(&["git", "--version"], Some(tmp.path()), true, &mut output) - .unwrap(); - let out = String::from_utf8(output).unwrap(); + let ((), out) = with_io_output(|io| { + let tmp = tempfile::TempDir::new().unwrap(); + run_command(&["git", "--version"], Some(tmp.path()), true, io.output).unwrap(); + }); assert!(out.contains("in directory:")); } #[test] fn test_run_command_fails_with_stderr() { - let mut output = Vec::new(); - let result = star_setup::utils::run_command( - &["git", "clone", "not-a-real-repo"], - None, - false, - &mut output, - ); + let (result, _) = + with_io_output(|io| run_command(&["git", "clone", "not-a-real-repo"], None, false, io.output)); assert!(result.is_err()); assert!(result.unwrap_err().contains("Command failed")); } #[test] fn test_run_command_fails_no_stderr() { - let mut output = Vec::new(); - let result = - star_setup::utils::run_command(&["git", "invalid-command-xyz"], None, false, &mut output); + let (result, _) = + with_io_output(|io| run_command(&["git", "invalid-command-xyz"], None, false, io.output)); assert!(result.is_err()); assert!(result.unwrap_err().contains("Command failed")); } #[test] fn test_run_command_verbose_fails_with_exit_code() { - let mut output = Vec::new(); - let result = star_setup::utils::run_command( - &["git", "clone", "not-a-real-repo"], - None, - true, - &mut output, - ); + let (result, _) = + with_io_output(|io| run_command(&["git", "clone", "not-a-real-repo"], None, true, io.output)); assert!(result.is_err()); assert!(result.unwrap_err().contains("Command failed")); } diff --git a/tests/workspace.rs b/tests/workspace.rs index 1d06a17..4117c50 100644 --- a/tests/workspace.rs +++ b/tests/workspace.rs @@ -1,7 +1,11 @@ -#[path = "workspace/clean.rs"] -mod clean; #[path = "common/mod.rs"] mod common; + +#[path = "workspace/helpers.rs"] +mod helpers; + +#[path = "workspace/clean.rs"] +mod clean; #[path = "workspace/resolve.rs"] mod resolve; #[path = "workspace/status.rs"] diff --git a/tests/workspace/clean.rs b/tests/workspace/clean.rs index a5e54cf..9f2c47d 100644 --- a/tests/workspace/clean.rs +++ b/tests/workspace/clean.rs @@ -1,78 +1,43 @@ -use crate::common::{empty_input, make_io, sink, MockRunner}; -use star_setup::{ctx::RunCtx, workspace::resolve::Workspace}; +use crate::{ + common::{with_ctx, MockRunner}, + helpers::make_workspace, +}; use std::fs; -use tempfile::TempDir; #[test] -fn test_clean_workspace_no_build_dir() { - let tmp = TempDir::new().unwrap(); - let ws = Workspace { - root: tmp.path().to_path_buf(), - repos_path: tmp.path().join("repos"), - build_path: tmp.path().join("build"), - repo_dirs: vec![], - }; - let mut input = empty_input(); - let mut output = Vec::new(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - star_setup::workspace::clean_workspace(&ws, &mut ctx).unwrap(); +fn test_workspace_clean_no_build_dir() { + let (_, output) = with_ctx(MockRunner::new(), |tmp, ctx| { + let ws = make_workspace(tmp, vec![]); + ws.clean(ctx).unwrap(); + }); let out = String::from_utf8(output).unwrap(); assert!(out.contains("does not exist")); } #[test] -fn test_clean_workspace_removes_build_dir() { - let tmp = TempDir::new().unwrap(); - let build = tmp.path().join("build"); - fs::create_dir_all(&build).unwrap(); - fs::write(build.join("dummy.txt"), "").unwrap(); - let ws = Workspace { - root: tmp.path().to_path_buf(), - repos_path: tmp.path().join("repos"), - build_path: build.clone(), - repo_dirs: vec![], - }; - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - star_setup::workspace::clean_workspace(&ws, &mut ctx).unwrap(); - assert!(!build.exists()); +fn test_workspace_clean_removes_build_dir() { + with_ctx(MockRunner::new(), |tmp, ctx| { + let ws = make_workspace(tmp, vec![]); + fs::create_dir_all(&ws.build_path).unwrap(); + fs::write(ws.build_path.join("dummy.txt"), "").unwrap(); + ws.clean(ctx).unwrap(); + assert!(!ws.build_path.exists()); + }); } #[test] -fn test_clean_workspace_dry_run() { - let tmp = TempDir::new().unwrap(); - let build = tmp.path().join("build"); - fs::create_dir_all(&build).unwrap(); - let ws = Workspace { - root: tmp.path().to_path_buf(), - repos_path: tmp.path().join("repos"), - build_path: build.clone(), - repo_dirs: vec![], - }; - let mut input = empty_input(); - let mut output = Vec::new(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: star_setup::ctx::IoCtx { - input: &mut input, - output: &mut output, - verbose: false, - timing: false, - dry_run: true, - }, - runner: &mut runner, - }; - star_setup::workspace::clean_workspace(&ws, &mut ctx).unwrap(); - assert!(build.exists()); +fn test_workspace_clean_dry_run() { + let (_, output) = with_ctx(MockRunner::new(), |tmp, ctx| { + ctx.io.dry_run = true; + + let ws = make_workspace(tmp, vec![]); + fs::create_dir_all(&ws.build_path).unwrap(); + + ws.clean(ctx).unwrap(); + + assert!(ws.build_path.exists()); + }); + let out = String::from_utf8(output).unwrap(); assert!(out.contains("Would remove directory:")); } diff --git a/tests/workspace/helpers.rs b/tests/workspace/helpers.rs new file mode 100644 index 0000000..30c6d34 --- /dev/null +++ b/tests/workspace/helpers.rs @@ -0,0 +1,11 @@ +use star_setup::workspace::Workspace; +use std::path::{Path, PathBuf}; + +pub fn make_workspace(root: &Path, repo_dirs: Vec) -> Workspace { + Workspace { + root: root.to_path_buf(), + repos_path: root.join("repos"), + build_path: root.join("build"), + repo_dirs, + } +} diff --git a/tests/workspace/resolve.rs b/tests/workspace/resolve.rs index 003fba6..e234ee5 100644 --- a/tests/workspace/resolve.rs +++ b/tests/workspace/resolve.rs @@ -1,66 +1,74 @@ use star_setup::workspace::resolve_workspace; use std::fs; -use tempfile::TempDir; + +use crate::common::with_io_dir; #[test] fn test_resolve_workspace_errors_when_missing() { - let tmp = TempDir::new().unwrap(); - let result = resolve_workspace(Some(tmp.path()), None, None); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Workspace not found")); + with_io_dir(|path, _| { + let result = resolve_workspace(Some(path), None, None); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Workspace not found")); + }); } #[test] fn test_resolve_workspace_errors_when_no_repos() { - let tmp = TempDir::new().unwrap(); - fs::create_dir_all(tmp.path().join("build-mono")).unwrap(); - let result = resolve_workspace(Some(tmp.path()), None, None); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Repos directory not found")); + with_io_dir(|path, _| { + fs::create_dir_all(path.join("build-mono")).unwrap(); + let result = resolve_workspace(Some(path), None, None); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Repos directory not found")); + }); } #[test] fn test_resolve_workspace_succeeds() { - let tmp = TempDir::new().unwrap(); - fs::create_dir_all(tmp.path().join("build-mono").join("repos")).unwrap(); - let result = resolve_workspace(Some(tmp.path()), None, None); - assert!(result.is_ok()); - let ws = result.unwrap(); - assert_eq!(ws.repo_dirs.len(), 0); + with_io_dir(|path, _| { + fs::create_dir_all(path.join("build-mono").join("repos")).unwrap(); + let result = resolve_workspace(Some(path), None, None); + assert!(result.is_ok()); + let ws = result.unwrap(); + assert_eq!(ws.repo_dirs.len(), 0); + }); } #[test] fn test_resolve_workspace_finds_repos() { - let tmp = TempDir::new().unwrap(); - let repos = tmp.path().join("build-mono").join("repos"); - fs::create_dir_all(repos.join("user-lib1").join(".git")).unwrap(); - fs::create_dir_all(repos.join("user-lib2").join(".git")).unwrap(); - let ws = resolve_workspace(Some(tmp.path()), None, None).unwrap(); - assert_eq!(ws.repo_dirs.len(), 2); + with_io_dir(|path, _| { + let repos = path.join("build-mono").join("repos"); + fs::create_dir_all(repos.join("user-lib1").join(".git")).unwrap(); + fs::create_dir_all(repos.join("user-lib2").join(".git")).unwrap(); + let ws = resolve_workspace(Some(path), None, None).unwrap(); + assert_eq!(ws.repo_dirs.len(), 2); + }); } #[test] fn test_resolve_workspace_custom_mono_dir() { - let tmp = TempDir::new().unwrap(); - fs::create_dir_all(tmp.path().join("my-workspace").join("repos")).unwrap(); - let result = resolve_workspace(Some(tmp.path()), Some("my-workspace"), None); - assert!(result.is_ok()); + with_io_dir(|path, _| { + fs::create_dir_all(path.join("my-workspace").join("repos")).unwrap(); + let result = resolve_workspace(Some(path), Some("my-workspace"), None); + assert!(result.is_ok()); + }); } #[test] fn test_resolve_workspace_custom_build_dir() { - let tmp = TempDir::new().unwrap(); - fs::create_dir_all(tmp.path().join("build-mono").join("repos")).unwrap(); - let ws = resolve_workspace(Some(tmp.path()), None, Some("out")).unwrap(); - assert!(ws.build_path.ends_with("out")); + with_io_dir(|path, _| { + fs::create_dir_all(path.join("build-mono").join("repos")).unwrap(); + let ws = resolve_workspace(Some(path), None, Some("out")).unwrap(); + assert!(ws.build_path.ends_with("out")); + }); } #[test] fn test_resolve_workspace_excludes_non_git_dirs() { - let tmp = TempDir::new().unwrap(); - let repos = tmp.path().join("build-mono").join("repos"); - fs::create_dir_all(repos.join("user-lib1").join(".git")).unwrap(); - fs::create_dir_all(repos.join("not-a-repo")).unwrap(); - let ws = resolve_workspace(Some(tmp.path()), None, None).unwrap(); - assert_eq!(ws.repo_dirs.len(), 1); + with_io_dir(|path, _| { + let repos = path.join("build-mono").join("repos"); + fs::create_dir_all(repos.join("user-lib1").join(".git")).unwrap(); + fs::create_dir_all(repos.join("not-a-repo")).unwrap(); + let ws = resolve_workspace(Some(path), None, None).unwrap(); + assert_eq!(ws.repo_dirs.len(), 1); + }); } diff --git a/tests/workspace/status.rs b/tests/workspace/status.rs index c027e3b..4d6fc5b 100644 --- a/tests/workspace/status.rs +++ b/tests/workspace/status.rs @@ -1,44 +1,30 @@ -use crate::common::{empty_input, make_io, MockRunner}; -use star_setup::{ctx::RunCtx, workspace::resolve::Workspace}; -use std::path::PathBuf; - -fn make_workspace(repo_dirs: Vec) -> Workspace { - Workspace { - root: PathBuf::from("build-mono"), - repos_path: PathBuf::from("build-mono/repos"), - build_path: PathBuf::from("build-mono/build"), - repo_dirs, - } -} +use crate::{ + common::{with_ctx, MockRunner}, + helpers::make_workspace, +}; #[test] fn test_status_workspace_empty() { - let ws = make_workspace(vec![]); - let mut input = empty_input(); - let mut output = Vec::new(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - star_setup::workspace::status_workspace(&ws, false, &mut ctx).unwrap(); + let (_, output) = with_ctx(MockRunner::new(), |tmp, ctx| { + let ws = make_workspace(tmp, vec![]); + ws.status(false, ctx).unwrap(); + }); + let out = String::from_utf8(output).unwrap(); assert!(out.contains("Workspace status:")); } #[test] fn test_status_workspace_shows_repos() { - let ws = make_workspace(vec![PathBuf::from("build-mono/repos/user-lib1")]); - let mut input = empty_input(); - let mut output = Vec::new(); let mut runner = MockRunner::new(); runner.capture_responses.push_back("main".to_string()); runner.capture_responses.push_back(String::new()); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - star_setup::workspace::status_workspace(&ws, false, &mut ctx).unwrap(); + + let (_, output) = with_ctx(runner, |tmp, ctx| { + let ws = make_workspace(tmp, vec![tmp.join("repos/user-lib1")]); + ws.status(false, ctx).unwrap(); + }); + let out = String::from_utf8(output).unwrap(); assert!(out.contains("user-lib1")); assert!(out.contains("clean")); @@ -46,19 +32,17 @@ fn test_status_workspace_shows_repos() { #[test] fn test_status_workspace_with_fetch() { - let ws = make_workspace(vec![PathBuf::from("build-mono/repos/user-lib1")]); - let mut input = empty_input(); - let mut output = Vec::new(); let mut runner = MockRunner::new(); runner.capture_responses.push_back("main".to_string()); runner.capture_responses.push_back(String::new()); runner.capture_responses.push_back("2".to_string()); runner.capture_responses.push_back("1".to_string()); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - star_setup::workspace::status_workspace(&ws, true, &mut ctx).unwrap(); + + let (runner, output) = with_ctx(runner, |tmp, ctx| { + let ws = make_workspace(tmp, vec![tmp.join("repos/user-lib1")]); + ws.status(true, ctx).unwrap(); + }); + let out = String::from_utf8(output).unwrap(); assert!(out.contains("↑2 ↓1")); assert!(runner diff --git a/tests/workspace/update.rs b/tests/workspace/update.rs index 79ad1a0..94302aa 100644 --- a/tests/workspace/update.rs +++ b/tests/workspace/update.rs @@ -1,44 +1,27 @@ -use crate::common::{empty_input, make_io, sink, MockRunner}; -use star_setup::{ctx::RunCtx, workspace::resolve::Workspace}; -use std::path::PathBuf; - -fn make_workspace(repo_dirs: Vec) -> Workspace { - Workspace { - root: PathBuf::from("build-mono"), - repos_path: PathBuf::from("build-mono/repos"), - build_path: PathBuf::from("build-mono/build"), - repo_dirs, - } -} +use crate::{ + common::{with_ctx, MockRunner}, + helpers::make_workspace, +}; #[test] fn test_update_workspace_empty() { - let ws = make_workspace(vec![]); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - star_setup::workspace::update_workspace(&ws, &mut ctx).unwrap(); + let (runner, _) = with_ctx(MockRunner::new(), |tmp, ctx| { + let ws = make_workspace(tmp, vec![]); + ws.update(ctx).unwrap(); + }); assert!(runner.calls.is_empty()); } #[test] fn test_update_workspace_pulls_each_repo() { - let ws = make_workspace(vec![ - PathBuf::from("build-mono/repos/user-lib1"), - PathBuf::from("build-mono/repos/user-lib2"), - ]); - let mut input = empty_input(); - let mut output = sink(); - let mut runner = MockRunner::new(); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - star_setup::workspace::update_workspace(&ws, &mut ctx).unwrap(); + let (runner, _) = with_ctx(MockRunner::new(), |tmp, ctx| { + let ws = make_workspace( + tmp, + vec![tmp.join("repos/user-lib1"), tmp.join("repos/user-lib2")], + ); + ws.update(ctx).unwrap(); + }); + assert_eq!(runner.calls.len(), 2); assert!(runner .calls @@ -48,20 +31,18 @@ fn test_update_workspace_pulls_each_repo() { #[test] fn test_update_workspace_continues_on_failure() { - let ws = make_workspace(vec![ - PathBuf::from("build-mono/repos/user-lib1"), - PathBuf::from("build-mono/repos/user-lib2"), - ]); - let mut input = empty_input(); - let mut output = Vec::new(); let mut runner = MockRunner::new(); runner.fail_on = Some("pull".to_string()); - let mut ctx = RunCtx { - io: make_io(&mut input, &mut output), - runner: &mut runner, - }; - let result = star_setup::workspace::update_workspace(&ws, &mut ctx); - assert!(result.is_err()); + + let (runner, output) = with_ctx(runner, |tmp, ctx| { + let ws = make_workspace( + tmp, + vec![tmp.join("repos/user-lib1"), tmp.join("repos/user-lib2")], + ); + let result = ws.update(ctx); + assert!(result.is_err()); + }); + assert_eq!(runner.calls.len(), 2); let out = String::from_utf8(output).unwrap(); assert!(out.contains("Failed to update"));