bin: Rewrite CLI tests to not require a binary

Before this patch, CLI tests required a successful debug build of
`rad-tui`. This rewrites the tests such that they still do what they did
before: testing argument processing. But without relying on a binary.
This commit is contained in:
Erik Kundt
2025-08-28 11:18:19 +02:00
parent 86e34ac2b4
commit b4dd057f45
4 changed files with 411 additions and 470 deletions
+125 -122
View File
@@ -41,19 +41,22 @@ Other options
"#,
};
#[derive(Debug, PartialEq)]
pub struct Options {
op: Operation,
}
#[derive(Debug, PartialEq)]
pub enum Operation {
List { opts: ListOptions },
Other { args: Vec<OsString> },
Unknown { args: Vec<OsString> },
}
#[derive(PartialEq, Eq)]
pub enum OperationName {
List,
Other,
Unknown,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
@@ -133,7 +136,14 @@ impl Args for Options {
Value(val) if op == OperationName::List => match val.to_string_lossy().as_ref() {
"list" => op = OperationName::List,
_ => op = OperationName::Other,
_ => {
op = OperationName::Unknown;
// Only enable forwarding if it was not already disabled explicitly
forward = match forward {
Some(false) => Some(false),
_ => Some(true),
};
}
},
_ => {
if op == OperationName::List {
@@ -169,6 +179,7 @@ impl Args for Options {
OperationName::List if !forward => Operation::List {
opts: ListOptions { json, ..list_opts },
},
OperationName::Unknown if !forward => Operation::Unknown { args },
_ => Operation::Other { args },
};
@@ -228,6 +239,9 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul
Operation::Other { args } => {
let _ = crate::terminal::run_rad(Some("inbox"), &args);
}
Operation::Unknown { .. } => {
anyhow::bail!("unknown operation provided");
}
}
Ok(())
@@ -235,149 +249,138 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul
#[cfg(test)]
mod cli {
use std::process::Command;
use radicle_cli::terminal::args::Error;
use radicle_cli::terminal::Args;
use assert_cmd::prelude::*;
use predicates::prelude::*;
mod assert {
use predicates::prelude::*;
use predicates::str::ContainsPredicate;
pub fn is_tui() -> ContainsPredicate {
predicate::str::contains("Inappropriate ioctl for device")
}
pub fn is_rad_manual() -> ContainsPredicate {
predicate::str::contains("rad-inbox")
}
pub fn is_inbox_help() -> ContainsPredicate {
predicate::str::contains("Terminal interfaces for notifications")
}
}
use super::{ListOptions, Operation, Options};
#[test]
#[ignore = "requires binary"]
fn empty_operation() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_operation_should_default_to_list_and_not_be_forwarded(
) -> Result<(), Box<dyn std::error::Error>> {
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.arg("inbox");
cmd.assert().failure().stdout(assert::is_tui());
let args = vec![];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore]
fn empty_operation_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_operation_with_help_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["--help".into()];
let expected_op = Operation::Other { args: args.clone() };
cmd.arg("inbox");
cmd.assert().failure().stdout(assert::is_tui());
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_with_help_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["inbox", "--help"]);
cmd.assert().success().stdout(assert::is_rad_manual());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_with_help_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["inbox", "--help", "--no-forward"]);
cmd.assert().success().stdout(assert::is_inbox_help());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_is_not_forwarded_explicitly() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["inbox", "--no-forward"]);
cmd.assert().failure().stdout(assert::is_tui());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["inbox", "list"]);
cmd.assert().failure().stdout(assert::is_tui());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_is_not_forwarded_explicitly() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["inbox", "list", "--no-forward"]);
cmd.assert().failure().stdout(assert::is_tui());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_with_help_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["inbox", "list", "--help"]);
cmd.assert().success().stdout(assert::is_rad_manual());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_with_help_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["inbox", "list", "--help", "--no-forward"]);
cmd.assert().success().stdout(assert::is_inbox_help());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_with_help_is_not_forwarded_reversed() -> Result<(), Box<dyn std::error::Error>>
fn empty_operation_with_help_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>>
{
let mut cmd = Command::cargo_bin("rad-tui")?;
let args = vec!["--help".into(), "--no-forward".into()];
cmd.args(["inbox", "list", "--no-forward", "--help"]);
cmd.assert().success().stdout(assert::is_inbox_help());
let actual = Options::from_args(args).unwrap_err().downcast::<Error>()?;
assert!(matches!(actual, Error::Help));
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn unknown_operation_show_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_operation_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.args(["inbox", "show"]);
cmd.assert()
.success()
.stdout(predicate::str::contains("a Notification ID must be given"));
let args = vec!["--no-forward".into()];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn list_operation_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let expected_op = Operation::List {
opts: ListOptions::default(),
};
let args = vec!["list".into()];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn list_operation_should_not_be_forwarded_explicitly() -> Result<(), Box<dyn std::error::Error>>
{
let expected_op = Operation::List {
opts: ListOptions::default(),
};
let args = vec!["list".into(), "--no-forward".into()];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn list_operation_with_help_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["list".into(), "--help".into()];
let expected_op = Operation::Other { args: args.clone() };
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn list_operation_with_help_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>>
{
let args = vec!["list".into(), "--help".into(), "--no-forward".into()];
let actual = Options::from_args(args).unwrap_err().downcast::<Error>()?;
assert!(matches!(actual, Error::Help));
Ok(())
}
#[test]
fn list_operation_with_help_should_not_be_forwarded_reversed(
) -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["list".into(), "--no-forward".into(), "--help".into()];
let actual = Options::from_args(args).unwrap_err().downcast::<Error>()?;
assert!(matches!(actual, Error::Help));
Ok(())
}
#[test]
fn unknown_operation_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["operation".into()];
let expected_op = Operation::Other { args: args.clone() };
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn unknown_operation_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["operation".into(), "--no-forward".into()];
let expected_op = Operation::Unknown { args: args.clone() };
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
+113 -136
View File
@@ -48,20 +48,23 @@ Other options
"#,
};
#[derive(Debug, PartialEq)]
pub struct Options {
op: Operation,
repo: Option<RepoId>,
}
#[derive(Debug, PartialEq)]
pub enum Operation {
List { opts: ListOptions },
Other { args: Vec<OsString> },
Unknown { args: Vec<OsString> },
}
#[derive(PartialEq, Eq)]
pub enum OperationName {
List,
Other,
Unknown,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
@@ -145,7 +148,14 @@ impl Args for Options {
Value(val) if op == OperationName::List => match val.to_string_lossy().as_ref() {
"list" => op = OperationName::List,
_ => op = OperationName::Other,
_ => {
op = OperationName::Unknown;
// Only enable forwarding if it was not already disabled explicitly
forward = match forward {
Some(false) => Some(false),
_ => Some(true),
};
}
},
_ => {
if op == OperationName::List {
@@ -169,6 +179,7 @@ impl Args for Options {
OperationName::List if !forward => Operation::List {
opts: ListOptions { json, ..list_opts },
},
OperationName::Unknown if !forward => Operation::Unknown { args },
_ => Operation::Other { args },
};
@@ -231,6 +242,9 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul
Operation::Other { args } => {
let _ = crate::terminal::run_rad(Some("issue"), &args);
}
Operation::Unknown { .. } => {
anyhow::bail!("unknown operation provided");
}
}
Ok(())
@@ -238,175 +252,138 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul
#[cfg(test)]
mod cli {
use std::process::Command;
use radicle_cli::terminal::args::Error;
use radicle_cli::terminal::Args;
use assert_cmd::prelude::*;
use predicates::prelude::*;
mod assert {
use predicates::prelude::*;
use predicates::str::ContainsPredicate;
pub fn is_tui() -> ContainsPredicate {
predicate::str::contains("Inappropriate ioctl for device")
}
pub fn is_rad_manual() -> ContainsPredicate {
predicate::str::contains("rad-issue")
}
pub fn is_issue_help() -> ContainsPredicate {
predicate::str::contains("Terminal interfaces for issues")
}
}
use super::{ListOptions, Operation, Options};
#[test]
#[ignore = "requires binary"]
fn empty_operation() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_operation_should_default_to_list_and_not_be_forwarded(
) -> Result<(), Box<dyn std::error::Error>> {
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.arg("issue");
cmd.assert().failure().stdout(assert::is_tui());
let args = vec![];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_operation_with_help_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["--help".into()];
let expected_op = Operation::Other { args: args.clone() };
cmd.arg("issue");
cmd.assert().failure().stdout(assert::is_tui());
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_with_help_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["issue", "--help"]);
cmd.assert().success().stdout(assert::is_rad_manual());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_with_help_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["issue", "--help", "--no-forward"]);
cmd.assert().success().stdout(assert::is_issue_help());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_is_not_forwarded_explicitly() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["issue", "--no-forward"]);
cmd.assert().failure().stdout(assert::is_tui());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["issue", "list"]);
cmd.assert().failure().stdout(assert::is_tui());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_is_not_forwarded_explicitly() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["issue", "list", "--no-forward"]);
cmd.assert().failure().stdout(assert::is_tui());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_with_help_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["issue", "list", "--help"]);
cmd.assert().success().stdout(assert::is_rad_manual());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_with_help_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["issue", "list", "--help", "--no-forward"]);
cmd.assert().success().stdout(assert::is_issue_help());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_with_help_is_not_forwarded_reversed() -> Result<(), Box<dyn std::error::Error>>
fn empty_operation_with_help_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>>
{
let mut cmd = Command::cargo_bin("rad-tui")?;
let args = vec!["--help".into(), "--no-forward".into()];
cmd.args(["issue", "list", "--no-forward", "--help"]);
cmd.assert().success().stdout(assert::is_issue_help());
let actual = Options::from_args(args).unwrap_err().downcast::<Error>()?;
assert!(matches!(actual, Error::Help));
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn unknown_operation_show_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_operation_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.args(["issue", "show"]);
cmd.assert().success().stdout(predicate::str::contains(
"Error: rad issue: an issue must be provided",
));
let args = vec!["--no-forward".into()];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn unknown_operation_edit_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn list_operation_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.args(["issue", "edit"]);
cmd.assert().success().stdout(predicate::str::contains(
"Error: rad issue: an issue must be provided",
));
let args = vec!["list".into()];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn unknown_operation_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn list_operation_should_not_be_forwarded_explicitly() -> Result<(), Box<dyn std::error::Error>>
{
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.args(["issue", "operation", "--no-forward"]);
cmd.assert().success().stdout(predicate::str::contains(
"Error: rad issue: unknown operation",
));
let args = vec!["list".into(), "--no-forward".into()];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn list_operation_with_help_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["list".into(), "--help".into()];
let expected_op = Operation::Other { args: args.clone() };
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn list_operation_with_help_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>>
{
let args = vec!["list".into(), "--help".into(), "--no-forward".into()];
let actual = Options::from_args(args).unwrap_err().downcast::<Error>()?;
assert!(matches!(actual, Error::Help));
Ok(())
}
#[test]
fn list_operation_with_help_should_not_be_forwarded_reversed(
) -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["list".into(), "--no-forward".into(), "--help".into()];
let actual = Options::from_args(args).unwrap_err().downcast::<Error>()?;
assert!(matches!(actual, Error::Help));
Ok(())
}
#[test]
fn unknown_operation_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["operation".into()];
let expected_op = Operation::Other { args: args.clone() };
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn unknown_operation_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["operation".into(), "--no-forward".into()];
let expected_op = Operation::Unknown { args: args.clone() };
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
+113 -137
View File
@@ -55,14 +55,17 @@ Other options
"#,
};
#[derive(Debug, PartialEq)]
pub struct Options {
op: Operation,
repo: Option<RepoId>,
}
#[derive(Debug, PartialEq)]
pub enum Operation {
List { opts: ListOptions },
Review { opts: ReviewOptions },
Unknown { args: Vec<OsString> },
Other { args: Vec<OsString> },
}
@@ -71,7 +74,7 @@ pub enum Operation {
pub enum OperationName {
List,
Review,
Other,
Unknown,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
@@ -197,7 +200,14 @@ impl Args for Options {
Value(val) if op == OperationName::List => match val.to_string_lossy().as_ref() {
"list" => op = OperationName::List,
"review" => op = OperationName::Review,
_ => op = OperationName::Other,
_ => {
op = OperationName::Unknown;
// Only enable forwarding if it was not already disabled explicitly
forward = match forward {
Some(false) => Some(false),
_ => Some(true),
};
}
},
Value(val) if patch_id.is_none() => {
let val = string(&val);
@@ -237,6 +247,7 @@ impl Args for Options {
},
},
OperationName::List if !forward => Operation::List { opts: list_opts },
OperationName::Unknown if !forward => Operation::Unknown { args },
_ => Operation::Other { args },
};
@@ -312,6 +323,9 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul
Operation::Other { args } => {
let _ = crate::terminal::run_rad(Some("patch"), &args);
}
Operation::Unknown { .. } => {
anyhow::bail!("unknown operation provided");
}
}
Ok(())
@@ -373,7 +387,6 @@ mod interface {
let patch = patch::find(&profile, &repo, &patch_id)?
.ok_or_else(|| anyhow!("Patch `{patch_id}` not found"))?;
let (_, revision) = opts.revision_or_latest(&patch, &repo)?;
let hunks = ReviewBuilder::new(&repo).hunks(revision)?;
@@ -485,175 +498,138 @@ mod interface {
#[cfg(test)]
mod cli {
use std::process::Command;
use radicle_cli::terminal::args::Error;
use radicle_cli::terminal::Args;
use assert_cmd::prelude::*;
use predicates::prelude::*;
mod assert {
use predicates::prelude::*;
use predicates::str::ContainsPredicate;
pub fn is_tui() -> ContainsPredicate {
predicate::str::contains("Inappropriate ioctl for device")
}
pub fn is_rad_manual() -> ContainsPredicate {
predicate::str::contains("RAD-PATCH(1)")
}
pub fn is_patch_help() -> ContainsPredicate {
predicate::str::contains("Terminal interfaces for patches")
}
}
use super::{ListOptions, Operation, Options};
#[test]
#[ignore = "requires binary"]
fn empty_operation() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_operation_should_default_to_list_and_not_be_forwarded(
) -> Result<(), Box<dyn std::error::Error>> {
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.arg("patch");
cmd.assert().failure().stdout(assert::is_tui());
let args = vec![];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_operation_with_help_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["--help".into()];
let expected_op = Operation::Other { args: args.clone() };
cmd.arg("patch");
cmd.assert().failure().stdout(assert::is_tui());
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_with_help_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["patch", "--help"]);
cmd.assert().success().stdout(assert::is_rad_manual());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_with_help_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["patch", "--help", "--no-forward"]);
cmd.assert().success().stdout(assert::is_patch_help());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_operation_is_not_forwarded_explicitly() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["patch", "--no-forward"]);
cmd.assert().failure().stdout(assert::is_tui());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["patch", "list"]);
cmd.assert().failure().stdout(assert::is_tui());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_is_not_forwarded_explicitly() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["patch", "list", "--no-forward"]);
cmd.assert().failure().stdout(assert::is_tui());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_with_help_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["patch", "list", "--help"]);
cmd.assert().success().stdout(assert::is_rad_manual());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_with_help_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.args(["patch", "list", "--help", "--no-forward"]);
cmd.assert().success().stdout(assert::is_patch_help());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn list_operation_with_help_is_not_forwarded_reversed() -> Result<(), Box<dyn std::error::Error>>
fn empty_operation_with_help_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>>
{
let mut cmd = Command::cargo_bin("rad-tui")?;
let args = vec!["--help".into(), "--no-forward".into()];
cmd.args(["patch", "list", "--no-forward", "--help"]);
cmd.assert().success().stdout(assert::is_patch_help());
let actual = Options::from_args(args).unwrap_err().downcast::<Error>()?;
assert!(matches!(actual, Error::Help));
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn unknown_operation_show_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_operation_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.args(["patch", "show"]);
cmd.assert().success().stdout(predicate::str::contains(
"Error: rad patch: a patch must be provided",
));
let args = vec!["--no-forward".into()];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn unknown_operation_edit_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn list_operation_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.args(["patch", "edit"]);
cmd.assert().success().stdout(predicate::str::contains(
"Error: rad patch: a patch must be provided",
));
let args = vec!["list".into()];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn unknown_operation_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn list_operation_should_not_be_forwarded_explicitly() -> Result<(), Box<dyn std::error::Error>>
{
let expected_op = Operation::List {
opts: ListOptions::default(),
};
cmd.args(["patch", "operation", "--no-forward"]);
cmd.assert().success().stdout(predicate::str::contains(
"Error: rad patch: unknown operation",
));
let args = vec!["list".into(), "--no-forward".into()];
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn list_operation_with_help_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["list".into(), "--help".into()];
let expected_op = Operation::Other { args: args.clone() };
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn list_operation_with_help_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>>
{
let args = vec!["list".into(), "--help".into(), "--no-forward".into()];
let actual = Options::from_args(args).unwrap_err().downcast::<Error>()?;
assert!(matches!(actual, Error::Help));
Ok(())
}
#[test]
fn list_operation_with_help_should_not_be_forwarded_reversed(
) -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["list".into(), "--no-forward".into(), "--help".into()];
let actual = Options::from_args(args).unwrap_err().downcast::<Error>()?;
assert!(matches!(actual, Error::Help));
Ok(())
}
#[test]
fn unknown_operation_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["operation".into()];
let expected_op = Operation::Other { args: args.clone() };
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
#[test]
fn unknown_operation_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["operation".into(), "--no-forward".into()];
let expected_op = Operation::Unknown { args: args.clone() };
let (actual, _) = Options::from_args(args)?;
assert_eq!(actual.op, expected_op);
Ok(())
}
+60 -75
View File
@@ -9,6 +9,7 @@ mod terminal;
mod test;
mod ui;
use std::env::args_os;
use std::ffi::OsString;
use std::io;
use std::{iter, process};
@@ -55,13 +56,13 @@ enum CommandName {
Version,
}
#[derive(Debug)]
#[derive(Default, Debug, PartialEq)]
struct OtherOptions {
args: Vec<OsString>,
forward: bool,
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
enum Command {
Other { opts: OtherOptions },
Help,
@@ -69,7 +70,9 @@ enum Command {
}
fn main() {
match parse_args().and_then(run) {
let args = args_os().collect::<Vec<_>>();
match parse_args(&args[1..]).and_then(run) {
Ok(_) => process::exit(0),
Err(err) => {
match err {
@@ -83,10 +86,10 @@ fn main() {
}
}
fn parse_args() -> anyhow::Result<Command, Error> {
fn parse_args(args: &[OsString]) -> anyhow::Result<Command, Error> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_env();
let mut parser = lexopt::Parser::from_args(args);
let mut command = None;
let mut forward = true;
let mut json = false;
@@ -227,115 +230,97 @@ fn run_other(command: Option<&str>, args: &[OsString]) -> Result<(), Error> {
#[cfg(test)]
mod cli {
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::process::Command;
mod assert {
use predicates::prelude::*;
use predicates::str::ContainsPredicate;
pub fn is_rad_manual() -> ContainsPredicate {
predicate::str::contains("Radicle CLI Manual")
}
pub fn is_rad_help() -> ContainsPredicate {
predicate::str::contains("Radicle command line interface")
}
pub fn is_help() -> ContainsPredicate {
predicate::str::contains("Radicle terminal interfaces")
}
}
use crate::{parse_args, OtherOptions};
#[test]
#[ignore = "requires binary"]
fn can_be_executed() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_command_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec![];
let expected = super::Command::Other {
opts: OtherOptions {
args: args.clone(),
forward: true,
},
};
cmd.assert().success();
let actual = parse_args(&args)?;
assert_eq!(actual, expected);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_command_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn empty_command_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["--no-forward".into()];
let expected = super::Command::Other {
opts: OtherOptions::default(),
};
cmd.assert().success().stdout(assert::is_rad_help());
let actual = parse_args(&args)?;
assert_eq!(actual, expected);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn empty_command_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn version_command_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["version".into()];
let expected = super::Command::Other {
opts: OtherOptions {
args: args.clone(),
forward: true,
},
};
cmd.arg("--no-forward");
cmd.assert().success().stdout(assert::is_help());
let actual = parse_args(&args)?;
assert_eq!(actual, expected);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn version_command_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn version_command_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["version".into(), "--no-forward".into()];
let expected = super::Command::Version { json: false };
cmd.arg("version");
cmd.assert()
.success()
.stdout(predicate::str::starts_with("rad "));
let actual = parse_args(&args)?;
assert_eq!(actual, expected);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn version_command_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn version_command_should_print_json() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["version".into(), "--no-forward".into(), "--json".into()];
let expected = super::Command::Version { json: true };
cmd.arg("version").arg("--no-forward");
cmd.assert()
.success()
.stdout(predicate::str::starts_with("rad-tui "));
let actual = parse_args(&args)?;
assert_eq!(actual, expected);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn version_command_prints_json() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn help_command_should_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["help".into()];
let expected = super::Command::Other {
opts: OtherOptions {
args: args.clone(),
forward: true,
},
};
cmd.arg("version").arg("--no-forward").arg("--json");
cmd.assert()
.success()
.stdout(predicate::str::contains("\"name\":\"rad-tui\""));
let actual = parse_args(&args)?;
assert_eq!(actual, expected);
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn help_command_is_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
fn help_command_should_not_be_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let args = vec!["help".into(), "--no-forward".into()];
cmd.arg("help");
cmd.assert().success().stdout(assert::is_rad_manual());
Ok(())
}
#[test]
#[ignore = "requires binary"]
fn help_command_is_not_forwarded() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("rad-tui")?;
cmd.arg("help").arg("--no-forward");
cmd.assert().success().stdout(assert::is_help());
let actual = parse_args(&args)?;
assert!(matches!(actual, super::Command::Help));
Ok(())
}