我有一个测试集合。有几个测试需要访问一个共享资源(外部库/API/硬件设备)。如果这些测试中的任何一个并行运行,它们都会失败。我知道我可以使用--test-threads=1运行所有的东西,但是我发现仅仅对于几个特殊的测试来说是不方便的。有没有办法保持所有的测试都并行运行,并且有一些测试例外?理想情况下,我想说不要同时运行X,Y,Z。
--test-threads=1
fnx2tebb1#
使用serial_test板条箱。添加此板条箱后,您可以输入代码:
#[serial]
放在您希望按顺序运行任何测试之前。
ykejflvf2#
正如mcarton在注解中提到的,可以使用Mutex来防止多段代码同时运行:
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运行,可以看到行为上的差异:
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中毒。这将导致随后的测试无法获取锁。如果您需要避免这种情况,并且您知道锁定的资源处于良好状态(()应该很好...),您可以处理中毒:
assert!
()
let _shared = THE_RESOURCE.lock().unwrap_or_else(|e| e.into_inner());
如果您需要并行运行有限的线程集,可以使用信号量。在这里,我使用Condvar和Mutex构建了一个很差的信号量:
Condvar
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(()) })
另见:
hwazgwia3#
您可以随时提供自己的测试工具,方法是将[[test]]条目添加到Cargo.toml:
[[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函数,并自己添加所有的“运行测试”逻辑,是的,这是一些工作,但至少你可以自己决定运行测试的一切。也可以创建两个测试文件:
cargo test
my_test.rs
main
tests/ - sequential.rs - parallel.rs
然后,您需要运行cargo test --test sequential -- --test-threads=1和cargo test --test parallel,因此它不能与单个cargo test一起工作,但是您不需要编写自己的测试工具逻辑。
cargo test --test sequential -- --test-threads=1
cargo test --test parallel
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),或者测试逻辑的相反。但我的观点是,总是先尝试简单的解决方法,不要在没有必要的时候把问题复杂化。
#[should_panic]
Result
4条答案
按热度按时间fnx2tebb1#
使用serial_test板条箱。添加此板条箱后,您可以输入代码:
放在您希望按顺序运行任何测试之前。
ykejflvf2#
正如mcarton在注解中提到的,可以使用
Mutex
来防止多段代码同时运行:如果使用
cargo test -- --nocapture
运行,可以看到行为上的差异:无锁
带锁
理想情况下,您应该将外部资源 * 本身 * 放在
Mutex
中,以使代码表示它是单例的事实,并消除记住锁定其他未使用的Mutex
的需要。这确实有一个“巨大的”缺点,即测试中的异常(也称为
assert!
失败)将导致Mutex
中毒。这将导致随后的测试无法获取锁。如果您需要避免这种情况,并且您知道锁定的资源处于良好状态(()
应该很好...),您可以处理中毒:如果您需要并行运行有限的线程集,可以使用信号量。在这里,我使用
Condvar
和Mutex
构建了一个很差的信号量:另见:
hwazgwia3#
您可以随时提供自己的测试工具,方法是将
[[test]]
条目添加到Cargo.toml
:在这种情况下,
cargo test
将把my_test.rs
编译成一个普通的可执行文件,这意味着你必须提供一个main
函数,并自己添加所有的“运行测试”逻辑,是的,这是一些工作,但至少你可以自己决定运行测试的一切。也可以创建两个测试文件:
然后,您需要运行
cargo test --test sequential -- --test-threads=1
和cargo test --test parallel
,因此它不能与单个cargo test
一起工作,但是您不需要编写自己的测试工具逻辑。jmp7cifd4#
我觉得很有趣的是,每个人都本能地去找互斥锁(一开始也包括我)。然后我意识到,在大多数情况下,强制顺序执行最简单的方法就是把所有的东西都放在一个函数中。当我说不是的时候,这是显而易见的。
改变...
变成...
有关真实示例,请参见ellie/atuin#748。
当然,有很多情况下,这个解决方案将不包括在内。例如:
1.如果你使用某种测试框架,它会变得有点复杂。
1.这不适用于标记为
#[should_panic]
的测试:你必须切换到可恢复的错误(例如Result
),或者测试逻辑的相反。但我的观点是,总是先尝试简单的解决方法,不要在没有必要的时候把问题复杂化。