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

How can I avoid running some tests in parallel?

我有一组测试。有一些测试需要访问共享资源(外部 library/API/hardware 设备)。如果这些测试中的任何一个 运行 并行,它们就会失败。

我知道我可以 运行 使用 --test-threads=1 的一切,但我发现这对一些特殊测试来说不方便。

有什么方法可以使所有测试保持 运行 并行进行并有少数例外?理想情况下,我想说不要同时 运行 X、Y、Z。

作为, you can use a Mutex防止多段代码同时被运行ning:

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.

这确实有 巨大的 缺点,即测试中的恐慌(a.k.a 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(())
})

另请参阅:

  • How to limit the number of test threads in 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 testmy_test.rs 编译为普通的可执行文件。这意味着您必须提供一个 main 函数并自己添加所有 "run tests" 逻辑。是的,这是一些工作,但至少您可以自己决定有关 运行ning 测试的所有内容。


您还可以创建两个测试文件:

tests/
  - sequential.rs
  - parallel.rs

然后您将需要 运行 cargo test --test sequential -- --test-threads=1cargo test --test parallel。所以它不适用于单个 cargo test,但您不需要编写自己的测试工具逻辑。

使用 serial_test 箱子。添加这个箱子后,您输入代码:

#[serial]

在您想要的任何测试前面 运行 按顺序。