-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
rstest does not ensure the execution order of cases #183
Comments
Can you write a simple example of how this become an issue?
The test names are deterministic but the execution not due cargo test
implementation that spawn a test for each case.
Il sab 4 mar 2023, 10:34 ChAoS_UnItY ***@***.***> ha scritto:
… As titled, rstest seems unable to ensure definite execution orders of
cases, this would be a serious case when other testing framework comes in,
such as insta <https://crates.io/crates/insta>, which would compares
current snapshot named with current testing function with previous one.
When there are more than 1 cases, it would possibly messed up test case
functions' name (order number) and caused further unexpected issues.
—
Reply to this email directly, view it on GitHub
<#183>, or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA5Y34IBUHW4NK422ICXVRTW2MEDNANCNFSM6AAAAAAVPOJD3Q>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
Ok, let's consider we have the following code for testing: use insta::assert_yaml_snapshot;
use rstest::rstest;
#[rstest]
#[case(Some(()))]
#[case(None)]
fn test_option_value(#[case] value: Option<()>) {
assert_yaml_snapshot!(value);
} Running this test, I would expect that there'll be 2 snapshot files, which are But after running tests for multiple times, it sometimes appears that their result contents are swapped (mainly due to execution order which effects test case name), and this would causes test to fail due to content mismatched. |
Follow code expose the issue almost every time. Seams that I'll go deaply later use insta::assert_yaml_snapshot;
use rstest::rstest;
#[rstest]
fn test_option_value(#[values(1,2,3,4,5,6,7,8,9,10)] value: u32) {
assert_yaml_snapshot!(value);
} |
Ok, the issue seams in Follow code that not use use insta::assert_yaml_snapshot;
mod test_option_value {
use super::*;
fn assert(v: u32) {
assert_yaml_snapshot!(v);
}
#[test]
fn value_01_1() {
assert(1);
}
#[test]
fn value_02_2() {
assert(2);
}
#[test]
fn value_03_3() {
assert(3);
}
#[test]
fn value_04_4() {
assert(4);
}
#[test]
fn value_05_5() {
assert(5);
}
#[test]
fn value_06_6() {
assert(6);
}
#[test]
fn value_07_7() {
assert(7);
}
#[test]
fn value_08_8() {
assert(8);
}
#[test]
fn value_09_9() {
assert(9);
}
#[test]
fn value_10_10() {
assert(10);
}
} Maybe is better replace rstest/rstest_test/src/utils.rs Line 349 in 23b5311
cargo test implementation.
|
One solution I've found to this (in the context of this issue, maybe counts more as a workaround...) is to use named snapshots, where the docs give an example like: #[test]
fn test_something() {
assert_snapshot!("first_snapshot", "first value");
assert_snapshot!("second_snapshot", "second value");
} The first argument to the family of So if you're explicitly specifying a snapshot name, I've found That gets old quickly, as you'll have to assemble a string from function signature arguments by hand for each test. Note this assumes the tuple of all current arguments to the test function uniquely identifies the test. If you have a duplicate test, this no holder holds true, but should be very easy to fix and rarely happen in real life: just remove the duplicate test. And even if it duplicates remain present, So this is where I came up with a macro. It's overengineered and probably poorly implemented (new to Rust). It generates a The macro reads: pub fn sanitize_for_filename_use(filename: &str) -> String {
const REPLACEMENT: char = '_';
filename
.replace(
[
' ', ':', '<', '>', '\"', '/', '\\', '|', '?', '*', '\n', '\r',
],
&REPLACEMENT.to_string(),
)
// Collapse consecutive underscores into one
.split(REPLACEMENT)
.filter(|&s| !s.is_empty())
.collect::<Vec<_>>()
.join(&REPLACEMENT.to_string())
}
#[macro_export]
macro_rules! instrament {
($(#[$attr:meta])* fn $name:ident ( $( $(#[$arg_attr:meta])* $arg:ident : $type:ty),* ) $body:expr ) => {
::paste::paste! {
#[derive(::serde::Serialize)]
struct [<$name:camel>]<'a> {
$( $arg: &'a $type, )*
}
impl<'a> ::std::fmt::Display for [<$name:camel>]<'a> {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let items: Vec<String> = vec![
stringify!($name).to_owned() ,
$(
format!("{:#?}", self.$arg).to_owned() ,
)*
];
let name = $crate::sanitize_for_filename_use(&items.join("-"));
write!(f, "{}", name)
}
}
$(#[$attr])*
fn $name ( $( $(#[$arg_attr])* $arg : $type),* ) {
let function_data = [<$name:camel>] { $($arg: &$arg),* };
let mut settings = ::insta::Settings::clone_current();
settings.set_info(&function_data);
settings.bind(|| {
#[allow(clippy::redundant_closure_call)]
$body(&function_data);
});
}
}
};
} and a test example reads: use itertools::Itertools;
fn power_set<C, T>(collection: C) -> Vec<Vec<T>>
where
C: IntoIterator<Item = T>,
T: Clone,
{
let vec = collection.into_iter().collect_vec();
// https://en.wikipedia.org/wiki/Power_set#Properties
let mut result = Vec::with_capacity(2usize.checked_pow(vec.len() as u32).expect("Overflow"));
for i in 0..=vec.len() {
result.extend(vec.iter().cloned().combinations(i));
}
result
}
#[cfg(test)]
mod tests {
use super::power_set;
use instrament::instrament;
use rstest::rstest;
instrament! {
#[rstest]
fn test_power_set(
#[values(vec![], vec![1], vec![1, 2], vec![1, 2, 3])]
collection: Vec<i32>
) (|data: &TestPowerSet| {
let result = power_set(collection.clone());
insta::assert_yaml_snapshot!(data.to_string(), result);
})
}
} in which the // Recursive expansion of instrament! macro
// =========================================
#[derive(::serde::Serialize)]
struct TestPowerSet<'a> {
collection: &'a Vec<i32>,
}
impl<'a> ::std::fmt::Display for TestPowerSet<'a> {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let items: Vec<String> = (<[_]>::into_vec(
#[rustc_box]
$crate::boxed::Box::new([
("test_power_set".to_owned()),
({
let res = $crate::fmt::format(::core::fmt::Arguments::new_v1(
&[""],
&[::core::fmt::ArgumentV1::new(
&(self.collection),
::core::fmt::Display::fmt,
)],
));
res
}
.to_owned()),
]),
));
let name = $crate::sanitize_for_filename_use(&items.join("-"));
f.write_fmt(::core::fmt::Arguments::new_v1(
&[""],
&[::core::fmt::ArgumentV1::new(
&(name),
::core::fmt::Display::fmt,
)],
))
}
}
#[rstest]
fn test_power_set(#[values(vec![],vec![1],vec![1,2],vec![1,2,3])] collection: Vec<i32>) {
let function_data = TestPowerSet {
collection: &collection,
};
let mut settings = ::insta::Settings::clone_current();
settings.set_info(&function_data);
settings.bind(|| {
#[allow(clippy::redundant_closure_call)]
(|data: &TestPowerSet| {
let result = power_set(collection.clone());
{
{
let value = $crate::_macro_support::serialize_value(
&result,
$crate::_macro_support::SerializationFormat::Yaml,
$crate::_macro_support::SnapshotLocation::File,
);
{
$crate::_macro_support::assert_snapshot(
(Some((data.to_string()))).into(),
&value,
"/some/path/to/project",
{
fn f() {}
fn type_name_of_val<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
let mut name =
type_name_of_val(f).strip_suffix("::f").unwrap_or("");
while let Some(rest) = name.strip_suffix("::{{closure}}") {
name = rest;
}
name
},
"module::path",
"",
0 as u32,
("result"),
)
.unwrap()
};
};
};
})(&function_data);
});
} The So far, to address the core issue of this thread, I have observed no instability ( One caveat is that, while snapshot names will be sanitized for filename use for both Linux and Windows, on a case-insensitive file system you cannot have test cases that, when cast to a filename via Another disadvantage is that intellisense shits the bed (as it does for other macros as well though), and the closure syntax |
THX @alexpovel to share your solution here. You're right, is not possible to use default named snapshot because sadly Anyway you can use something like use insta::assert_yaml_snapshot;
use rstest::rstest;
#[fixture]
pub fn testname() -> String {
thread::current().name().unwrap().to_string()
}
#[rstest]
fn test_option_value(testname: String, #[values(1,2,3,4,5,6,7,8,9,10)] value: u32) {
assert_yaml_snapshot!(testname, value);
} The |
I just ran into this problem by combining rstest with insta. My solution is to input the values as a tuple of (name, case) instead, to get stable names: So, instead of: #[rstest]
fn test_option_value(
#[values(
1,
2
)]
value: u32
) {
assert_yaml_snapshot!(value);
} This: #[rstest]
fn test_option_value(
#[values(
("foo", 1),
("bar", 2)
)]
value: (&str, u32)
) {
assert_yaml_snapshot!(value.0, value.1);
} |
As titled, rstest seems unable to ensure definite execution orders of cases, this would be a serious case when other testing framework comes in, such as insta, which would compares current snapshot named with current testing function with previous one.
When there are more than 1 cases, it would possibly messed up test case functions' name (order number) and caused further unexpected issues.
The text was updated successfully, but these errors were encountered: