Testing with tempfiles and environment variables

testing tempfile threads RUST_TEST_THREADS

There are many applications that need access to files on the filesystem or to a database. In order to have clear separation between the test runs I create a temporary folder (or a temporary database) and then I need to pass this to the test somehow. While programming in Perl or Python I used to do this by setting an environment variable in the test and then reading it in the application itself.

Sometimes this environment variable would contain a path to a configuration file that is then used by the application.

This patters is somewhat broken when trying use it in Rust. The reason is that Rust by default runs the tests in parallel in threads and environment variables are per process. Thus if I set two different temporary paths in two test-files one will override the other. Then depending on the timing one test might look at the temporary file of the other test.

Here is an example. I tried to simplify it, but it still feels a bit convoluted.

The dependencies

examples/test-tempfile/Cargo.toml

[package]
name = "test-tempfile"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.5"
tempfile = "3.9.0"


The code with the tests

examples/test-tempfile/src/main.rs


use std::fs::File;
use std::io::Write;

fn main() {
    let args = std::env::args().collect::<Vec<String>>();
    let name = &args[1];
    println!("Hello {name}");
}

fn add(x: i32, y: i32) -> i32 {
    let time: u64 = rand::random();
    let time = time % 3;
    
    std::thread::sleep(std::time::Duration::from_secs(time));

    match std::env::var("RESULT_PATH") {
        Ok(file_path) => {
            let mut file = File::create(&file_path).unwrap();
            println!("add({x}, {y}) file {file_path}");
            writeln!(&mut file, "{}", x + y).unwrap();        
        },
        Err(_) => {},
    };
    x + y
}


#[test]
fn test_add_2_3_5() {
    let tmp_dir = tempfile::tempdir().unwrap();
    println!("tmp_dir: {:?}", tmp_dir);
    let file_path = tmp_dir.path().join("result.txt");
    println!("2+3 - file_path {:?}", file_path);
    std::env::set_var("RESULT_PATH", &file_path);

    let result= add(2, 3);
    assert_eq!(result, 5);


    let result = std::fs::read_to_string(file_path).unwrap();
    assert_eq!(result, "5\n");
}

#[test]
fn test_add_4_4_8() {
    let tmp_dir = tempfile::tempdir().unwrap();
    println!("tmp_dir: {:?}", tmp_dir);
    let file_path = tmp_dir.path().join("result.txt");
    println!("4+4 - file_path {:?}", file_path);
    std::env::set_var("RESULT_PATH", &file_path);

    let result= add(4, 4);
    assert_eq!(result, 8);

    let result = std::fs::read_to_string(file_path).unwrap();
    assert_eq!(result, "8\n");
}

Running the tests

Running the tests in the usual way will randomly break as the environment variable get mixed between the two test-cases.

cargo test

The solution, for now, is to run the tests in a single thread:

cargo test -- --test-threads=1

Unfortunately people new to the project will likely try to run the default command and the test will fail for them.

We could mention this in the README file, but can we really rely on people reading the documentation?

My question on the users forum got a suggestion to include the setting in the cargo/config.toml file:

examples/test-tempfile/.cargo/config.toml


[alias]
t = "test -- --test-threads=1"

[env]
RUST_TEST_THREADS = "1"



Actually I include two suggestions.

Alias

The first one was to create an alias for the command. This way I can run

cargo t

and it will run the tests with the --test-threads=1. This is convenient, but people will still run cargo test and complain about the failures.

Set the RUST_TEST_THREADS env variable

The second advice was to set the RUST_TEST_THREADS environment variable in the .cargo/config.toml file.

That worked. Now if I run

cargo test

It will run in a single thread.

Future

I am not entirely happy with this whole solution as it means I needed some special code in my application and I can't run the tests in a thread. I asked about this and I'll update this post when I have a better solution.

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo