I use a CLI wrapper that simply reads args into a vec and lets a macro extract it into vars.
To be able to test this, I have a static global vec (in Mutex, of course, with all necessary ceremonies), and write into it if I want to simulate a call. Turns out, when tests are run in parallel, it experiences race conditions, and tests may randomly fail or not.
I checked if clap has testing capabilities, and found trycmd, but it seems far from what I'm trying to do.
Is there a relatively simple fix, like thread-local context object, or something like that?
Here's a very simplified model of what my code does:
Trying to describe it as simple as possible, I try to imitate a CLI call. main calls a macro that checks PARAMS, or if empty, reads from std::env::args_os(). In tests (and only in tests), I fill PARAMS with Some(vec![...]).
The issue here is that when 2 tests are running, a race condition happens with PARAMS -- a test writes them, then the other one overrides them with another set of values, then the first test's main gets wrong params and fails.
static PARAMS: Mutex<Option<Vec<String>>> = ...
fn set_params(data: &[&str]) {
let mut params = PARAMS.lock().unwrap()?;
*params = Some(data.iter().map(|v| v.to_string()).collect());
}
fn get_params() -> Option<Vec<String>> {
PARAMS.lock().unwrap().clone()
}
macro_rules! my_parser ($($varname:ident),+) => {
// read PARAMS into $varname's
/// if PARAMS is None, reads from std::env::args_os()
}
fn main() {
my_parser!(var1, var2, var3);
// do useful work
}
#[cfg(test)]
mod my_binary_tests {
#[test]
fn sample_call1() {
set_params("param1", "param2", "param3");
main();
}
#[test]
fn sample_call2() {
set_params("param4", "param5", "param6");
main();
}
}
Yes, it's viable. I actually tried to avoid that main + "real main" function tandem, but maybe that's the only sound way, and I'll try to minimize bureacracy with a macro.
In Python, it's a very ugly common pattern:
if __name__ == '__main__': # = this file is running as an executable
main()
and there's a package that was called argh that simplified this into a decorated function:
@argh.dispatch_command
def main(param1, param2, etc...):
# do useful work
The best solution is not to use globals. They are generally an anti-pattern, and Rust makes them even worse (on purpose, out of prejudice against global mutable state).
You could use thread-local storage. You can also make tests single-threaded:
Weren't you using clap before? Why are writing code like that now? Your whole code reminds me something like this, which is not very generous of me, but it feels like you're shooting yourself in the foot by not doing things simply.
Or have your own argument parser that you can test in isolation. It should take a slice of strings and output your desired struct maybe in a result, and you should just test that interface.
Clap looks like shooting sparrows with a cannon (in English it's equal to "drinking from firehose" as I understand). Clap adds 10 crates to my dependencies, to a project which already becomes slow to compile. And all I need is to pass filenames in arguments. Even if I consider some more amenities, like deserialization, implementing non-trivial Deserialize is pain in the neck. So I decided I don't need 10 extra crates, just a 100 SLOC.