#![deny(missing_docs)]
#![deny(clippy::missing_docs_in_private_items)]
#![allow(deprecated)]
use std::env;
use std::error::Error;
use expect_exit::{ExpectationFailed, Expected, ExpectedResult, ExpectedWithError};
struct UnwindTest {
value: u32,
}
impl Drop for UnwindTest {
#[allow(clippy::print_stdout)]
fn drop(&mut self) {
println!("UnwindTest.drop() invoked for {value}", value = self.value);
}
}
#[derive(Debug, Clone, Copy)]
#[allow(clippy::missing_docs_in_private_items)]
enum OpMode {
RNUOk,
RNUFail,
RUOk,
RUFail,
RFNUOk,
RFNUFail,
RFUOk,
RFUFail,
ONUOk,
ONUFail,
OUOk,
OUFail,
OFNUOk,
OFNUFail,
OFUOk,
OFUFail,
BNUTrue,
BNUFalse,
BUTrue,
BUFalse,
BFNUTrue,
BFNUFalse,
BFUTrue,
BFUFalse,
RENBOk,
RENBFail,
REBOk,
REBFail,
REFNBOk,
REFNBFail,
REFBOk,
REFBFail,
OENBOk,
OENBFail,
OEBOk,
OEBFail,
OEFNBOk,
OEFNBFail,
OEFBOk,
OEFBFail,
BENBOk,
BENBFail,
BEBOk,
BEBFail,
BEFNBOk,
BEFNBFail,
BEFBOk,
BEFBFail,
}
#[allow(clippy::unnecessary_wraps)]
const fn return_ok() -> Result<(), String> {
Ok(())
}
fn return_err() -> Result<(), String> {
Err(String::from("this error message should be displayed"))
}
#[allow(clippy::unnecessary_wraps)]
const fn o_return_some() -> Option<()> {
Some(())
}
const fn o_return_none() -> Option<()> {
None
}
const fn return_true() -> bool {
true
}
const fn return_false() -> bool {
false
}
fn e_res_handle(success: bool) -> Result<(), String> {
#[allow(clippy::unnecessary_lazy_evaluations)]
success.then(|| ()).ok_or_else(|| "oof".to_owned())
}
fn e_opt_handle(success: bool) -> Option<u32> {
#[allow(clippy::unnecessary_lazy_evaluations)]
success.then(|| 0)
}
fn e_res_nb(success: bool) -> Result<(), ExpectationFailed> {
e_res_handle(success).expect_result_nb_("this error message should be displayed")
}
fn e_res(success: bool) -> Result<(), Box<dyn Error>> {
e_res_handle(success).expect_result_("this error message should be displayed")
}
#[allow(clippy::panic_in_result_fn)]
fn e_opt_nb(success: bool) -> Result<(), ExpectationFailed> {
let res = e_opt_handle(success).expect_result_nb_("this error message should be displayed")?;
assert!(res == 0, "e_opt_handle() did not return 0");
Ok(())
}
#[allow(clippy::panic_in_result_fn)]
fn e_opt(success: bool) -> Result<(), Box<dyn Error>> {
let res = e_opt_handle(success).expect_result_("this error message should be displayed")?;
assert!(res == 0, "e_opt_handle() did not return 0");
Ok(())
}
fn e_bool_nb(success: bool) -> Result<(), ExpectationFailed> {
success.expect_result_nb_("this error message should be displayed")
}
fn e_bool(success: bool) -> Result<(), Box<dyn Error>> {
success.expect_result_("this error message should be displayed")
}
fn usage() -> ! {
expect_exit::die("Usage: expect-exit <mode>");
}
fn parse_args() -> OpMode {
let args: Vec<String> = env::args().collect();
match *args {
[_, ref req] => {
let action = match req.as_str() {
"nu_ok" => OpMode::RNUOk,
"nu_fail" => OpMode::RNUFail,
"ok" => OpMode::RUOk,
"fail" => OpMode::RUFail,
"f_nu_ok" => OpMode::RFNUOk,
"f_nu_fail" => OpMode::RFNUFail,
"f_ok" => OpMode::RFUOk,
"f_fail" => OpMode::RFUFail,
"o_nu_ok" => OpMode::ONUOk,
"o_nu_fail" => OpMode::ONUFail,
"o_ok" => OpMode::OUOk,
"o_fail" => OpMode::OUFail,
"o_f_nu_ok" => OpMode::OFNUOk,
"o_f_nu_fail" => OpMode::OFNUFail,
"o_f_ok" => OpMode::OFUOk,
"o_f_fail" => OpMode::OFUFail,
"b_n_u_true" => OpMode::BNUTrue,
"b_n_u_false" => OpMode::BNUFalse,
"b_u_true" => OpMode::BUTrue,
"b_u_false" => OpMode::BUFalse,
"b_f_n_u_true" => OpMode::BFNUTrue,
"b_f_n_u_false" => OpMode::BFNUFalse,
"b_f_u_true" => OpMode::BFUTrue,
"b_f_u_false" => OpMode::BFUFalse,
"e_nb_ok" => OpMode::RENBOk,
"e_nb_fail" => OpMode::RENBFail,
"e_b_ok" => OpMode::REBOk,
"e_b_fail" => OpMode::REBFail,
"e_f_nb_ok" => OpMode::REFNBOk,
"e_f_nb_fail" => OpMode::REFNBFail,
"e_f_b_ok" => OpMode::REFBOk,
"e_f_b_fail" => OpMode::REFBFail,
"o_e_nb_ok" => OpMode::OENBOk,
"o_e_nb_fail" => OpMode::OENBFail,
"o_e_b_ok" => OpMode::OEBOk,
"o_e_b_fail" => OpMode::OEBFail,
"o_e_f_nb_ok" => OpMode::OEFNBOk,
"o_e_f_nb_fail" => OpMode::OEFNBFail,
"o_e_f_b_ok" => OpMode::OEFBOk,
"o_e_f_b_fail" => OpMode::OEFBFail,
"b_e_nb_ok" => OpMode::BENBOk,
"b_e_nb_fail" => OpMode::BENBFail,
"b_e_b_ok" => OpMode::BEBOk,
"b_e_b_fail" => OpMode::BEBFail,
"b_e_f_nb_ok" => OpMode::BEFNBOk,
"b_e_f_nb_fail" => OpMode::BEFNBFail,
"b_e_f_b_ok" => OpMode::BEFBOk,
"b_e_f_b_fail" => OpMode::BEFBFail,
_ => usage(),
};
action
}
_ => usage(),
}
}
#[allow(clippy::print_stdout)]
fn handle(mode: OpMode) {
let val = UnwindTest { value: 616 };
println!(
"Created a test variable with the value {value}",
value = val.value
);
let cb = || {
println!("The format callback was invoked.");
format!("something about {value}", value = val.value)
};
match mode {
OpMode::RNUOk => return_ok().or_die_e_("This should not be triggered"),
OpMode::RNUFail => return_err().or_die_e_("This should be triggered"),
OpMode::RUOk => return_ok().or_exit_e_("This should not be triggered"),
OpMode::RUFail => return_err().or_exit_e_("This should be triggered"),
OpMode::RFNUOk => return_ok().or_die_e(cb),
OpMode::RFNUFail => return_err().or_die_e(cb),
OpMode::RFUOk => return_ok().or_exit_e(cb),
OpMode::RFUFail => return_err().or_exit_e(cb),
OpMode::ONUOk => o_return_some().or_die_("This should not be triggered"),
OpMode::ONUFail => o_return_none().or_die_("This should be triggered"),
OpMode::OUOk => o_return_some().or_exit_("This should not be triggered"),
OpMode::OUFail => o_return_none().or_exit_("This should be triggered"),
OpMode::OFNUOk => o_return_some().or_die(cb),
OpMode::OFNUFail => o_return_none().or_die(cb),
OpMode::OFUOk => o_return_some().or_exit(cb),
OpMode::OFUFail => o_return_none().or_exit(cb),
OpMode::BNUTrue => {
return_true().or_die_("This should not be triggered");
}
OpMode::BNUFalse => {
return_false().or_die_("This should be triggered");
}
OpMode::BUTrue => {
return_true().or_exit_("This should not be triggered");
}
OpMode::BUFalse => {
return_false().or_exit_("This should be triggered");
}
OpMode::BFNUTrue => {
return_true().or_die(cb);
}
OpMode::BFNUFalse => {
return_false().or_die(cb);
}
OpMode::BFUTrue => {
return_true().or_exit(cb);
}
OpMode::BFUFalse => {
return_false().or_exit(cb);
}
OpMode::RENBOk => e_res_nb(true).or_die_e_("This should not be triggered"),
OpMode::RENBFail => e_res_nb(false).or_die_e_("This should be triggered"),
OpMode::REBOk => e_res(true).or_die_e_("This should not be triggered"),
OpMode::REBFail => e_res(false).or_die_e_("This should be triggered"),
OpMode::REFNBOk => e_res_nb(true).or_die_e(cb),
OpMode::REFNBFail => e_res_nb(false).or_die_e(cb),
OpMode::REFBOk => e_res(true).or_die_e(cb),
OpMode::REFBFail => e_res(false).or_die_e(cb),
OpMode::OENBOk => e_opt_nb(true).or_die_e_("This should not be triggered"),
OpMode::OENBFail => e_opt_nb(false).or_die_e_("This should be triggered"),
OpMode::OEBOk => e_opt(true).or_die_e_("This should not be triggered"),
OpMode::OEBFail => e_opt(false).or_die_e_("This should be triggered"),
OpMode::OEFNBOk => e_opt_nb(true).or_die_e(cb),
OpMode::OEFNBFail => e_opt_nb(false).or_die_e(cb),
OpMode::OEFBOk => e_opt(true).or_die_e(cb),
OpMode::OEFBFail => e_opt(false).or_die_e(cb),
OpMode::BENBOk => e_bool_nb(true).or_die_e_("This should not be triggered"),
OpMode::BENBFail => e_bool_nb(false).or_die_e_("This should be triggered"),
OpMode::BEBOk => e_bool(true).or_die_e_("This should not be triggered"),
OpMode::BEBFail => e_bool(false).or_die_e_("This should be triggered"),
OpMode::BEFNBOk => e_bool_nb(true).or_die_e(cb),
OpMode::BEFNBFail => e_bool_nb(false).or_die_e(cb),
OpMode::BEFBOk => e_bool(true).or_die_e(cb),
OpMode::BEFBFail => e_bool(false).or_die_e(cb),
};
println!("This is the end.");
}
fn main() {
let mode = parse_args();
handle(mode);
}
#[cfg(test)]
#[allow(clippy::panic_in_result_fn)]
#[allow(clippy::print_stdout)]
#[allow(clippy::use_debug)]
mod tests {
use std::env;
use std::process::{Command, Stdio};
use std::str;
use anyhow::{anyhow, bail, Context, Result};
use camino::Utf8PathBuf;
use once_cell::sync::Lazy;
use rstest::rstest;
struct TestCase<'data> {
arg: &'data str,
stdout: Vec<&'data str>,
stderr: Vec<&'data str>,
result: i32,
}
const EXP_FIRST: &str = "with the value 616";
const EXP_LAST: &str = "the end";
const EXP_DROP: &str = "UnwindTest.drop";
const EXP_ERROR: &str = "should be displayed";
const EXP_TRIG: &str = "should be triggered";
const EXP_FMT_CB: &str = "format callback";
const EXP_FMT_RES: &str = "something about 616";
fn get_exe_path() -> Result<Utf8PathBuf> {
static PATH: Lazy<Result<Utf8PathBuf>> = Lazy::new(|| {
let current = Utf8PathBuf::from_path_buf(
env::current_exe().context("Could not get the current executable file's path")?,
)
.map_err(|path| {
anyhow!(
"Could not represent the current executable file's path {path} as UTF-8",
path = path.display()
)
})?;
let exe_dir = {
let basedir = current
.parent()
.with_context(|| format!("Could not get the parent directory of {current}"))?;
if basedir
.file_name()
.with_context(|| format!("Could not get the base name of {basedir}"))?
== "deps"
{
basedir.parent().with_context(|| {
format!("Could not get the parent directory of {basedir}")
})?
} else {
basedir
}
};
Ok(exe_dir.join("test_expect_exit"))
});
match *PATH {
Ok(ref res) => Ok(res.clone()),
Err(ref err) => bail!("Could not determine the path to the test program: {err}"),
}
}
fn check_output_contains(expected: &[&str], actual: &[&str]) -> bool {
expected.len() == actual.len()
&& expected
.iter()
.zip(actual.iter())
.all(|(&exp, &act)| act.contains(exp))
}
fn check_test_output(
case: &TestCase<'_>,
stdout_lines: &[&str],
stderr_lines: &[&str],
) -> bool {
check_output_contains(&case.stdout, stdout_lines)
&& check_output_contains(&case.stderr, stderr_lines)
}
#[rstest]
#[case(TestCase {
arg: "oof",
stdout: vec![],
stderr: vec!["Usage: "],
result: 1,
})]
#[case(TestCase {
arg: "nu_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "nu_fail",
stdout: vec![EXP_FIRST],
stderr: vec![EXP_ERROR],
result: 1,
})]
#[case(TestCase {
arg: "f_nu_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "f_nu_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB],
stderr: vec![EXP_ERROR],
result: 1,
})]
#[case(TestCase {
arg: "ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "fail",
stdout: vec![EXP_FIRST, EXP_DROP],
stderr: vec![EXP_ERROR],
result: 1,
})]
#[case(TestCase {
arg: "f_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "f_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB, EXP_DROP],
stderr: vec![EXP_ERROR],
result: 1,
})]
#[case(TestCase {
arg: "o_nu_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "o_nu_fail",
stdout: vec![EXP_FIRST],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "o_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "o_fail",
stdout: vec![EXP_FIRST, EXP_DROP],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "o_f_nu_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "o_f_nu_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
#[case(TestCase {
arg: "o_f_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "o_f_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB, EXP_DROP],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
#[case(TestCase {
arg: "b_n_u_true",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "b_n_u_false",
stdout: vec![EXP_FIRST],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "b_u_true",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "b_u_false",
stdout: vec![EXP_FIRST, EXP_DROP],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "b_f_n_u_true",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "b_f_n_u_false",
stdout: vec![EXP_FIRST, EXP_FMT_CB],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
#[case(TestCase {
arg: "b_f_u_true",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "b_f_u_false",
stdout: vec![EXP_FIRST, EXP_FMT_CB, EXP_DROP],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
#[case(TestCase {
arg: "e_nb_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "e_nb_fail",
stdout: vec![EXP_FIRST],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "e_b_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "e_b_fail",
stdout: vec![EXP_FIRST],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "e_f_nb_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "e_f_nb_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
#[case(TestCase {
arg: "e_f_b_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "e_f_b_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
#[case(TestCase {
arg: "o_e_nb_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "o_e_nb_fail",
stdout: vec![EXP_FIRST],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "o_e_b_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "o_e_b_fail",
stdout: vec![EXP_FIRST],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "o_e_f_nb_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "o_e_f_nb_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
#[case(TestCase {
arg: "o_e_f_b_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "o_e_f_b_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
#[case(TestCase {
arg: "b_e_nb_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "b_e_nb_fail",
stdout: vec![EXP_FIRST],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "b_e_b_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "b_e_b_fail",
stdout: vec![EXP_FIRST],
stderr: vec![EXP_TRIG],
result: 1,
})]
#[case(TestCase {
arg: "b_e_f_nb_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "b_e_f_nb_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
#[case(TestCase {
arg: "b_e_f_b_ok",
stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
stderr: vec![],
result: 0,
})]
#[case(TestCase {
arg: "b_e_f_b_fail",
stdout: vec![EXP_FIRST, EXP_FMT_CB],
stderr: vec![EXP_FMT_RES],
result: 1,
})]
fn run_test(#[case] case: TestCase<'_>) -> Result<()> {
let program = get_exe_path()?;
let desc = &format!("'{program} {arg}'", arg = case.arg);
println!(
"\n=====\nrun {desc} stdout {stdout:?} stderr {stderr:?}",
stdout = case.stdout,
stderr = case.stderr
);
let output = Command::new(&program)
.arg(case.arg)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.with_context(|| format!("Could not run {desc}"))?;
let stdout_lines = str::from_utf8(&output.stdout)
.with_context(|| format!("Could not parse the output of {desc} as a UTF-8 string"))?
.lines()
.collect::<Vec<_>>();
let stderr_lines = str::from_utf8(&output.stderr)
.with_context(|| {
format!("Could not parse the error output of {desc} as a UTF-8 string")
})?
.lines()
.collect::<Vec<_>>();
println!(
"Got {stdout_count} line(s) of output, {stderr_count} line(s) of error output.",
stdout_count = stdout_lines.len(),
stderr_count = stderr_lines.len()
);
assert!(
check_test_output(&case, &stdout_lines, &stderr_lines),
"Output mismatch for {desc}: ({stdout:?}, {stderr:?}) vs ({expout:?}, {experr:?})",
stdout = case.stdout,
stderr = case.stderr,
expout = stdout_lines,
experr = stderr_lines
);
let res = output
.status
.code()
.with_context(|| format!("Could not fetch the exit code for {desc}"))?;
println!("\n{desc} exited with code {res}");
assert!(
res == case.result,
"{desc} exited with code {res}, expected {expected}",
expected = case.result
);
Ok(())
}
}