rust 如何避免并行运行一些测试?

d6kp6zgx  于 2023-03-18  发布在  其他
关注(0)|答案(4)|浏览(151)

我有一个测试集合。有几个测试需要访问一个共享资源(外部库/API/硬件设备)。如果这些测试中的任何一个并行运行,它们都会失败。
我知道我可以使用--test-threads=1运行所有的东西,但是我发现仅仅对于几个特殊的测试来说是不方便的。
有没有办法保持所有的测试都并行运行,并且有一些测试例外?理想情况下,我想说不要同时运行X,Y,Z。

fnx2tebb

fnx2tebb1#

使用serial_test板条箱。添加此板条箱后,您可以输入代码:

#[serial]

放在您希望按顺序运行任何测试之前。

ykejflvf

ykejflvf2#

正如mcarton在注解中提到的,可以使用Mutex来防止多段代码同时运行:

use once_cell::sync::Lazy; // 1.4.0
use std::{sync::Mutex, thread::sleep, time::Duration};

static THE_RESOURCE: Lazy<Mutex<()>> = Lazy::new(Mutex::default);

type TestResult<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;

#[test]
fn one() -> TestResult {
    let _shared = THE_RESOURCE.lock()?;
    eprintln!("Starting test one");
    sleep(Duration::from_secs(1));
    eprintln!("Finishing test one");
    Ok(())
}

#[test]
fn two() -> TestResult {
    let _shared = THE_RESOURCE.lock()?;
    eprintln!("Starting test two");
    sleep(Duration::from_secs(1));
    eprintln!("Finishing test two");
    Ok(())
}

如果使用cargo test -- --nocapture运行,可以看到行为上的差异:

无锁

running 2 tests
Starting test one
Starting test two
Finishing test two
Finishing test one
test one ... ok
test two ... ok

带锁

running 2 tests
Starting test one
Finishing test one
Starting test two
test one ... ok
Finishing test two
test two ... ok

理想情况下,您应该将外部资源 * 本身 * 放在Mutex中,以使代码表示它是单例的事实,并消除记住锁定其他未使用的Mutex的需要。
这确实有一个“巨大的”缺点,即测试中的异常(也称为assert!失败)将导致Mutex中毒。这将导致随后的测试无法获取锁。如果您需要避免这种情况,并且您知道锁定的资源处于良好状态(()应该很好...),您可以处理中毒:

let _shared = THE_RESOURCE.lock().unwrap_or_else(|e| e.into_inner());

如果您需要并行运行有限的线程集,可以使用信号量。在这里,我使用CondvarMutex构建了一个很差的信号量:

use std::{
    sync::{Condvar, Mutex},
    thread::sleep,
    time::Duration,
};

#[derive(Debug)]
struct Semaphore {
    mutex: Mutex<usize>,
    condvar: Condvar,
}

impl Semaphore {
    fn new(count: usize) -> Self {
        Semaphore {
            mutex: Mutex::new(count),
            condvar: Condvar::new(),
        }
    }

    fn wait(&self) -> TestResult {
        let mut count = self.mutex.lock().map_err(|_| "unable to lock")?;
        while *count == 0 {
            count = self.condvar.wait(count).map_err(|_| "unable to lock")?;
        }
        *count -= 1;
        Ok(())
    }

    fn signal(&self) -> TestResult {
        let mut count = self.mutex.lock().map_err(|_| "unable to lock")?;
        *count += 1;
        self.condvar.notify_one();
        Ok(())
    }

    fn guarded(&self, f: impl FnOnce() -> TestResult) -> TestResult {
        // Not panic-safe!
        self.wait()?;
        let x = f();
        self.signal()?;
        x
    }
}

lazy_static! {
    static ref THE_COUNT: Semaphore = Semaphore::new(4);
}
THE_COUNT.guarded(|| {
    eprintln!("Starting test {}", id);
    sleep(Duration::from_secs(1));
    eprintln!("Finishing test {}", id);
    Ok(())
})

另见:

hwazgwia

hwazgwia3#

您可以随时提供自己的测试工具,方法是将[[test]]条目添加到Cargo.toml

[[test]]
name = "my_test"
# If your test file is not `tests/my_test.rs`, add this key:
#path = "path/to/my_test.rs" 
harness = false

在这种情况下,cargo test将把my_test.rs编译成一个普通的可执行文件,这意味着你必须提供一个main函数,并自己添加所有的“运行测试”逻辑,是的,这是一些工作,但至少你可以自己决定运行测试的一切。
也可以创建两个测试文件:

tests/
  - sequential.rs
  - parallel.rs

然后,您需要运行cargo test --test sequential -- --test-threads=1cargo test --test parallel,因此它不能与单个cargo test一起工作,但是您不需要编写自己的测试工具逻辑。

jmp7cifd

jmp7cifd4#

我觉得很有趣的是,每个人都本能地去找互斥锁(一开始也包括我)。然后我意识到,在大多数情况下,强制顺序执行最简单的方法就是把所有的东西都放在一个函数中。当我说不是的时候,这是显而易见的。
改变...

#[cfg(test)]
mod test {
    #[test]
    fn test1() {
        // ...
    }
    #[test]
    fn test2() {
        // ...
    }
}

变成...

#[cfg(test)]
mod test {
    #[test]
    fn test_all_sequential() {
        test1();
        test2();
    }
    fn test1() {
        // ...
    }
    fn test2() {
        // ...
    }
}

有关真实示例,请参见ellie/atuin#748
当然,有很多情况下,这个解决方案将不包括在内。例如:
1.如果你使用某种测试框架,它会变得有点复杂。
1.这不适用于标记为#[should_panic]的测试:你必须切换到可恢复的错误(例如Result),或者测试逻辑的相反。
但我的观点是,总是先尝试简单的解决方法,不要在没有必要的时候把问题复杂化。

相关问题