s3rvac
s3rvac

Reputation: 10289

How to check the exit code from std::process::exit() in tests?

In Rust tests, is there a way of checking that a function that calls std::process::exit() really terminated the process with a specific exit code?

Contrived example:

fn foo(n: i32) {
    std::process::exit(n);
}

#[test]
fn exits_with_correct_exit_code() {
    // How do I assert that the following call terminates
    // the process with exit code 1?
    foo(1);
}

Upvotes: 1

Views: 2720

Answers (1)

Shepmaster
Shepmaster

Reputation: 430673

You cannot. std::process:exit is well-named, but just in case an aspect of what it does is unclear:

Terminates the current process with the specified exit code.

This function will never return and will immediately terminate the current process. The exit code is passed through to the underlying OS and will be available for consumption by another process.

Note that because this function never returns, and that it terminates the process, no destructors on the current stack or any other thread's stack will be run. If a clean shutdown is needed it is recommended to only call this function at a known point where there are no more destructors left to run.

When running tests, each test is run in a separate thread, but all tests are run within a single process. When that process is terminated, the tests go with it.

You could try to do some complicated recursive testing:

#[test]
#[ignore]
fn real() {
    std::process::exit(42)
}

#[test]
fn shim() {
    let status = std::process::Command::new("/proc/self/exe")
        .args(&["--ignored", "real"])
        .status()
        .expect("Unable to run program");

    assert_eq!(Some(42), status.code());
}

This has platform-specific code to find the current process, but it "works".


Honestly though, I'd say that the code is over-reaching its testing bounds. You should not be testing that std::process::exit does what it says it will. If you really need to assert that a function is called with an argument, that's what a mock object is for.

Use dependency injection to provide a closure, capture the value in the closure, and write a thin shim:

fn foo_logic<F>(n: i32, f: F)
    where F: FnOnce(i32)
{
    f(n);
}

fn foo(n: i32) {
    foo_logic(n, |n| std::process::exit(n));
}

#[test]
fn exits_with_correct_exit_code() {
    let mut value = None;
    foo_logic(1, |v| value = Some(v));
    assert_eq!(Some(1), value);
}

You may also consider extracting the logic that calculates the potential error code and testing that directly.

Upvotes: 4

Related Questions