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:
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(©.src, &_dst) {
- eprintln!(" Error: {}", why);
- }
+ errors |= handle_error(copy_file(©.src, &_dst));
} else {
- if let Err(why) = stage_file(©.src, &_dst, staging_dir) {
- eprintln!(" Error: {}", why);
- }
+ errors |= handle_error(stage_file(©.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(),
+ )
}