diff --git a/Cargo.lock b/Cargo.lock index c3dacc2..a8a605d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,33 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "clap" version = "4.5.16" @@ -153,6 +180,24 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "eyre" version = "0.6.12" @@ -181,12 +226,61 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -205,13 +299,41 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" + [[package]] name = "runner" version = "0.1.0" dependencies = [ + "assert_cmd", "clap", "crossbeam", "eyre", + "pretty_assertions", +] + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -231,6 +353,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -243,6 +371,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -315,3 +452,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml index ce5e3fd..8795677 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,13 @@ clap = { version = "4.5.16", features = ["derive", "env"] } crossbeam = "0.8.4" eyre = "0.6.12" +[dev-dependencies] +assert_cmd = "2.0.16" +pretty_assertions = "1.4.0" + [lints.clippy] all = "deny" pedantic = "deny" single_match_else = { level = "allow", priority = 1 } enum_glob_use = { level = "allow", priority = 1 } +needless_for_each = { level = "allow", priority = 1 } diff --git a/src/flat_map_err.rs b/src/flat_map_err.rs index 0a4d9c9..f924b4f 100644 --- a/src/flat_map_err.rs +++ b/src/flat_map_err.rs @@ -27,6 +27,7 @@ mod test { #[test] fn flat_map_err() { use super::FlatMapErr; + use pretty_assertions::assert_eq; #[derive(Debug, PartialEq, Eq)] enum Error { @@ -39,12 +40,15 @@ mod test { Error::GraveError => Err(e), }; - let success = Ok(()); - let ignorable_error = Err(Error::IgnorableError); - let grave_error = Err(Error::GraveError); - - assert_eq!(success.flat_map_err(ignore), Ok(())); - assert_eq!(ignorable_error.flat_map_err(ignore), Ok(())); - assert_eq!(grave_error.flat_map_err(ignore), Err(Error::GraveError)); + [ + (Ok(()), Ok(())), + (Err(Error::IgnorableError), Ok(())), + (Err(Error::GraveError), Err(Error::GraveError)), + ] + .into_iter() + .for_each(|(input, expected)| { + let actual = input.flat_map_err(ignore); + assert_eq!(actual, expected); + }); } } diff --git a/src/main.rs b/src/main.rs index acb8903..a86ce10 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,37 +3,129 @@ use eyre::Result; use std::path::PathBuf; /// A command runner that optionally logs the I/O streams to files. -#[derive(Debug, Parser, PartialEq)] +#[derive(Debug, Parser, PartialEq, Eq)] #[command(version)] -struct Cli { +struct Args { /// The file to log stdin to. - #[clap(short, long, env)] + #[arg(short, long, env)] in_file: Option, /// The file to log stdout to. - #[clap(short, long, env)] + #[arg(short, long, env)] out_file: Option, /// The file to log stderr to. - #[clap(short, long, env)] + #[arg(short, long, env)] err_file: Option, /// The command to run and its arguments. A command must be specified, arguments are space delimited. - #[clap(last = true, required = true, num_args = 1..)] + #[arg(last = true, required = true, num_args = 1..)] exec: Vec, } fn main() -> Result<()> { - let cli = Cli::parse(); + let args = Args::parse(); - let exec = cli.exec.iter().map(String::as_str).collect::>(); + let exec = args.exec.iter().map(String::as_str).collect::>(); let code = runner::run( exec[0], &exec[1..], - cli.in_file.as_deref(), - cli.out_file.as_deref(), - cli.err_file.as_deref(), + args.in_file.as_deref(), + args.out_file.as_deref(), + args.err_file.as_deref(), )?; std::process::exit(code); } + +#[cfg(test)] +mod test { + #[test] + fn arg_parse() { + use super::Args; + use clap::Parser; + use pretty_assertions::assert_eq; + + [ + ( + vec!["runner", "--", "echo", "hello"], + Args { + in_file: None, + out_file: None, + err_file: None, + exec: vec!["echo".to_string(), "hello".to_string()], + }, + ), + ( + vec!["runner", "--in-file", "in.txt", "--", "echo", "hello"], + Args { + in_file: Some("in.txt".into()), + out_file: None, + err_file: None, + exec: vec!["echo".to_string(), "hello".to_string()], + }, + ), + ( + vec!["runner", "--out-file", "out.txt", "--", "echo", "hello"], + Args { + in_file: None, + out_file: Some("out.txt".into()), + err_file: None, + exec: vec!["echo".to_string(), "hello".to_string()], + }, + ), + ( + vec!["runner", "--err-file", "err.txt", "--", "echo", "hello"], + Args { + in_file: None, + out_file: None, + err_file: Some("err.txt".into()), + exec: vec!["echo".to_string(), "hello".to_string()], + }, + ), + ( + vec![ + "runner", + "--in-file", + "in.txt", + "--out-file", + "out.txt", + "--", + "echo", + "hello", + ], + Args { + in_file: Some("in.txt".into()), + out_file: Some("out.txt".into()), + err_file: None, + exec: vec!["echo".to_string(), "hello".to_string()], + }, + ), + ( + vec![ + "runner", + "--in-file", + "in.txt", + "--out-file", + "out.txt", + "--err-file", + "err.txt", + "--", + "echo", + "hello", + ], + Args { + in_file: Some("in.txt".into()), + out_file: Some("out.txt".into()), + err_file: Some("err.txt".into()), + exec: vec!["echo".to_string(), "hello".to_string()], + }, + ), + ] + .into_iter() + .for_each(|(input, expected)| { + let actual = Args::try_parse_from(input).unwrap(); + assert_eq!(actual, expected); + }); + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..218e106 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,15 @@ +use assert_cmd::Command; + +#[test] +fn child_exit() { + let mut runner = Command::cargo_bin("runner").unwrap(); + let assert = runner.args(["--", "echo", "hello"]).assert(); + assert.success().stdout("hello\n"); +} + +#[test] +fn stdin_close() { + let mut runner = Command::cargo_bin("runner").unwrap(); + let assert = runner.args(["--", "cat"]).write_stdin("hello").assert(); + assert.success().stdout("hello"); +}