Skip to content

Commit

Permalink
test: add tests, add abstract printer (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
soanvig authored Nov 8, 2024
1 parent 828c7fe commit b10cecb
Show file tree
Hide file tree
Showing 2 changed files with 239 additions and 46 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
## 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))
280 changes: 235 additions & 45 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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<W: Write> {
writer: W,
prefix: String,
color: Option<String>,
}

impl<W: Write + std::fmt::Debug> Printer<W> {
fn new(writer: W) -> Printer<W> {
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<S: AsRef<str>>(&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<String> = env::args().collect();
let args: Vec<String> = env::args().collect();

run(args)
}

let parsed_args = parse_args(&mut args);
fn run(mut args: Vec<String>) -> 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);

Expand Down Expand Up @@ -45,6 +101,7 @@ fn print_help() {
println!("Easily run multiple long-running commands in parallel.");
println!("");
println!("Usage: runmany [RUNMANY FLAGS] [:: <COMMAND>] [:: <COMMAND>] [:: <COMMAND>]");
println!("Example: runmany :: npm build:watch :: npm serve");
println!("");
println!("Flags:");
println!(" -h, --help - print help");
Expand All @@ -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<String>) -> 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());
Expand All @@ -70,14 +127,21 @@ fn runmany_args_to_options(args: &&[String]) -> RunmanyOptions {
}
}

fn spawn_commands(commands: &[&[String]], options: &RunmanyOptions) {
fn spawn_commands(commands: &[Vec<String>], 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);
}
Expand All @@ -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<String>, 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<Mutex>> that requires locking. Maybe there is simple way to do it
fn spawn_command<W: Write + Send + std::fmt::Debug + 'static>(
command_with_args: Vec<String>,
printer: Arc<Mutex<Printer<W>>>,
) -> Arc<Mutex<Printer<W>>> {
let main_printer = printer.clone();

print_color(format!(
"[{}]: Spawning command: \"{}\"",
command_number,
main_printer.lock().unwrap().print(format!(
"Spawning command: \"{}\"",
command_with_args.join(" ")
));

Expand All @@ -113,24 +173,24 @@ fn spawn_command(command_with_args: Vec<String>, 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"));
}
});

Expand All @@ -140,29 +200,159 @@ fn spawn_command(command_with_args: Vec<String>, 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<String>) -> Vec<&'a [String]> {
args.remove(0);
printer
}

fn parse_args<'a>(args: Vec<String>) -> Vec<Vec<String>> {
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<String> {
vec.iter().map(|i| i.to_string()).collect()
}

#[test]
fn test_parse_args() {
let input = to_vec_str(vec![""]);
let expected: Vec<Vec<String>> = vec![to_vec_str(vec![""])];
assert_eq!(parse_args(input), expected);

let input = to_vec_str(vec!["-v"]);
let expected: Vec<Vec<String>> = vec![to_vec_str(vec!["-v"])];
assert_eq!(parse_args(input), expected);

let input = to_vec_str(vec!["-v", "-r"]);
let expected: Vec<Vec<String>> = 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<String>> = 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<String>> =
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<String>> = 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<String>> = 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<String>> = 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<String>> = 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());
}
}

0 comments on commit b10cecb

Please sign in to comment.