expect-exit 0.5.2

Result.expected(): display an error message and exit without a panic.
Documentation
#![warn(missing_docs)]
//! Run some tests for the functions implemented by the `expect_exit` crate.
//! A separate program is needed because the tests involve the process
//! exiting, not merely panicking, which cannot be tested by
//! the standard toolset.

/*
 * Copyright (c) 2020 - 2022  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
// Turn most of clippy::restriction's lints on...
#![warn(clippy::if_then_some_else_none)]
#![warn(clippy::indexing_slicing)]
#![warn(clippy::missing_const_for_fn)]
#![warn(clippy::missing_docs_in_private_items)]
#![warn(clippy::missing_inline_in_public_items)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::panic)]
#![warn(clippy::unnecessary_wraps)]
#![warn(clippy::single_char_lifetime_names)]
// ...except for these ones.
#![allow(clippy::panic_in_result_fn)]
#![allow(clippy::print_stdout)]
#![allow(clippy::print_stderr)]
#![allow(clippy::implicit_return)]
#![allow(clippy::use_debug)]

use std::env;
use std::error::Error;

use expect_exit::{ExpectationFailed, Expected, ExpectedResult, ExpectedWithError};

/// Something to enqueue a [`Drop::drop()`] action on and check whether it occurred.
struct UnwindTest {
    /// The value to look for in the test output.
    value: u32,
}

impl Drop for UnwindTest {
    fn drop(&mut self) {
        println!("UnwindTest.drop() invoked for {}", self.value);
    }
}

/// The action that the test program was invoked to perform.
#[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,
}

/// Do nothing, successfully.
#[allow(clippy::unnecessary_wraps)]
const fn return_ok() -> Result<(), String> {
    Ok(())
}

/// Do nothing and fail.
fn return_err() -> Result<(), String> {
    Err(String::from("this error message should be displayed"))
}

/// Return something.
#[allow(clippy::unnecessary_wraps)]
const fn o_return_some() -> Option<()> {
    Some(())
}

/// Return nothing.
const fn o_return_none() -> Option<()> {
    None
}

/// Return a true value.
const fn return_true() -> bool {
    true
}

/// Return a false value.
const fn return_false() -> bool {
    false
}

/// Return a [`Result`] as specified by the `success` parameter.
fn e_res_handle(success: bool) -> Result<(), String> {
    #[allow(clippy::unnecessary_lazy_evaluations)]
    success.then(|| ()).ok_or_else(|| "oof".to_owned())
}

/// Return an [`Option`] as specified by the `success` parameter.
fn e_opt_handle(success: bool) -> Option<u32> {
    #[allow(clippy::unnecessary_lazy_evaluations)]
    success.then(|| 0)
}

/// Test a result, return a result with an unboxed error.
fn e_res_nb(success: bool) -> Result<(), ExpectationFailed> {
    e_res_handle(success).expect_result_nb_("this error message should be displayed")
}

/// Test a result, return a result with a boxed error.
fn e_res(success: bool) -> Result<(), Box<dyn Error>> {
    e_res_handle(success).expect_result_("this error message should be displayed")
}

/// Test an option, return a result with an unboxed error.
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(())
}

/// Test an option, return a result with a boxed error.
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(())
}

/// Test a boolean value, return a result with an unboxed error.
fn e_bool_nb(success: bool) -> Result<(), ExpectationFailed> {
    success.expect_result_nb_("this error message should be displayed")
}

/// Test a boolean value, return a result with a boxed error.
fn e_bool(success: bool) -> Result<(), Box<dyn Error>> {
    success.expect_result_("this error message should be displayed")
}

/// Output a usage message and exit the program.
fn usage() -> ! {
    expect_exit::die("Usage: expect-exit <mode>");
}

/// Parse the command-line arguments, figure out what we are expected to check.
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(),
    }
}

/// Perform a single test action: check something, succeed or fail, unwind the stack or not.
fn handle(mode: OpMode) {
    let val = UnwindTest { value: 616 };
    println!("Created a test variable with the value {}", val.value);

    let cb = || {
        println!("The format callback was invoked.");
        format!("something about {}", 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)]
mod tests {
    #![warn(clippy::panic_in_result_fn)]
    #![allow(clippy::panic)]

    use std::env;
    use std::error::Error;
    use std::io::Read;
    use std::path::{Path, PathBuf};
    use std::process::{Command, Stdio};

    use once_cell::sync::Lazy;

    /// A single expect-exit test case.
    struct TestCase<'data> {
        /// The command-line argument to pass to the test program.
        arg: &'data str,
        /// The lines to look for in the test program's standard output stream.
        stdout: Vec<&'data str>,
        /// The lines to look for in the test program's standard error stream.
        stderr: Vec<&'data str>,
        /// The expected exit code for the test program.
        result: i32,
    }

    /// The first line expected to be output.
    const EXP_FIRST: &str = "with the value 616";
    /// The last line (apart from the drop markers) expected to be output.
    const EXP_LAST: &str = "the end";
    /// The drop marker for the tests that unwind the stack.
    const EXP_DROP: &str = "UnwindTest.drop";
    /// The error message marker.
    const EXP_ERROR: &str = "should be displayed";
    /// Another error message marker.
    const EXP_TRIG: &str = "should be triggered";
    /// The marker for the format function being invoked.
    const EXP_FMT_CB: &str = "format callback";
    /// The marker for the format function being invoked with an argument.
    const EXP_FMT_RES: &str = "something about 616";

    fn get_exe_path(name: &str) -> Result<PathBuf, Box<dyn Error>> {
        let current = env::current_exe()?;
        let exe_dir = {
            let basedir = Path::new(&current).parent().ok_or_else(|| {
                format!(
                    "Could not get the parent directory of {}",
                    current.display()
                )
            })?;
            if basedir
                .file_name()
                .ok_or_else(|| format!("Could not get the base name of {}", basedir.display()))?
                == "deps"
            {
                basedir.parent().ok_or_else(|| {
                    format!(
                        "Could not get the parent directory of {}",
                        basedir.display()
                    )
                })?
            } else {
                basedir
            }
        };
        Ok(exe_dir.join(name))
    }

    /// Build up the array of test cases.
    #[allow(clippy::too_many_lines)]
    fn get_test_cases() -> &'static [TestCase<'static>] {
        static TEST_CASES: Lazy<Vec<TestCase<'static>>> = Lazy::new(|| {
            vec![
                TestCase {
                    arg: "oof",
                    stdout: vec![],
                    stderr: vec!["Usage: "],
                    result: 1,
                },
                TestCase {
                    arg: "nu_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "nu_fail",
                    stdout: vec![EXP_FIRST],
                    stderr: vec![EXP_ERROR],
                    result: 1,
                },
                TestCase {
                    arg: "f_nu_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "f_nu_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB],
                    stderr: vec![EXP_ERROR],
                    result: 1,
                },
                TestCase {
                    arg: "ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "fail",
                    stdout: vec![EXP_FIRST, EXP_DROP],
                    stderr: vec![EXP_ERROR],
                    result: 1,
                },
                TestCase {
                    arg: "f_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "f_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB, EXP_DROP],
                    stderr: vec![EXP_ERROR],
                    result: 1,
                },
                TestCase {
                    arg: "o_nu_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "o_nu_fail",
                    stdout: vec![EXP_FIRST],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "o_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "o_fail",
                    stdout: vec![EXP_FIRST, EXP_DROP],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "o_f_nu_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "o_f_nu_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
                TestCase {
                    arg: "o_f_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "o_f_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB, EXP_DROP],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
                TestCase {
                    arg: "b_n_u_true",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "b_n_u_false",
                    stdout: vec![EXP_FIRST],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "b_u_true",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "b_u_false",
                    stdout: vec![EXP_FIRST, EXP_DROP],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "b_f_n_u_true",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "b_f_n_u_false",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
                TestCase {
                    arg: "b_f_u_true",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "b_f_u_false",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB, EXP_DROP],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
                TestCase {
                    arg: "e_nb_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "e_nb_fail",
                    stdout: vec![EXP_FIRST],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "e_b_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "e_b_fail",
                    stdout: vec![EXP_FIRST],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "e_f_nb_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "e_f_nb_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
                TestCase {
                    arg: "e_f_b_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "e_f_b_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
                TestCase {
                    arg: "o_e_nb_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "o_e_nb_fail",
                    stdout: vec![EXP_FIRST],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "o_e_b_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "o_e_b_fail",
                    stdout: vec![EXP_FIRST],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "o_e_f_nb_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "o_e_f_nb_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
                TestCase {
                    arg: "o_e_f_b_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "o_e_f_b_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
                TestCase {
                    arg: "b_e_nb_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "b_e_nb_fail",
                    stdout: vec![EXP_FIRST],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "b_e_b_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "b_e_b_fail",
                    stdout: vec![EXP_FIRST],
                    stderr: vec![EXP_TRIG],
                    result: 1,
                },
                TestCase {
                    arg: "b_e_f_nb_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "b_e_f_nb_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
                TestCase {
                    arg: "b_e_f_b_ok",
                    stdout: vec![EXP_FIRST, EXP_LAST, EXP_DROP],
                    stderr: vec![],
                    result: 0,
                },
                TestCase {
                    arg: "b_e_f_b_fail",
                    stdout: vec![EXP_FIRST, EXP_FMT_CB],
                    stderr: vec![EXP_FMT_RES],
                    result: 1,
                },
            ]
        });
        &TEST_CASES
    }

    /// Check whether a program's output stream contains the expected lines.
    fn check_output_contains(expected: &[&str], actual: &[&str]) -> bool {
        expected.len() == actual.len()
            && expected
                .iter()
                .zip(actual.iter())
                .all(|(&exp, &act)| act.contains(exp))
    }

    /// Check whether the program output the expected lines to its output and error streams.
    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)
    }

    /// Run all the tests: invoke ourselves with arguments, check the result code and output.
    fn run_test(program: &str) {
        println!("Running a test on {}", program);

        for case in get_test_cases() {
            let desc = &format!("'{} {}'", program, case.arg);
            println!(
                "\n=====\nrun {} stdout {:?} stderr {:?}",
                desc, case.stdout, case.stderr
            );

            let mut proc = Command::new(program)
                .arg(case.arg)
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()
                .unwrap_or_else(|err| panic!("Could not run {}: {}", desc, err));

            let mut stdout_text = String::new();
            proc.stdout
                .take()
                .unwrap_or_else(|| panic!("Could not open the standard output stream of {}", desc))
                .read_to_string(&mut stdout_text)
                .unwrap_or_else(|err| panic!("Could not read the output of {}: {}", desc, err));
            let stdout_lines: Vec<&str> = stdout_text.split_terminator('\n').collect();

            let mut stderr_text = String::new();
            proc.stderr
                .take()
                .unwrap_or_else(|| panic!("Could not open the standard error stream of {}", desc))
                .read_to_string(&mut stderr_text)
                .unwrap_or_else(|err| {
                    panic!("Could not read the error output of {}: {}", desc, err)
                });
            let stderr_lines: Vec<&str> = stderr_text.split_terminator('\n').collect();

            println!(
                "Got {} line(s) of output, {} line(s) of error output.",
                stdout_lines.len(),
                stderr_lines.len()
            );
            assert!(
                check_test_output(case, &stdout_lines, &stderr_lines),
                "Output mismatch for {}: ({:?}, {:?}) vs ({:?}, {:?})",
                desc,
                case.stdout,
                case.stderr,
                stdout_lines,
                stderr_lines
            );

            let res = proc
                .wait()
                .unwrap_or_else(|err| panic!("Could not wait for {}: {}", desc, err))
                .code()
                .unwrap_or_else(|| panic!("Could not fetch the exit code for {}", desc));
            println!("\n{} exited with code {}", desc, res);
            assert!(
                res == case.result,
                "{} exited with code {}, expected {}",
                desc,
                res,
                case.result
            );
        }

        println!("\n=====\nThe self-test seems to have run successfully.");
    }

    #[test]
    fn test_expect_exit() -> Result<(), Box<dyn Error>> {
        let exe_path = get_exe_path("test_expect_exit")?;
        run_test(
            exe_path
                .to_str()
                .ok_or_else(|| format!("Could not convert {:?} to a string", exe_path))?,
        );
        Ok(())
    }
}