Repository: swellaby/rusty-hook (rust)

Issue:
Feature Request - stream git hook output
## Description
I run cargo make ci-flow as part of git pre push.
that script runs many tasks and takes a bit to finish.
it would be nice to see where it is in the execution, so if you would stream the githook script output in realtime instead of buffering it into memory and flushing only at the end, it would be really help and will improve the usability.

## Value
Users for long running hooks won't think it is stuck and would get real time progress.

by the way, this crate is really great.


Pull request:
feat: support streaming hook output
## Changes
<!-- Summary of changes included in this PR -->
- [x] updates to v0.5.0 of nias (which contains a streaming command runner)
- [x] updates execution to support streaming of git hook commands
- [x] bumps version to v0.11.0 

## Related Issues
<!-- List any related/impacted issues -->
- Resolves #96
- Refs #98 


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index dd8c8d0..dfb4328 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -31,16 +31,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "nias"
-version = "0.4.0"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "rusty-hook"
-version = "0.10.3"
+version = "0.11.0"
 dependencies = [
  "ci_info 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
- "nias 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "nias 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -67,7 +67,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum envmnt 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db101f16134ca37f91a6e30c32d9df5bbfcbe8d926f0c1f03602baf2fc2f7352"
 "checksum getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
 "checksum indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7e81a7c05f79578dbc15793d8b619db9ba32b4577003ef3af1a91c416798c58d"
-"checksum nias 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f76f01f5c032698d0e4d61dd761213ebfaeb11bb22d558811dd740bf75046376"
+"checksum nias 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0"
 "checksum serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5626ac617da2f2d9c48af5515a21d5a480dbd151e01bb1c355e26a3e68113"
 "checksum toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a"
 "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
diff --git a/Cargo.toml b/Cargo.toml
index 279b381..5d33c3e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "rusty-hook"
-version = "0.10.3"
+version = "0.11.0"
 authors = ["Swellaby <opensource@swellaby.com>"]
 description = "git hook utility"
 license = "MIT"
@@ -34,12 +34,12 @@ exclude = [
 ci_info = "0.10.0"
 getopts = "0.2.21"
 toml = "0.5.6"
-nias = "0.4.0"
+nias = "0.5.0"
 
 [build-dependencies]
 ci_info = "0.10.0"
 toml = "0.5.6"
-nias = "0.4.0"
+nias = "0.5.0"
 
 [badges]
 azure-devops = { project = "swellaby/opensource", pipeline = "rusty-hook.linux-pr", build="49" }
diff --git a/LICENSE b/LICENSE
index 9883e59..9e4790f 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2018-2019 Swellaby
+Copyright (c) 2018-2020 Swellaby
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/src/git.rs b/src/git.rs
index c017e0b..d4282d3 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -5,18 +5,25 @@ pub use hooks::NO_CONFIG_FILE_FOUND_ERROR_CODE;
 pub fn get_root_directory_path<F>(
     run_command: F,
     target_directory: Option<&str>,
-) -> Result<String, String>
+) -> Result<Option<String>, Option<String>>
 where
-    F: Fn(&str, Option<&str>) -> Result<String, String>,
+    F: Fn(&str, Option<&str>, bool) -> Result<Option<String>, Option<String>>,
 {
-    run_command("git rev-parse --show-toplevel", target_directory)
+    run_command("git rev-parse --show-toplevel", target_directory, false)
 }
 
-fn get_hooks_directory<F>(run_command: F, root_directory: &str) -> Result<String, String>
+fn get_hooks_directory<F>(
+    run_command: F,
+    root_directory: &str,
+) -> Result<Option<String>, Option<String>>
 where
-    F: Fn(&str, Option<&str>) -> Result<String, String>,
+    F: Fn(&str, Option<&str>, bool) -> Result<Option<String>, Option<String>>,
 {
-    run_command("git rev-parse --git-path hooks", Some(&root_directory))
+    run_command(
+        "git rev-parse --git-path hooks",
+        Some(&root_directory),
+        false,
+    )
 }
 
 pub fn setup_hooks<F, G>(
@@ -25,12 +32,12 @@ pub fn setup_hooks<F, G>(
     root_directory_path: &str,
 ) -> Result<(), String>
 where
-    F: Fn(&str, Option<&str>) -> Result<String, String>,
+    F: Fn(&str, Option<&str>, bool) -> Result<Option<String>, Option<String>>,
     G: Fn(&str, &str, bool) -> Result<(), String>,
 {
     let hooks_directory = match get_hooks_directory(&run_command, &root_directory_path) {
-        Ok(path) => path,
-        Err(_) => return Err(String::from("Failure determining git hooks directory")),
+        Ok(Some(path)) => path,
+        _ => return Err(String::from("Failure determining git hooks directory")),
     };
     hooks::create_hook_files(write_file, &root_directory_path, &hooks_directory)
 }
diff --git a/src/hooks.rs b/src/hooks.rs
index 3ed4ae5..e574c73 100644
--- a/src/hooks.rs
+++ b/src/hooks.rs
@@ -2,7 +2,7 @@ pub const HOOK_CREATION_ERROR: &str =
     "Fatal error encountered while trying to create git hook files";
 pub const NO_CONFIG_FILE_FOUND_ERROR_CODE: i32 = 3;
 const MINIMUM_CLI_MAJOR_VERSION: i32 = 0;
-const MINIMUM_CLI_MINOR_VERSION: i32 = 10;
+const MINIMUM_CLI_MINOR_VERSION: i32 = 11;
 const MINIMUM_CLI_PATCH_VERSION: i32 = 0;
 const MINIMUM_CLI_VERSION_ALLOW_PRERELEASE: bool = false;
 
diff --git a/src/main.rs b/src/main.rs
index 7639fdd..ba8688d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -56,11 +56,15 @@ fn run(args: Vec<String>) {
         nias::get_conditional_logger(),
         &hook_name,
     ) {
-        if err == rusty_hook::NO_CONFIG_FILE_FOUND {
-            exit(rusty_hook::NO_CONFIG_FILE_FOUND_ERROR_CODE);
-        } else {
-            eprintln!("{}", err);
-            exit(1);
+        match err {
+            Some(e) if e == rusty_hook::NO_CONFIG_FILE_FOUND => {
+                exit(rusty_hook::NO_CONFIG_FILE_FOUND_ERROR_CODE);
+            }
+            Some(e) => {
+                eprintln!("{}", e);
+                exit(1);
+            }
+            None => exit(1),
         }
     }
 }
diff --git a/src/rusty_hook.rs b/src/rusty_hook.rs
index 2a71ddc..4c759d5 100644
--- a/src/rusty_hook.rs
+++ b/src/rusty_hook.rs
@@ -14,13 +14,13 @@ pub fn init_directory<F, G, H>(
     target_directory: Option<&str>,
 ) -> Result<(), String>
 where
-    F: Fn(&str, Option<&str>) -> Result<String, String>,
+    F: Fn(&str, Option<&str>, bool) -> Result<Option<String>, Option<String>>,
     G: Fn(&str, &str, bool) -> Result<(), String>,
     H: Fn(&str) -> Result<bool, ()>,
 {
     let root_directory_path = match git::get_root_directory_path(&run_command, target_directory) {
-        Ok(path) => path,
-        Err(_) => return Err(String::from("Failure determining git repo root directory")),
+        Ok(Some(path)) => path,
+        _ => return Err(String::from("Failure determining git repo root directory")),
     };
 
     if git::setup_hooks(&run_command, &write_file, &root_directory_path).is_err() {
@@ -37,7 +37,7 @@ where
 
 pub fn init<F, G, H>(run_command: F, write_file: G, file_exists: H) -> Result<(), String>
 where
-    F: Fn(&str, Option<&str>) -> Result<String, String>,
+    F: Fn(&str, Option<&str>, bool) -> Result<Option<String>, Option<String>>,
     G: Fn(&str, &str, bool) -> Result<(), String>,
     H: Fn(&str) -> Result<bool, ()>,
 {
@@ -50,16 +50,20 @@ pub fn run<F, G, H, I>(
     read_file: H,
     log: I,
     hook_name: &str,
-) -> Result<(), String>
+) -> Result<(), Option<String>>
 where
-    F: Fn(&str, Option<&str>) -> Result<String, String>,
+    F: Fn(&str, Option<&str>, bool) -> Result<Option<String>, Option<String>>,
     G: Fn(&str) -> Result<bool, ()>,
     H: Fn(&str) -> Result<String, ()>,
     I: Fn(&str, bool),
 {
     let root_directory_path = match git::get_root_directory_path(&run_command, None) {
-        Ok(path) => path,
-        Err(_) => return Err(String::from("Failure determining git repo root directory")),
+        Ok(Some(path)) => path,
+        _ => {
+            return Err(Some(String::from(
+                "Failure determining git repo root directory",
+            )));
+        }
     };
 
     let config_file_contents =
@@ -67,9 +71,9 @@ where
             Ok(contents) => contents,
             Err(err) => {
                 if err == config::NO_CONFIG_FILE_FOUND {
-                    return Err(String::from(config::NO_CONFIG_FILE_FOUND));
+                    return Err(Some(String::from(config::NO_CONFIG_FILE_FOUND)));
                 } else {
-                    return Err(String::from("Failed to parse config file"));
+                    return Err(Some(String::from("Failed to parse config file")));
                 }
             }
         };
@@ -81,7 +85,7 @@ where
             if err == config::MISSING_CONFIG_KEY {
                 return Ok(());
             }
-            return Err(String::from("Invalid rusty-hook config file"));
+            return Err(Some(String::from("Invalid rusty-hook config file")));
         }
     };
 
@@ -91,12 +95,9 @@ where
     );
     log(&message, log_details);
 
-    match run_command(&script, Some(&root_directory_path)) {
-        Err(stderr) => Err(stderr),
-        Ok(stdout) => {
-            log(&stdout, log_details);
-            Ok(())
-        }
+    match run_command(&script, Some(&root_directory_path), log_details) {
+        Err(e) => Err(e),
+        Ok(_) => Ok(()),
     }
 }
 


Test patch:
diff --git a/src/git_test.rs b/src/git_test.rs
index 33509e0..60df403 100644
--- a/src/git_test.rs
+++ b/src/git_test.rs
@@ -8,23 +8,24 @@ mod get_root_directory_path_tests {
     fn uses_git_rev_parse_top_level_command() {
         let exp = "/usr/me/foo";
         let target_dir = "";
-        let run_command = |cmd: &str, dir: Option<&str>| {
-            if cmd == "git rev-parse --show-toplevel" && dir == Some(target_dir) {
-                Ok(String::from(exp))
+        let run_command = |cmd: &str, dir: Option<&str>, stream_io: bool| {
+            if cmd == "git rev-parse --show-toplevel" && dir == Some(target_dir) && !stream_io {
+                Ok(Some(String::from(exp)))
             } else {
-                Ok(String::from(""))
+                Ok(None)
             }
         };
         let act = get_root_directory_path(run_command, Some(&target_dir));
-        assert_eq!(act.unwrap(), exp);
+        assert_eq!(act.unwrap(), Some(String::from(exp)));
     }
 
     #[test]
     fn returns_error_on_command_error() {
         let exp_err = "Ah!";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Err(String::from(exp_err));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Err(Some(String::from(exp_err)));
         let act = get_root_directory_path(run_command, None);
-        assert_eq!(act, Err(String::from(exp_err)));
+        assert_eq!(act, Err(Some(String::from(exp_err))));
     }
 }
 
@@ -36,23 +37,24 @@ mod get_hooks_directory_tests {
     fn uses_git_hooks_path_command() {
         let exp = ".git/hooks";
         let target_dir = "";
-        let run_command = |cmd: &str, dir: Option<&str>| {
-            if cmd == "git rev-parse --git-path hooks" && dir == Some(target_dir) {
-                Ok(String::from(exp))
+        let run_command = |cmd: &str, dir: Option<&str>, stream_io: bool| {
+            if cmd == "git rev-parse --git-path hooks" && dir == Some(target_dir) && !stream_io {
+                Ok(Some(String::from(exp)))
             } else {
-                Ok(String::from(""))
+                Ok(None)
             }
         };
         let act = get_hooks_directory(run_command, &target_dir);
-        assert_eq!(act.unwrap(), exp);
+        assert_eq!(act.unwrap(), Some(String::from(exp)));
     }
 
     #[test]
     fn returns_error_on_command_error() {
         let exp_err = "failed";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Err(String::from(exp_err));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Err(Some(String::from(exp_err)));
         let act = get_hooks_directory(run_command, "");
-        assert_eq!(act, Err(String::from(exp_err)));
+        assert_eq!(act, Err(Some(String::from(exp_err))));
     }
 }
 
@@ -63,7 +65,7 @@ mod setup_hooks_tests {
     #[test]
     fn errors_when_hooks_directory_unknown() {
         let exp_err = "Failure determining git hooks directory";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Err(String::from(""));
+        let run_command = |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Err(None);
         let write_file = |_path: &str, _contents: &str, _x: bool| Ok(());
         let result = setup_hooks(run_command, write_file, "");
         assert_eq!(result, Err(String::from(exp_err)));
@@ -71,8 +73,9 @@ mod setup_hooks_tests {
 
     #[test]
     fn errors_when_hook_write_fails() {
-        let run_command =
-            |_cmd: &str, _dir: Option<&str>| Ok(String::from("/usr/repos/foo/.git/hooks"));
+        let run_command = |_cmd: &str, _dir: Option<&str>, _stream_io: bool| {
+            Ok(Some(String::from("/usr/repos/foo/.git/hooks")))
+        };
         let write_file = |_path: &str, _contents: &str, _x: bool| Err(String::from(""));
         let result = setup_hooks(run_command, write_file, "");
         assert_eq!(result, Err(String::from(hooks::HOOK_CREATION_ERROR)));
@@ -82,7 +85,7 @@ mod setup_hooks_tests {
     fn creates_all_hooks() {
         let root_dir = "/usr/repos/foo";
         let git_hooks = ".git/hooks";
-        let run_command = |_c: &str, _d: Option<&str>| Ok(String::from(git_hooks));
+        let run_command = |_c: &str, _d: Option<&str>, _s: bool| Ok(Some(String::from(git_hooks)));
         let write_file = |_p: &str, _c: &str, _x: bool| Ok(());
         let result = setup_hooks(run_command, write_file, root_dir);
         assert_eq!(result, Ok(()));
diff --git a/src/hooks_test.rs b/src/hooks_test.rs
index 8dd17bd..0d37e5b 100644
--- a/src/hooks_test.rs
+++ b/src/hooks_test.rs
@@ -33,7 +33,7 @@ const EXP_CLI_SCRIPT_NAME: &str = "cli.sh";
 const EXP_SEMVER_SCRIPT_NAME: &str = "semver.sh";
 
 const EXP_MINIMUM_CLI_MAJOR_VERSION: i32 = 0;
-const EXP_MINIMUM_CLI_MINOR_VERSION: i32 = 10;
+const EXP_MINIMUM_CLI_MINOR_VERSION: i32 = 11;
 const EXP_MINIMUM_CLI_PATCH_VERSION: i32 = 0;
 const EXP_MINIMUM_CLI_VERSION_ALLOW_PRERELEASE: bool = false;
 
diff --git a/src/rusty_hook_test.rs b/src/rusty_hook_test.rs
index c1e3565..3c8399a 100644
--- a/src/rusty_hook_test.rs
+++ b/src/rusty_hook_test.rs
@@ -7,7 +7,8 @@ mod init_directory_tests {
     #[test]
     fn returns_error_when_root_directory_detect_fails() {
         let exp_err = "Failure determining git repo root directory";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Err(String::from(exp_err));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Err(Some(String::from(exp_err)));
         let write_file = |_file_path: &str, _contents: &str, _x: bool| {
             panic!("Should not get here");
         };
@@ -18,7 +19,8 @@ mod init_directory_tests {
 
     #[test]
     fn should_return_error_when_hook_creation_fails() {
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Ok(Some(String::from("")));
         let write_file = |_file_path: &str, _contents: &str, _x: bool| Err(String::from(""));
         let file_exists = |_path: &str| panic!("Should not get here");
         let result = init(run_command, write_file, file_exists);
@@ -27,7 +29,8 @@ mod init_directory_tests {
 
     #[test]
     fn should_return_error_when_config_creation_fails() {
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Ok(Some(String::from("")));
         let write_file = |_file_path: &str, _contents: &str, _x: bool| Ok(());
         let file_exists = |_path: &str| Err(());
         let result = init(run_command, write_file, file_exists);
@@ -36,7 +39,8 @@ mod init_directory_tests {
 
     #[test]
     fn should_return_ok_on_success() {
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Ok(Some(String::from("")));
         let write_file = |_file_path: &str, _contents: &str, _x: bool| Ok(());
         let file_exists = |_path: &str| Ok(false);
         let result = init(run_command, write_file, file_exists);
@@ -49,14 +53,14 @@ mod init_tests {
 
     #[test]
     fn invokes_init_directory_with_cwd() {
-        let run_command = |_cmd: &str, dir: Option<&str>| {
+        let run_command = |_cmd: &str, dir: Option<&str>, _stream_io: bool| {
             if let Some(target_dir) = dir {
                 if target_dir != "." {
-                    return Err(String::from(""));
+                    return Err(None);
                 }
-                Ok(String::from(""))
+                Ok(Some(String::from("")))
             } else {
-                Ok(String::from("."))
+                Ok(Some(String::from(".")))
             }
         };
         let write_file = |_file_path: &str, _contents: &str, _x: bool| Ok(());
@@ -73,34 +77,37 @@ mod run_tests {
     #[test]
     fn returns_error_when_root_directory_detect_fails() {
         let exp_err = "Failure determining git repo root directory";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Err(String::from(exp_err));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Err(Some(String::from(exp_err)));
         let read_file = |_file_path: &str| panic!("");
         let file_exists = |_path: &str| panic!("");
         let log = |_path: &str, _should_log: bool| panic!("");
         let result = run(run_command, file_exists, read_file, log, "");
-        assert_eq!(result, Err(String::from(exp_err)));
+        assert_eq!(result, Err(Some(String::from(exp_err))));
     }
 
     #[test]
     fn returns_error_when_config_file_missing() {
         let exp_err = config::NO_CONFIG_FILE_FOUND;
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Ok(Some(String::from("")));
         let read_file = |_file_path: &str| Err(());
         let file_exists = |_path: &str| Ok(false);
         let log = |_path: &str, _should_log: bool| panic!("");
         let result = run(run_command, file_exists, read_file, log, "");
-        assert_eq!(result, Err(String::from(exp_err)));
+        assert_eq!(result, Err(Some(String::from(exp_err))));
     }
 
     #[test]
     fn returns_error_when_config_contents_unloadable() {
         let exp_err = "Failed to parse config file";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Ok(Some(String::from("")));
         let read_file = |_file_path: &str| Err(());
         let file_exists = |_path: &str| Ok(true);
         let log = |_path: &str, _should_log: bool| panic!("");
         let result = run(run_command, file_exists, read_file, log, "");
-        assert_eq!(result, Err(String::from(exp_err)));
+        assert_eq!(result, Err(Some(String::from(exp_err))));
     }
 
     #[test]
@@ -108,7 +115,8 @@ mod run_tests {
         let contents = "[hooks]
             pre-commit = 'cargo test'
         ";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Ok(Some(String::from("")));
         let read_file = |_file_path: &str| Ok(String::from(contents));
         let file_exists = |_path: &str| Ok(true);
         let log = |_path: &str, _should_log: bool| panic!("");
@@ -120,12 +128,13 @@ mod run_tests {
     fn returns_error_on_invalid_config() {
         let exp_err = "Invalid rusty-hook config file";
         let contents = "abc";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Ok(Some(String::from("")));
         let read_file = |_file_path: &str| Ok(String::from(contents));
         let file_exists = |_path: &str| Ok(true);
         let log = |_path: &str, _should_log: bool| panic!("");
         let result = run(run_command, file_exists, read_file, log, "pre-push");
-        assert_eq!(result, Err(String::from(exp_err)));
+        assert_eq!(result, Err(Some(String::from(exp_err))));
     }
 
     #[test]
@@ -136,7 +145,12 @@ mod run_tests {
             [logging]
             verbose = false
         ";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command = |cmd: &str, _dir: Option<&str>, stream_io: bool| {
+            if cmd == "cargo test" && stream_io {
+                panic!("")
+            }
+            Ok(Some(String::from("")))
+        };
         let read_file = |_file_path: &str| Ok(String::from(contents));
         let file_exists = |_path: &str| Ok(true);
         let log = |_path: &str, should_log: bool| {
@@ -156,7 +170,12 @@ mod run_tests {
             [logging]
             verbose = true
         ";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command = |cmd: &str, _dir: Option<&str>, stream_io: bool| {
+            if cmd == "cargo test" && !stream_io {
+                panic!("")
+            }
+            Ok(Some(String::from("")))
+        };
         let read_file = |_file_path: &str| Ok(String::from(contents));
         let file_exists = |_path: &str| Ok(true);
         let log = |_path: &str, should_log: bool| {
@@ -176,7 +195,8 @@ mod run_tests {
             [logging]
             verbose = false
         ";
-        let run_command = |_cmd: &str, _dir: Option<&str>| Ok(String::from(""));
+        let run_command =
+            |_cmd: &str, _dir: Option<&str>, _stream_io: bool| Ok(Some(String::from("")));
         let read_file = |_file_path: &str| Ok(String::from(contents));
         let file_exists = |_path: &str| Ok(true);
         let log = |_path: &str, _should_log: bool| ();
@@ -193,16 +213,16 @@ mod run_tests {
             [logging]
             verbose = false
         ";
-        let run_command = |cmd: &str, _dir: Option<&str>| {
+        let run_command = |cmd: &str, _dir: Option<&str>, _stream_io: bool| {
             if cmd == "cargo test" {
-                return Err(String::from(exp_err));
+                return Err(Some(String::from(exp_err)));
             }
-            Ok(String::from(""))
+            Ok(Some(String::from("")))
         };
         let read_file = |_file_path: &str| Ok(String::from(contents));
         let file_exists = |_path: &str| Ok(true);
         let log = |_path: &str, _should_log: bool| ();
         let result = run(run_command, file_exists, read_file, log, "pre-commit");
-        assert_eq!(result, Err(String::from(exp_err)));
+        assert_eq!(result, Err(Some(String::from(exp_err))));
     }
 }