diff --git a/Cargo.toml b/Cargo.toml index 66c536b..dd282c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "star-setup" -version = "0.4.3" +version = "0.4.4" 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/commands/build.rs b/src/commands/build.rs index ef85824..5bd63cc 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -4,6 +4,21 @@ use crate::{ }; use std::path::Path; +/// Runs `cmd`, timing it if `ctx.flags.timing` is set. +/// # Errors +/// Returns an error if the command fails. +fn run_timed( + cmd: &[&str], + cwd: Option<&Path>, + label: &str, + ctx: &mut RunCtx<'_, '_>, +) -> Result<(), String> { + crate::time!(ctx.flags.timing, ctx.io.output, label, { + ctx.runner.run(cmd, cwd, ctx.flags, ctx.io.output)?; + }); + Ok(()) +} + /// Runs `CMake` configuration and optionally builds the project in `build_path`. /// # Errors /// Returns an error if any `CMake` command fails. @@ -21,28 +36,22 @@ pub fn cmake_build( }; cmake_cmd.extend(args.build.cmake_flags.iter().map(String::as_str)); - crate::time!(ctx.flags.timing, ctx.io.output, "CMake configure", { - ctx - .runner - .run(&cmake_cmd, Some(build_path), ctx.flags, ctx.io.output)?; - }); + run_timed(&cmake_cmd, Some(build_path), "CMake configure", ctx)?; if !args.build.no_build { writeln!(ctx.io.output, "Building project").ok(); - crate::time!(ctx.flags.timing, ctx.io.output, "CMake build", { - ctx.runner.run( - &[ - "cmake", - "--build", - ".", - "--config", - args.build.build_type.to_cmake(), - ], - Some(build_path), - ctx.flags, - ctx.io.output, - )?; - }); + run_timed( + &[ + "cmake", + "--build", + ".", + "--config", + args.build.build_type.to_cmake(), + ], + Some(build_path), + "CMake build", + ctx, + )?; } Ok(()) } @@ -69,19 +78,15 @@ pub fn meson_build( meson_cmd.push(to_str(source_path)?); meson_cmd.extend(args.build.meson_flags.iter().map(String::as_str)); - crate::time!(ctx.flags.timing, ctx.io.output, "Meson setup", { - ctx.runner.run(&meson_cmd, None, ctx.flags, ctx.io.output)?; - }); + run_timed(&meson_cmd, None, "Meson setup", ctx)?; if !args.build.no_build { writeln!(ctx.io.output, "Building project").ok(); - crate::time!(ctx.flags.timing, ctx.io.output, "Meson compile", { - ctx.runner.run( - &["meson", "compile", "-C", to_str(build_path)?], - None, - ctx.flags, - ctx.io.output, - )?; - }); + run_timed( + &["meson", "compile", "-C", to_str(build_path)?], + None, + "Meson compile", + ctx, + )?; } Ok(()) } @@ -96,24 +101,15 @@ pub fn npm_build( ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { writeln!(ctx.io.output, "Installing dependencies").ok(); - crate::time!(ctx.flags.timing, ctx.io.output, "npm install", { - ctx.runner.run( - &["npm", "install"], - Some(source_path), - ctx.flags, - ctx.io.output, - )?; - }); + run_timed(&["npm", "install"], Some(source_path), "npm install", ctx)?; if !args.build.no_build && !is_mono { writeln!(ctx.io.output, "Building project").ok(); - crate::time!(ctx.flags.timing, ctx.io.output, "npm build", { - ctx.runner.run( - &["npm", "run", "build"], - Some(source_path), - ctx.flags, - ctx.io.output, - )?; - }); + run_timed( + &["npm", "run", "build"], + Some(source_path), + "npm build", + ctx, + )?; } Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f7106fa..5eb2c1f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,7 +6,8 @@ pub mod mono; pub use mono::{ build_repo_list, create_mono_repo_cmakelists, create_mono_repo_mesonbuild, create_mono_repo_package_json, generate_mono_config, generate_watch_scripts, hoist_wraps, - mono_repo_mode, resolve_repos_for_mono, resolve_setup_paths, resolve_test_repo, + mono_repo_mode, read_package_json, resolve_repos_for_mono, resolve_setup_paths, + resolve_test_repo, wraps::{parse_project_name, parse_provide_pairs}, }; pub mod single; diff --git a/src/commands/mono/config.rs b/src/commands/mono/config.rs index d008135..c4bd0e3 100644 --- a/src/commands/mono/config.rs +++ b/src/commands/mono/config.rs @@ -1,13 +1,10 @@ use crate::{ + commands::read_package_json, ctx::{IoCtx, RunFlags}, repository::repo_dir_name, - utils::dry_run_or_do, -}; -use serde_json::{from_str, Value}; -use std::{ - fs::{self, read_to_string}, - path::Path, + utils::{dry_run_or_do, report_summary}, }; +use std::{fs::write, path::Path}; /// Shared helper to generate, write, and log monorepo build configuration files. fn write_mono_repo_config( @@ -31,31 +28,15 @@ fn write_mono_repo_config( io, flags, &format!("Generate {filename}"), - || fs::write(&file_path, content).map_err(|e| e.to_string()), + || write(&file_path, content).map_err(|e| e.to_string()), )?; - // .to_string() is required to force an allocation and satisfy line coverage tracking - if flags.dry_run { - if flags.verbose { - #[allow(clippy::to_string_in_format_args)] - writeln!( - io.output, - " Would create root {} at {}\n", - filename.to_string(), - mono_dir.display() - ) - .ok(); - } - } else { - #[allow(clippy::to_string_in_format_args)] - writeln!( - io.output, - " Created root {} at {}\n", - filename.to_string(), - mono_dir.display() - ) - .ok(); - } + report_summary( + io, + flags, + &format!("create root {filename} at {}\n", mono_dir.display()), + &format!("Created root {filename} at {}\n", mono_dir.display()), + ); Ok(()) } @@ -173,30 +154,10 @@ pub fn create_mono_repo_package_json( if i == 0 { continue; } - let pkg_path = repos_path.join(dir).join("package.json"); - if let Ok(content) = read_to_string(&pkg_path) { - match from_str::(&content) { - Ok(json) => { - if let Some(name) = json.get("name").and_then(|n| n.as_str()) { - overrides.push(format!(" \"{name}\": \"*\"")); - } - } - Err(_) => { - if flags.verbose { - writeln!( - io.output, - " Warning: malformed {dir}/package.json, skipping override" - ) - .ok(); - } - } + if let Some(json) = read_package_json(repos_path, dir, "skipping override", io, flags) { + if let Some(name) = json.get("name").and_then(|n| n.as_str()) { + overrides.push(format!(" \"{name}\": \"*\"")); } - } else if flags.verbose { - writeln!( - io.output, - " Warning: could not read {dir}/package.json, skipping override" - ) - .ok(); } } @@ -218,26 +179,15 @@ pub fn create_mono_repo_package_json( io, flags, "Generate package.json", - || fs::write(&file_path, content).map_err(|e| e.to_string()), + || write(&file_path, content).map_err(|e| e.to_string()), )?; - if flags.dry_run { - if flags.verbose { - writeln!( - io.output, - " Would create root package.json at {}\n", - mono_dir.display() - ) - .ok(); - } - } else { - writeln!( - io.output, - " Created root package.json at {}\n", - mono_dir.display() - ) - .ok(); - } + report_summary( + io, + flags, + &format!("create root package.json at {}\n", mono_dir.display()), + &format!("Created root package.json at {}\n", mono_dir.display()), + ); Ok(()) } diff --git a/src/commands/mono/mod.rs b/src/commands/mono/mod.rs index ed0aaa4..3f10964 100644 --- a/src/commands/mono/mod.rs +++ b/src/commands/mono/mod.rs @@ -14,3 +14,5 @@ pub mod setup; pub use setup::{build_repo_list, generate_mono_config}; pub mod watch; pub use watch::{generate_watch_scripts, open_watch_scripts}; +pub mod npm; +pub use npm::read_package_json; diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 7ab2fd4..017e85e 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -11,7 +11,7 @@ use crate::{ config::SetupConfig, ctx::RunCtx, repository::{clone_repos, repo_dir_name}, - utils::{dry_run::detect_or_dry_run, dry_run_or_do}, + utils::{detect_or_dry_run, dry_run_or_do}, }; use std::{ fs, diff --git a/src/commands/mono/npm.rs b/src/commands/mono/npm.rs new file mode 100644 index 0000000..019bb1d --- /dev/null +++ b/src/commands/mono/npm.rs @@ -0,0 +1,40 @@ +use crate::ctx::{IoCtx, RunFlags}; +use serde_json::{from_str, Value}; +use std::{fs::read_to_string, path::Path}; + +/// Reads and parses `{repos_path}/{dir}/package.json`, warning (if verbose) and +/// returning `None` on I/O or parse failure. +pub fn read_package_json( + repos_path: &Path, + dir: &str, + skip_suffix: &str, + io: &mut IoCtx<'_>, + flags: RunFlags, +) -> Option { + let pkg_path = repos_path.join(dir).join("package.json"); + match read_to_string(&pkg_path) { + Err(_) => { + if flags.verbose { + writeln!( + io.output, + " Warning: could not read {dir}/package.json, {skip_suffix}" + ) + .ok(); + } + None + } + Ok(content) => match from_str::(&content) { + Err(_) => { + if flags.verbose { + writeln!( + io.output, + " Warning: malformed {dir}/package.json, {skip_suffix}" + ) + .ok(); + } + None + } + Ok(json) => Some(json), + }, + } +} diff --git a/src/commands/mono/watch.rs b/src/commands/mono/watch.rs index 33fbda1..849fb69 100644 --- a/src/commands/mono/watch.rs +++ b/src/commands/mono/watch.rs @@ -1,15 +1,12 @@ use crate::{ + commands::read_package_json, ctx::{IoCtx, RunFlags}, repository::repo_dir_name, - utils::dry_run_or_do, + utils::{dry_run_or_do, report_summary}, }; use dunce::canonicalize; -use serde_json::{from_str, Value}; use std::process::Command; -use std::{ - fs::{read_to_string, write}, - path::Path, -}; +use std::{fs::write, path::Path}; /// Reads a lib's package.json and returns the appropriate watch command. fn get_watch_command( @@ -18,47 +15,21 @@ fn get_watch_command( io: &mut IoCtx<'_>, flags: RunFlags, ) -> Option { - let pkg_path = repos_path.join(dir).join("package.json"); - match read_to_string(&pkg_path) { - Err(_) => { - if flags.verbose { - writeln!( - io.output, - " Warning: could not read {dir}/package.json, skipping" - ) - .ok(); - } - None + let json = read_package_json(repos_path, dir, "skipping", io, flags)?; + let scripts = json.get("scripts")?; + if scripts.get("watch").is_some() { + Some(format!("npm --workspace=repos/{dir} run watch")) + } else if scripts.get("build").is_some() { + Some(format!("npm --workspace=repos/{dir} run build -- --watch")) + } else { + if flags.verbose { + writeln!( + io.output, + " Warning: {dir} has no watch or build script, skipping" + ) + .ok(); } - Ok(content) => match from_str::(&content) { - Err(_) => { - if flags.verbose { - writeln!( - io.output, - " Warning: malformed {dir}/package.json, skipping" - ) - .ok(); - } - None - } - Ok(json) => { - let scripts = json.get("scripts")?; - if scripts.get("watch").is_some() { - Some(format!("npm --workspace=repos/{dir} run watch")) - } else if scripts.get("build").is_some() { - Some(format!("npm --workspace=repos/{dir} run build -- --watch")) - } else { - if flags.verbose { - writeln!( - io.output, - " Warning: {dir} has no watch or build script, skipping" - ) - .ok(); - } - None - } - } - }, + None } } @@ -78,26 +49,26 @@ pub fn generate_watch_scripts( return Ok(false); } - let ps1_lines: Vec = lib_dirs + let watch_cmds: Vec = lib_dirs .iter() - .filter_map(|d| { - get_watch_command(repos_path, d, io, flags).map(|cmd| { - format!( - "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'cd \"{}\"; {cmd}'", - mono_dir.display() - ) - }) - }) + .filter_map(|d| get_watch_command(repos_path, d, io, flags)) .collect(); - let sh_lines: Vec = lib_dirs + let ps1_lines: Vec = watch_cmds .iter() - .filter_map(|d| { - get_watch_command(repos_path, d, io, flags) - .map(|cmd| format!("cd \"{}\" && {cmd} &", mono_dir.display())) + .map(|cmd| { + format!( + "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'cd \"{}\"; {cmd}'", + mono_dir.display() + ) }) .collect(); + let sh_lines: Vec = watch_cmds + .iter() + .map(|cmd| format!("cd \"{}\" && {cmd} &", mono_dir.display())) + .collect(); + let ps1_content = format!("# Watch all lib repositories\n{}\n", ps1_lines.join("\n")); let sh_content = format!( "#!/bin/bash\ntrap 'kill $(jobs -p)' EXIT\n# Watch all lib repositories\n{}\nwait\n", @@ -120,23 +91,12 @@ pub fn generate_watch_scripts( }, )?; - if flags.dry_run { - if flags.verbose { - writeln!( - io.output, - " Would generate watch scripts at {}", - mono_dir.display() - ) - .ok(); - } - } else { - writeln!( - io.output, - " Generated watch scripts at {}", - mono_dir.display() - ) - .ok(); - } + report_summary( + io, + flags, + &format!("generate watch scripts at {}", mono_dir.display()), + &format!("Generated watch scripts at {}", mono_dir.display()), + ); if flags.verbose { writeln!(io.output, " Watching {} libraries:", lib_dirs.len()).ok(); diff --git a/src/commands/mono/wraps.rs b/src/commands/mono/wraps.rs index 0cb3626..7dd8a63 100644 --- a/src/commands/mono/wraps.rs +++ b/src/commands/mono/wraps.rs @@ -127,8 +127,8 @@ pub fn hoist_wraps( "Write wrap", || fs::write(&wrap_path, &wrap_content).map_err(|e| e.to_string()), )?; - if flags.dry_run { - if flags.verbose { + if flags.verbose { + if flags.dry_run { writeln!( io.output, " Would generate wrap: {canonical_name}.wrap -> {dir_name}" diff --git a/src/commands/single.rs b/src/commands/single.rs index d016866..9f4255b 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -7,7 +7,7 @@ use crate::{ clone_repo, repo_dir_name, ExistsAction::{Skip, Update}, }, - utils::dry_run::detect_or_dry_run, + utils::detect_or_dry_run, }; use std::{path::Path, time::Instant}; diff --git a/src/config/crud.rs b/src/config/crud.rs index ad1b364..dd9cfae 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -1,6 +1,6 @@ use crate::{ cli::BuildType::Debug, - config::{format_entry, save_config, ConfigEntry, SetupConfig}, + config::{format_entry, persist_or_dry_run, save_config, ConfigEntry, SetupConfig}, ctx::{IoCtx, RunFlags}, prompts::confirm_abort, }; @@ -110,26 +110,24 @@ pub fn add_config( return Ok(()); } - if flags.dry_run { - writeln!( - io.output, - " Would save configuration '{name}' to config file" - ) - .ok(); - } else { - insert_config(config, name, entry); - let path: PathBuf = save_config(config, flags.timing, &mut io.output)?; - writeln!( - io.output, - " Configuration '{name}' added successfully to {}", - path.display() - ) - .ok(); - let e: &ConfigEntry = &config.configs[name]; - writeln!(io.output, " Configuration details:").ok(); - write!(io.output, " {}", format_entry(e)).ok(); - } - Ok(()) + persist_or_dry_run( + config, + flags, + io, + &format!("Would save configuration '{name}' to config file"), + |config| insert_config(config, name, entry), + |config, path, io| { + writeln!( + io.output, + " Configuration '{name}' added successfully to {}", + path.display() + ) + .ok(); + let e: &ConfigEntry = &config.configs[name]; + writeln!(io.output, " Configuration details:").ok(); + write!(io.output, " {}", format_entry(e)).ok(); + }, + ) } /// Removes a named configuration entry. @@ -155,17 +153,17 @@ pub fn remove_config( return Ok(()); } - if flags.dry_run { - writeln!( - io.output, - " Would remove configuration '{name}' from config file" - ) - .ok(); - } else { - remove_config_entry(config, name); - let path = save_config(config, flags.timing, &mut io.output)?; - writeln!(io.output, " Config '{name}' was successfully removed").ok(); - writeln!(io.output, " Configuration saved to: {}", path.display()).ok(); - } - Ok(()) + persist_or_dry_run( + config, + flags, + io, + &format!("Would remove configuration '{name}' from config file"), + |config| { + remove_config_entry(config, name); + }, + |_config, path, io| { + writeln!(io.output, " Config '{name}' was successfully removed").ok(); + writeln!(io.output, " Configuration saved to: {}", path.display()).ok(); + }, + ) } diff --git a/src/config/io.rs b/src/config/io.rs index 83f484e..534062f 100644 --- a/src/config/io.rs +++ b/src/config/io.rs @@ -1,4 +1,7 @@ -use crate::config::SetupConfig; +use crate::{ + config::SetupConfig, + ctx::{IoCtx, RunFlags}, +}; use serde_json::{from_str, to_string_pretty}; use std::{ fs::{read_to_string, write}, @@ -118,3 +121,26 @@ pub fn save_config( Ok(path) } + +/// Runs `mutate` and saves the config, or prints `would_msg` in dry-run mode +/// without mutating or saving. Calls `on_saved` with the saved path after a +/// successful save. +/// # Errors +/// Returns an error if saving the config file fails. +pub fn persist_or_dry_run( + config: &mut SetupConfig, + flags: RunFlags, + io: &mut IoCtx<'_>, + would_msg: &str, + mutate: impl FnOnce(&mut SetupConfig), + on_saved: impl FnOnce(&SetupConfig, &Path, &mut IoCtx<'_>), +) -> Result<(), String> { + if flags.dry_run { + writeln!(io.output, " {would_msg}").ok(); + } else { + mutate(config); + let path = save_config(config, flags.timing, &mut io.output)?; + on_saved(config, &path, io); + } + Ok(()) +} diff --git a/src/config/mod.rs b/src/config/mod.rs index fad42fe..b74b6a5 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -5,6 +5,6 @@ pub use crud::{ pub mod display; pub use display::{format_entry, list_configs}; pub mod io; -pub use io::{config_locations, load_config, save_config}; +pub use io::{config_locations, load_config, persist_or_dry_run, save_config}; pub mod types; pub use types::{ConfigEntry, SetupConfig}; diff --git a/src/profile/crud.rs b/src/profile/crud.rs index 30ea6a1..bbfefd5 100644 --- a/src/profile/crud.rs +++ b/src/profile/crud.rs @@ -1,5 +1,5 @@ use crate::{ - config::{save_config, SetupConfig}, + config::{persist_or_dry_run, SetupConfig}, ctx::{IoCtx, RunFlags}, profile::print_profile_details, prompts::confirm_abort, @@ -49,14 +49,17 @@ pub fn add_profile( return Ok(()); } - if flags.dry_run { - writeln!(io.output, " Would save profile '{name}' to config file").ok(); - } else { - insert_profile(config, &name, repos.clone()); - let path = save_config(config, flags.timing, &mut io.output)?; - writeln!(io.output, " Profile '{name}' added successfully").ok(); - writeln!(io.output, " Configuration saved to: {}", path.display()).ok(); - } + persist_or_dry_run( + config, + flags, + io, + &format!("Would save profile '{name}' to config file"), + |config| insert_profile(config, &name, repos.clone()), + |_config, path, io| { + writeln!(io.output, " Profile '{name}' added successfully").ok(); + writeln!(io.output, " Configuration saved to: {}", path.display()).ok(); + }, + )?; print_profile_details(io.output, "Profile details:", "Repositories", &repos); writeln!( io.output, @@ -99,17 +102,17 @@ pub fn remove_profile( return Ok(()); } - if flags.dry_run { - writeln!( - io.output, - " Would remove profile '{name}' from config file" - ) - .ok(); - } else { - remove_profile_entry(config, name); - let path = save_config(config, flags.timing, &mut io.output)?; - writeln!(io.output, " Profile '{name}' removed successfully").ok(); - writeln!(io.output, " Configuration saved to: {}", path.display()).ok(); - } - Ok(()) + persist_or_dry_run( + config, + flags, + io, + &format!("Would remove profile '{name}' from config file"), + |config| { + remove_profile_entry(config, name); + }, + |_config, path, io| { + writeln!(io.output, " Profile '{name}' removed successfully").ok(); + writeln!(io.output, " Configuration saved to: {}", path.display()).ok(); + }, + ) } diff --git a/src/repository/clone.rs b/src/repository/clone.rs index aaf0f7f..1a3ce8e 100644 --- a/src/repository/clone.rs +++ b/src/repository/clone.rs @@ -116,9 +116,7 @@ pub fn clone_repos( writeln!(ctx.io.output, "Cloning repositories").ok(); crate::time!(ctx.flags.timing, ctx.io.output, "Clone", { for repo in repos { - if ctx.flags.verbose { - writeln!(ctx.io.output, " Cloning {}", repo_dir_name(repo)).ok(); - } + writeln!(ctx.io.output, " Cloning {}", repo_dir_name(repo)).ok(); clone_repo( repo, target_dir, diff --git a/src/utils/dry_run.rs b/src/utils/dry_run.rs index 7afdbd8..07fcc85 100644 --- a/src/utils/dry_run.rs +++ b/src/utils/dry_run.rs @@ -4,6 +4,17 @@ use crate::{ }; use std::path::Path; +/// Prints a "Would ..." summary in verbose dry-run mode, or a completed-action summary otherwise. +pub fn report_summary(io: &mut IoCtx<'_>, flags: RunFlags, would_msg: &str, done_msg: &str) { + if flags.dry_run { + if flags.verbose { + writeln!(io.output, " Would {would_msg}").ok(); + } + } else { + writeln!(io.output, " {done_msg}").ok(); + } +} + /// Prints a dry-run message or executes `op`, timing it if not a dry run. /// # Errors /// Returns an error if `op` fails. diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 47c2f12..c62b1be 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -3,5 +3,5 @@ pub use prerequisites::check_prerequisites; pub mod process; pub use process::run_command; pub mod dry_run; +pub use dry_run::{detect_or_dry_run, dry_run_or_do, report_summary}; pub mod timing; -pub use dry_run::dry_run_or_do; diff --git a/tests/commands/mono.rs b/tests/commands/mono.rs index d44f962..90c9a89 100644 --- a/tests/commands/mono.rs +++ b/tests/commands/mono.rs @@ -2,6 +2,8 @@ mod config; #[path = "mono/mode.rs"] mod mode; +#[path = "mono/npm.rs"] +mod npm; #[path = "mono/resolve.rs"] mod resolve; #[path = "mono/setup.rs"] diff --git a/tests/commands/mono/npm.rs b/tests/commands/mono/npm.rs new file mode 100644 index 0000000..a7bcc51 --- /dev/null +++ b/tests/commands/mono/npm.rs @@ -0,0 +1,67 @@ +use crate::common::with_io_output; +use star_setup::{commands::read_package_json, ctx::RunFlags}; +use std::fs::{create_dir_all, write}; +use tempfile::TempDir; + +fn flags(verbose: bool) -> RunFlags { + RunFlags { + verbose, + timing: false, + dry_run: false, + } +} + +#[test] +fn test_read_package_json_valid_returns_json_silently() { + let tmp = TempDir::new().unwrap(); + let repos = tmp.path(); + create_dir_all(repos.join("user-lib1")).unwrap(); + write( + repos.join("user-lib1").join("package.json"), + r#"{"name": "@user/lib1"}"#, + ) + .unwrap(); + + let (result, out) = + with_io_output(|io| read_package_json(repos, "user-lib1", "skipping", io, flags(true))); + assert!(result.is_some()); + assert_eq!(out, ""); +} + +#[test] +fn test_read_package_json_missing_file_warns_when_verbose() { + let tmp = TempDir::new().unwrap(); + let (result, out) = with_io_output(|io| { + read_package_json( + tmp.path(), + "user-lib1", + "skipping override", + io, + flags(true), + ) + }); + assert!(result.is_none()); + assert!(out.contains("could not read user-lib1/package.json, skipping override")); +} + +#[test] +fn test_read_package_json_missing_file_silent_when_quiet() { + let tmp = TempDir::new().unwrap(); + let (result, out) = + with_io_output(|io| read_package_json(tmp.path(), "user-lib1", "skipping", io, flags(false))); + assert!(result.is_none()); + assert_eq!(out, ""); +} + +#[test] +fn test_read_package_json_malformed_warns_when_verbose() { + let tmp = TempDir::new().unwrap(); + let repos = tmp.path(); + create_dir_all(repos.join("user-lib1")).unwrap(); + write(repos.join("user-lib1").join("package.json"), "{ not json").unwrap(); + + let (result, out) = + with_io_output(|io| read_package_json(repos, "user-lib1", "skipping", io, flags(true))); + assert!(result.is_none()); + assert!(out.contains("malformed user-lib1/package.json, skipping")); +} diff --git a/tests/repository/clone.rs b/tests/repository/clone.rs index 3eac8c0..1680841 100644 --- a/tests/repository/clone.rs +++ b/tests/repository/clone.rs @@ -156,7 +156,7 @@ fn test_clone_repos_empty() { } #[test] -fn test_clone_repos_per_repo_lines_require_verbose() { +fn test_clone_repos_per_repo_lines_always_shown() { let repos = vec!["user/repo1".to_string(), "user/repo2".to_string()]; let (_, output) = with_ctx(MockRunner::new(), |tmp_path, ctx| { @@ -164,7 +164,7 @@ fn test_clone_repos_per_repo_lines_require_verbose() { }); let out = String::from_utf8(output).unwrap(); assert!(out.contains("Cloning repositories")); - assert!(!out.contains("Cloning user-repo1")); + assert!(out.contains("Cloning user-repo1")); assert!(!out.contains("Finished cloning (2 repositories)")); let (_, output) = with_ctx(MockRunner::new(), |tmp_path, ctx| { @@ -172,6 +172,7 @@ fn test_clone_repos_per_repo_lines_require_verbose() { clone_repos(&repos, tmp_path, false, false, ctx).unwrap(); }); let out = String::from_utf8(output).unwrap(); + assert!(out.contains("Cloning repositories")); assert!(out.contains("Cloning user-repo1")); assert!(out.contains("Finished cloning (2 repositories)")); } diff --git a/tests/utils.rs b/tests/utils.rs index 14bc18f..632201a 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -1,6 +1,8 @@ #[path = "common/mod.rs"] mod common; +#[path = "utils/dry_run.rs"] +mod dry_run; #[path = "utils/prerequisites.rs"] mod prerequisites; #[path = "utils/process.rs"] diff --git a/tests/utils/dry_run.rs b/tests/utils/dry_run.rs new file mode 100644 index 0000000..050496b --- /dev/null +++ b/tests/utils/dry_run.rs @@ -0,0 +1,44 @@ +use crate::common::with_io_output; +use star_setup::{ctx::RunFlags, utils::report_summary}; + +/* ===== HELPERS ===== */ +fn flags(dry_run: bool, verbose: bool) -> RunFlags { + RunFlags { + verbose, + timing: false, + dry_run, + } +} + +/* ===== REPORT_SUMMARY ===== */ +#[test] +fn test_report_summary_dry_run_verbose_prints_would() { + let ((), out) = with_io_output(|io| { + report_summary(io, flags(true, true), "create thing", "Created thing"); + }); + assert_eq!(out, " Would create thing\n"); +} + +#[test] +fn test_report_summary_dry_run_quiet_prints_nothing() { + let ((), out) = with_io_output(|io| { + report_summary(io, flags(true, false), "create thing", "Created thing"); + }); + assert_eq!(out, ""); +} + +#[test] +fn test_report_summary_real_verbose_prints_done() { + let ((), out) = with_io_output(|io| { + report_summary(io, flags(false, true), "create thing", "Created thing"); + }); + assert_eq!(out, " Created thing\n"); +} + +#[test] +fn test_report_summary_real_quiet_still_prints_done() { + let ((), out) = with_io_output(|io| { + report_summary(io, flags(false, false), "create thing", "Created thing"); + }); + assert_eq!(out, " Created thing\n"); +}