From b10cecbc666974ae6b6d70c0a4ccfb505aa20021 Mon Sep 17 00:00:00 2001 From: Mateusz Koteja Date: Fri, 8 Nov 2024 13:36:04 +0100 Subject: [PATCH] test: add tests, add abstract printer (#9) --- README.md | 5 +- src/main.rs | 280 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 239 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 4bc00c8..4275fff 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,7 @@ If you already have a Rust environment set up, you can use the cargo install com > cargo install runmany ``` -@TODO: add releases with built binaries \ No newline at end of file +## Notes + +1. Command's `stderr` is printed to `stdout` ([issue](https://github.com/soanvig/runmany/issues/10)) +2. Command's are run directly in the system ([issue](https://github.com/soanvig/runmany/issues/2)) \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 406546e..6ff3ea5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,78 @@ use colored::*; use std::env; -use std::io::{BufRead, BufReader}; +use std::io::{stdout, BufRead, BufReader, Write}; use std::process::{Command, ExitCode, Stdio}; +use std::sync::{Arc, Mutex}; use std::thread; -static colors: [&str; 5] = ["green", "yellow", "blue", "magenta", "cyan"]; +static COLORS: [&str; 5] = ["green", "yellow", "blue", "magenta", "cyan"]; -#[derive(Clone)] +#[derive(Clone, PartialEq, Debug, Default)] struct RunmanyOptions { help: bool, version: bool, no_color: bool, } +#[derive(Clone, PartialEq, Debug)] +struct Printer { + writer: W, + prefix: String, + color: Option, +} + +impl Printer { + fn new(writer: W) -> Printer { + Printer { + writer, + prefix: "".to_string(), + color: None, + } + } + + fn set_prefix(mut self, prefix: String) -> Self { + self.prefix = prefix; + + self + } + + fn set_color(mut self, color: String) -> Self { + self.color = Some(color); + + self + } + + fn print>(&mut self, str: S) { + let str = str.as_ref(); + + let to_print = { + if let Some(color) = &self.color { + &str.color(color.to_owned()) + } else { + str + } + }; + + self.writer + .write_all( + [self.prefix.as_bytes(), to_print.as_bytes()] + .concat() + .as_slice(), + ) + .unwrap(); + self.writer.write_all(b"\n").unwrap(); + } +} + fn main() -> ExitCode { - let mut args: Vec = env::args().collect(); + let args: Vec = env::args().collect(); + + run(args) +} - let parsed_args = parse_args(&mut args); +fn run(mut args: Vec) -> ExitCode { + args.remove(0); + let parsed_args = parse_args(args); if let Some((runmany_params, commands)) = parsed_args.split_first() { let runmany_options = runmany_args_to_options(runmany_params); @@ -45,6 +101,7 @@ fn print_help() { println!("Easily run multiple long-running commands in parallel."); println!(""); println!("Usage: runmany [RUNMANY FLAGS] [:: ] [:: ] [:: ]"); + println!("Example: runmany :: npm build:watch :: npm serve"); println!(""); println!("Flags:"); println!(" -h, --help - print help"); @@ -57,7 +114,7 @@ fn print_version() { println!("v{}", version) } -fn runmany_args_to_options(args: &&[String]) -> RunmanyOptions { +fn runmany_args_to_options(args: &Vec) -> RunmanyOptions { // todo: wtf is wrong with those types :D let help = args.contains(&"-h".to_string()) || args.contains(&"--help".to_string()); let version = args.contains(&"-v".to_string()) || args.contains(&"--version".to_string()); @@ -70,14 +127,21 @@ fn runmany_args_to_options(args: &&[String]) -> RunmanyOptions { } } -fn spawn_commands(commands: &[&[String]], options: &RunmanyOptions) { +fn spawn_commands(commands: &[Vec], options: &RunmanyOptions) { let mut handles = vec![]; - for (index, &command) in commands.iter().enumerate() { - let command = command.to_vec(); + for (index, command) in commands.iter().enumerate() { + let command = command.clone(); let options = options.clone(); + let mut printer = + Printer::new(stdout()).set_color(COLORS[(index) % COLORS.len()].to_string()); + + if !options.no_color { + printer = printer.set_prefix(format!("[{}]", index + 1)); + } + let handle = thread::spawn(move || { - spawn_command(command, index + 1, options); + spawn_command(command, Arc::new(Mutex::new(printer))); }); handles.push(handle); } @@ -87,21 +151,17 @@ fn spawn_commands(commands: &[&[String]], options: &RunmanyOptions) { } } -/// command_number has to start from 1 -fn spawn_command(command_with_args: Vec, command_number: usize, options: RunmanyOptions) { - let color = colors[(command_number - 1) % colors.len()]; - - let print_color = move |str: String| { - if options.no_color { - println!("{}", str); - } else { - println!("{}", str.color(color)); - } - }; +/// command's stderr is logged to stdout +/// +/// todo: might need a refactor due to Arc> that requires locking. Maybe there is simple way to do it +fn spawn_command( + command_with_args: Vec, + printer: Arc>>, +) -> Arc>> { + let main_printer = printer.clone(); - print_color(format!( - "[{}]: Spawning command: \"{}\"", - command_number, + main_printer.lock().unwrap().print(format!( + "Spawning command: \"{}\"", command_with_args.join(" ") )); @@ -113,24 +173,24 @@ fn spawn_command(command_with_args: Vec, command_number: usize, options: .expect("Failed to start process"); let stdout = BufReader::new(child.stdout.take().expect("Cannot reference stdout")); + let stdout_printer = printer.clone(); let stdout_handle = thread::spawn(move || { for line in stdout.lines() { - print_color(format!( - "[{}]: {}", - command_number, - line.expect("stdout to be line") - )); + stdout_printer + .lock() + .unwrap() + .print(line.expect("stdout to be line")); } }); let stderr = BufReader::new(child.stderr.take().expect("Cannot reference stderr")); + let stderr_printer = printer.clone(); let stderr_handle = thread::spawn(move || { for line in stderr.lines() { - print_color(format!( - "[{}]: {}", - command_number, - line.expect("stdout to be line") - )); + stderr_printer + .lock() + .unwrap() + .print(line.expect("stdout to be line")); } }); @@ -140,29 +200,159 @@ fn spawn_command(command_with_args: Vec, command_number: usize, options: let status_code = child.wait().unwrap(); if status_code.success() { - print_color(format!( - "[{}]: Command finished successfully", - command_number - )); + main_printer + .lock() + .unwrap() + .print("Command finished successfully"); } else { - print_color(format!( - "[{}]: Command exited with status: {}", - command_number, + main_printer.lock().unwrap().print(format!( + "Command exited with status: {}", status_code .code() .map(|code| code.to_string()) .unwrap_or("unknown".to_string()) )); } -} -fn parse_args<'a>(args: &'a mut Vec) -> Vec<&'a [String]> { - args.remove(0); + printer +} +fn parse_args<'a>(args: Vec) -> Vec> { args.split(|arg| arg == "::") .enumerate() // Keep first part as possibly empty .filter(|(index, part)| *index == 0 || part.len() > 0) - .map(|(_, part)| part) + .map(|(_, part)| part.to_vec()) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + + fn to_vec_str(vec: Vec<&str>) -> Vec { + vec.iter().map(|i| i.to_string()).collect() + } + + #[test] + fn test_parse_args() { + let input = to_vec_str(vec![""]); + let expected: Vec> = vec![to_vec_str(vec![""])]; + assert_eq!(parse_args(input), expected); + + let input = to_vec_str(vec!["-v"]); + let expected: Vec> = vec![to_vec_str(vec!["-v"])]; + assert_eq!(parse_args(input), expected); + + let input = to_vec_str(vec!["-v", "-r"]); + let expected: Vec> = vec![to_vec_str(vec!["-v", "-r"])]; + assert_eq!(parse_args(input), expected); + + let input = to_vec_str(vec!["-v", "-r", "::"]); + let expected: Vec> = vec![to_vec_str(vec!["-v", "-r"])]; + assert_eq!(parse_args(input), expected); + + let input = to_vec_str(vec!["-v", "-r", "::", "command"]); + let expected: Vec> = + vec![to_vec_str(vec!["-v", "-r"]), to_vec_str(vec!["command"])]; + assert_eq!(parse_args(input), expected); + + let input = to_vec_str(vec!["-v", "-r", "::", "command", "-v"]); + let expected: Vec> = vec![ + to_vec_str(vec!["-v", "-r"]), + to_vec_str(vec!["command", "-v"]), + ]; + assert_eq!(parse_args(input), expected); + + let input = to_vec_str(vec!["-v", "-r", "::", "command", "-v", "::"]); + let expected: Vec> = vec![ + to_vec_str(vec!["-v", "-r"]), + to_vec_str(vec!["command", "-v"]), + ]; + assert_eq!(parse_args(input), expected); + + let input = to_vec_str(vec!["-v", "-r", "::", "command", "-v", "::", "command2"]); + let expected: Vec> = vec![ + to_vec_str(vec!["-v", "-r"]), + to_vec_str(vec!["command", "-v"]), + to_vec_str(vec!["command2"]), + ]; + assert_eq!(parse_args(input), expected); + + let input = to_vec_str(vec!["-v", "-r", "::", "command::xxx", "-v"]); + let expected: Vec> = vec![ + to_vec_str(vec!["-v", "-r"]), + to_vec_str(vec!["command::xxx", "-v"]), + ]; + assert_eq!(parse_args(input), expected); + } + + #[test] + fn test_runmany_args_to_options() { + let input = to_vec_str(vec!["-v"]); + let expected = RunmanyOptions { + help: false, + no_color: false, + version: true, + }; + assert_eq!(runmany_args_to_options(&input), expected); + + let input = to_vec_str(vec!["-h"]); + let expected = RunmanyOptions { + help: true, + no_color: false, + version: false, + }; + assert_eq!(runmany_args_to_options(&input), expected); + + let input = to_vec_str(vec!["--no-color"]); + let expected = RunmanyOptions { + help: false, + no_color: true, + version: false, + }; + assert_eq!(runmany_args_to_options(&input), expected); + + let input = to_vec_str(vec!["-v", "-h", "--no-color"]); + let expected = RunmanyOptions { + help: true, + no_color: true, + version: true, + }; + assert_eq!(runmany_args_to_options(&input), expected); + + let input = to_vec_str(vec!["--not-existing", "-n"]); + let expected = RunmanyOptions { + help: false, + no_color: false, + version: false, + }; + assert_eq!(runmany_args_to_options(&input), expected); + } + + #[test] + fn test_spawn_command_output() { + let printer = spawn_command( + to_vec_str(vec!["echo", "foobar"]), + Arc::new(Mutex::new(Printer::new(vec![]))), + ); + + let expected = "Spawning command: \"echo foobar\"\nfoobar\nCommand finished successfully\n"; + + assert_eq!(printer.lock().unwrap().writer, expected.as_bytes()); + } + + #[test] + fn test_spawn_command_prefixed_output() { + let printer = spawn_command( + to_vec_str(vec!["echo", "foobar"]), + Arc::new(Mutex::new( + Printer::new(vec![]).set_prefix("[foo] ".to_string()), + )), + ); + + let expected = "[foo] Spawning command: \"echo foobar\"\n[foo] foobar\n[foo] Command finished successfully\n"; + + assert_eq!(printer.lock().unwrap().writer, expected.as_bytes()); + } +}