coliru

A minimal, flexible, dotfile installer
git clone https://git.ashermorgan.net/coliru/
Log | Files | Refs | README

commit 31d0756fbab9cbe266c12b473b51f79311c85f30
parent 3b4d891b5c407796aa9e413132db07323440551b
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sat,  6 Jul 2024 13:03:08 -0700

Return proper exit codes

Diffstat:
Msrc/cli.rs | 13+++++++++++--
Msrc/core.rs | 103+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mtests/basic.rs | 56++++++++++++++++++++++++++++++++++++++++++++------------
Mtests/local.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mtests/ssh.rs | 42++++++++++++++++++++++++++++--------------
Mtests/test_utils/mod.rs | 16++++++++--------
6 files changed, 195 insertions(+), 107 deletions(-)

diff --git a/src/cli.rs b/src/cli.rs @@ -45,6 +45,15 @@ struct Args { pub fn run() { let args = Args::parse(); let manifest_path = Path::new(&args.manifest); - execute_manifest_file(&manifest_path, args.tag_rules, &args.host, - args.dry_run, args.copy); + + match execute_manifest_file(&manifest_path, args.tag_rules, &args.host, + args.dry_run, args.copy) { + Err(why) => { + eprintln!("Error: {}", why); + std::process::exit(2); + }, + Ok(minor_errors) => { + std::process::exit(if minor_errors { 1 } else { 0 }); + }, + } } diff --git a/src/core.rs b/src/core.rs @@ -1,6 +1,7 @@ //! Manifest execution functions use std::env::set_current_dir; +use std::fmt::Display; use std::path::Path; use super::manifest::{CopyLinkOptions, RunOptions, parse_manifest_file}; use super::tags::tags_match; @@ -25,51 +26,57 @@ macro_rules! check_dry_run { } } +/// Handles minor errors that occur during command execution and returns a bool +/// indicating whether an error occurred +fn handle_error<T: Display>(result: Result<(), T>) -> bool { + if let Err(why) = result { + eprintln!(" Error: {}", why); + return true; + } + false +} + /// Executes the steps in a coliru manifest file according to a set of tag rules +/// +/// Returns an Err if a critical err occurs and returns a bool indicating +/// whether any minor errors occurred otherwise pub fn execute_manifest_file(path: &Path, tag_rules: Vec<String>, host: &str, - dry_run: bool, copy: bool) { + dry_run: bool, copy: bool) -> Result<bool, String> { - let _manifest = parse_manifest_file(path); - if let Err(why) = _manifest { - eprintln!("Error: {}", why); - return; - } - let manifest = _manifest.unwrap(); + let manifest = parse_manifest_file(path).map_err(|why| why.to_string())?; + let temp_dir = tempdir().map_err(|why| why.to_string())?; + set_current_dir(manifest.base_dir).map_err(|why| why.to_string())?; - let _temp_dir = tempdir(); - if let Err(why) = _temp_dir { - eprintln!("Error: {}", why); - return; - } - let temp_dir = _temp_dir.unwrap(); - - if let Err(why) = set_current_dir(manifest.base_dir) { - eprintln!("Error: {}", why); - return; - } + let mut errors = false; for (i, step) in manifest.steps.iter().enumerate() { if !tags_match(&tag_rules, &step.tags) { continue; } let step_str = format!("[{}/{}]", i+1, manifest.steps.len()); - execute_copies(&step.copy, host, temp_dir.path(), dry_run, &step_str); + errors |= execute_copies(&step.copy, host, temp_dir.path(), dry_run, + &step_str); if !copy && host == "" { - execute_links(&step.link, dry_run, &step_str); + errors |= execute_links(&step.link, dry_run, &step_str); } else { - execute_copies(&step.link, host, temp_dir.path(), dry_run, + errors |= execute_copies(&step.link, host, temp_dir.path(), dry_run, &step_str); } - execute_runs(&step.run, &tag_rules, host, temp_dir.path(), dry_run, - &step_str); + errors |= execute_runs(&step.run, &tag_rules, host, temp_dir.path(), + dry_run, &step_str); } + + Ok(errors) } -/// Executes a set of copy commands +/// Executes a set of copy commands and returns a bool indicating whether any +/// error occurred fn execute_copies(copies: &[CopyLinkOptions], host: &str, staging_dir: &Path, - dry_run: bool, step_str: &str) { + dry_run: bool, step_str: &str) -> bool { + + let mut errors = false; for copy in copies { // Resolve relative dst paths if installing over SSH @@ -88,39 +95,43 @@ fn execute_copies(copies: &[CopyLinkOptions], host: &str, staging_dir: &Path, check_dry_run!(dry_run); if host == "" { - if let Err(why) = copy_file(&copy.src, &_dst) { - eprintln!(" Error: {}", why); - } + errors |= handle_error(copy_file(&copy.src, &_dst)); } else { - if let Err(why) = stage_file(&copy.src, &_dst, staging_dir) { - eprintln!(" Error: {}", why); - } + errors |= handle_error(stage_file(&copy.src, &_dst, staging_dir)); } } if !dry_run { - if let Err(why) = send_staged_files(staging_dir, host) { - eprintln!(" Error: {}", why); - } + errors |= handle_error(send_staged_files(staging_dir, host)); } + + errors } -/// Executes a set of link commands -fn execute_links(links: &[CopyLinkOptions], dry_run: bool, step_str: &str) { +/// Executes a set of link commands and returns a bool indicating whether any +/// error occurred +fn execute_links(links: &[CopyLinkOptions], dry_run: bool, step_str: &str) + -> bool { + + let mut errors = false; + for link in links { print!("{} Link {} to {}", step_str, link.src, link.dst); check_dry_run!(dry_run); - if let Err(why) = link_file(&link.src, &link.dst) { - eprintln!(" Error: {}", why); - } + errors |= handle_error(link_file(&link.src, &link.dst)); } + + errors } -/// Executes a set of run commands +/// Executes a set of run commands and returns a bool indicating whether any +/// error occurred fn execute_runs(runs: &[RunOptions], tag_rules: &[String], host: &str, - staging_dir: &Path, dry_run: bool, step_str: &str) { + staging_dir: &Path, dry_run: bool, step_str: &str) -> bool { + + let mut errors = false; if host != "" { // Copy scripts to remote machine @@ -144,14 +155,12 @@ fn execute_runs(runs: &[RunOptions], tag_rules: &[String], host: &str, check_dry_run!(dry_run); if host == "" { - if let Err(why) = run_command(&cmd) { - eprintln!(" Error: {}", why); - } + errors |= handle_error(run_command(&cmd)); } else { let ssh_cmd = format!("cd {} && {}", SSH_INSTALL_DIR, &cmd); - if let Err(why) = send_command(&ssh_cmd, host) { - eprintln!(" Error: {}", why); - } + errors |= handle_error(send_command(&ssh_cmd, host)); } } + + errors } diff --git a/tests/basic.rs b/tests/basic.rs @@ -32,8 +32,30 @@ Examples: # Install dotfiles from manifest.yml to user@hostname over SSH coliru manifest.yml --tag-rules A B,C ^D --host user@hostname "); - assert_eq!(stdout_to_string(&mut cmd), expected); - assert_eq!(&stderr_to_string(&mut cmd), ""); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, &expected); + assert_eq!(exitcode, Some(0)); +} + +#[test] +fn test_basic_bad_arguments() { + let (_dirs, mut cmd) = setup_e2e_local("test_basic_bad_arguments"); + cmd.args(["--foo", "bar"]); + + let expected = "\ +error: unexpected argument '--foo' found + + tip: to pass '--foo' as a value, use '-- --foo' + +Usage: coliru [OPTIONS] <MANIFEST> + +For more information, try '--help'. +"; + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected); + assert_eq!(&stdout, ""); + assert_eq!(exitcode, Some(2)); } #[test] @@ -43,8 +65,10 @@ fn test_basic_empty_manifest() { write_file(&dirs.local.join("manifest.yml"), ""); let expected = "Error: missing field `steps`\n"; - assert_eq!(&stderr_to_string(&mut cmd), expected); - assert_eq!(&stdout_to_string(&mut cmd), ""); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected); + assert_eq!(&stdout, ""); + assert_eq!(exitcode, Some(2)); } #[test] @@ -54,8 +78,10 @@ fn test_basic_missing_manifest() { cmd.args(["missing.yml"]); let expected = "Error: No such file or directory (os error 2)\n"; - assert_eq!(&stderr_to_string(&mut cmd), expected); - assert_eq!(&stdout_to_string(&mut cmd), ""); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected); + assert_eq!(&stdout, ""); + assert_eq!(exitcode, Some(2)); } #[test] @@ -66,8 +92,10 @@ fn test_basic_missing_manifest() { let expected = "Error: The system cannot find the file specified. \ (os error 2)\n"; - assert_eq!(&stderr_to_string(&mut cmd), expected); - assert_eq!(&stdout_to_string(&mut cmd), ""); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected); + assert_eq!(&stdout, ""); + assert_eq!(exitcode, Some(2)); } #[test] @@ -83,8 +111,10 @@ fn test_basic_absolute_manifest() { [2/3] Link vimrc to ~/.vimrc (DRY RUN) [2/3] Run sh script.sh arg1 linux (DRY RUN) "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run let bash_exists = dirs.home.join(".bashrc").exists(); @@ -112,8 +142,10 @@ fn test_basic_absolute_manifest() { [2/3] Link vimrc to .vimrc (DRY RUN) [2/3] Run sh script.sh arg1 linux (DRY RUN) "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run let bash_exists = dirs.local.join(".bashrc").exists(); diff --git a/tests/local.rs b/tests/local.rs @@ -18,8 +18,10 @@ fn test_local_standard() { [2/3] Run sh script.sh arg1 linux script.sh called with arg1 linux "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("bashrc"), "bash #2\n"); @@ -49,8 +51,10 @@ fn test_local_standard() { [3/3] Run script.bat arg1 windows script.bat called with arg1 windows\r "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("gitconfig"), "git #2\r\n"); @@ -79,8 +83,10 @@ fn test_local_run_alternate_tag_rules_1() { [2/3] Run sh script.sh arg1 linux ^windows script.sh called with arg1 linux ^windows "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("bashrc"), "bash #2\n"); @@ -110,8 +116,10 @@ fn test_local_run_alternate_tag_rules_2() { [2/3] Run sh script.sh arg1 macos script.sh called with arg1 macos "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("bashrc"), "bash #2\n"); @@ -141,8 +149,10 @@ fn test_local_dry_run() { [2/3] Link vimrc to ~/.vimrc (DRY RUN) [2/3] Run sh script.sh arg1 linux (DRY RUN) "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run let bash_exists = dirs.home.join(".bashrc").exists(); @@ -168,8 +178,10 @@ fn test_local_dry_run() { [3/3] Link vimrc to _vimrc (DRY RUN) [3/3] Run script.bat arg1 windows (DRY RUN) "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run let bash_exists = dirs.local.join(".bashrc").exists(); @@ -197,8 +209,10 @@ fn test_local_copy() { [2/3] Run sh script.sh arg1 linux script.sh called with arg1 linux "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("bashrc"), "bash #2\n"); @@ -228,8 +242,10 @@ fn test_local_copy() { [3/3] Run script.bat arg1 windows script.bat called with arg1 windows\r "; - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(&stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("gitconfig"), "git #2\r\n"); @@ -260,8 +276,10 @@ fn test_local_run_failure() { [2/3] Run sh script.sh arg1 linux "; let expected_stderr = " Error: Process exited with exit status: 1\n"; - assert_eq!(&stderr_to_string(&mut cmd), expected_stderr); - assert_eq!(&stdout_to_string(&mut cmd), expected_stdout); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected_stderr); + assert_eq!(&stdout, expected_stdout); + assert_eq!(exitcode, Some(1)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("bashrc"), "bash #2\n"); @@ -290,8 +308,10 @@ fn test_local_run_failure() { [3/3] Run script.bat arg1 windows "; let expected_stderr = " Error: Process exited with exit code: 1\n"; - assert_eq!(&stderr_to_string(&mut cmd), expected_stderr); - assert_eq!(&stdout_to_string(&mut cmd), expected_stdout); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected_stderr); + assert_eq!(&stdout, expected_stdout); + assert_eq!(exitcode, Some(1)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("gitconfig"), "git #2\r\n"); @@ -321,8 +341,10 @@ fn test_local_missing_file() { script.sh called with arg1 linux "; let expected_stderr = " Error: No such file or directory (os error 2)\n"; - assert_eq!(&stderr_to_string(&mut cmd), expected_stderr); - assert_eq!(&stdout_to_string(&mut cmd), expected_stdout); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected_stderr); + assert_eq!(&stdout, expected_stdout); + assert_eq!(exitcode, Some(1)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("bashrc"), "bash #2\n"); @@ -350,8 +372,10 @@ script.bat called with arg1 windows\r "; let expected_stderr = " Error: The system cannot find the file specified. \ (os error 2)\n"; - assert_eq!(&stderr_to_string(&mut cmd), expected_stderr); - assert_eq!(&stdout_to_string(&mut cmd), expected_stdout); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected_stderr); + assert_eq!(&stdout, expected_stdout); + assert_eq!(exitcode, Some(1)); // Assert files are correctly copied/linked/run write_file(&dirs.local.join("gitconfig"), "git #2\r\n"); diff --git a/tests/ssh.rs b/tests/ssh.rs @@ -19,8 +19,10 @@ fn test_ssh_standard() { [2/3] Run sh test_ssh_standard/script.sh arg1 linux on {SSH_HOST} script.sh called with arg1 linux "); - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, &expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/run let bash_contents = read_file(&dirs.ssh.join(".bashrc")); @@ -48,8 +50,10 @@ fn test_ssh_run_alternate_tag_rules_1() { [2/3] Run sh test_ssh_run_alternate_tag_rules_1/script.sh arg1 linux ^windows on {SSH_HOST} script.sh called with arg1 linux ^windows "); - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, &expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/run let bash_contents = read_file(&dirs.ssh.join(".bashrc")); @@ -78,8 +82,10 @@ fn test_ssh_run_alternate_tag_rules_2() { [2/3] Run sh test_ssh_run_alternate_tag_rules_2/script.sh arg1 macos on {SSH_HOST} script.sh called with arg1 macos "); - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, &expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/run let bash_contents = read_file(&dirs.ssh.join(".bashrc")); @@ -106,8 +112,10 @@ fn test_ssh_dry_run() { [2/3] Copy test_ssh_dry_run/script.sh to {SSH_HOST}:~/.coliru/test_ssh_dry_run/script.sh (DRY RUN) [2/3] Run sh test_ssh_dry_run/script.sh arg1 linux on {SSH_HOST} (DRY RUN) "); - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, &expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/run let bash_exists = dirs.ssh.join(".bashrc").exists(); @@ -136,8 +144,10 @@ fn test_ssh_copy() { [2/3] Run sh test_ssh_copy/script.sh arg1 linux on {SSH_HOST} script.sh called with arg1 linux "); - assert_eq!(&stderr_to_string(&mut cmd), ""); - assert_eq!(stdout_to_string(&mut cmd), expected); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, &expected); + assert_eq!(exitcode, Some(0)); // Assert files are correctly copied/run let bash_contents = read_file(&dirs.ssh.join(".bashrc")); @@ -167,8 +177,10 @@ fn test_ssh_run_failure() { [2/3] Run sh test_ssh_run_failure/script.sh arg1 linux on {SSH_HOST} "); let expected_stderr = " Error: SSH exited with exit status: 1\n"; - assert_eq!(&stderr_to_string(&mut cmd), expected_stderr); - assert_eq!(stdout_to_string(&mut cmd), expected_stdout); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected_stderr); + assert_eq!(&stdout, &expected_stdout); + assert_eq!(exitcode, Some(1)); // Assert files are correctly copied/run let bash_contents = read_file(&dirs.ssh.join(".bashrc")); @@ -197,8 +209,10 @@ fn test_ssh_missing_file() { script.sh called with arg1 linux "); let expected_stderr = " Error: No such file or directory (os error 2)\n"; - assert_eq!(&stderr_to_string(&mut cmd), expected_stderr); - assert_eq!(stdout_to_string(&mut cmd), expected_stdout); + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, expected_stderr); + assert_eq!(&stdout, &expected_stdout); + assert_eq!(exitcode, Some(1)); // Assert files are correctly copied/run let bash_contents = read_file(&dirs.ssh.join(".bashrc")); diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs @@ -196,12 +196,12 @@ pub fn read_file(path: &Path) -> String { fs::read_to_string(path).unwrap() } -/// Returns the stdout of a command as a String -pub fn stdout_to_string(cmd: &mut Command) -> String { - String::from_utf8_lossy(&cmd.output().unwrap().stdout).into_owned() -} - -/// Returns the stderr of a command as a String -pub fn stderr_to_string(cmd: &mut Command) -> String { - String::from_utf8_lossy(&cmd.output().unwrap().stderr).into_owned() +/// Run a command and return its output (stdout and stderr) and exit status +pub fn run_command(cmd: &mut Command) -> (String, String, Option<i32>) { + let output = cmd.output().unwrap(); + ( + String::from_utf8_lossy(&output.stdout).into_owned(), + String::from_utf8_lossy(&output.stderr).into_owned(), + output.status.code(), + ) }