【问题标题】:How can I avoid running some tests in parallel?如何避免并行运行一些测试?
【发布时间】:2018-08-05 11:45:11
【问题描述】:

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

我知道我可以使用 --test-threads=1 运行所有内容,但我发现仅进行几次特殊测试就很不方便。

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

【问题讨论】:

  • 无法表示只有部分测试不能并行运行。这些测试应该通过Mutex 访问共享资源。
  • 听起来你有“测试”和“集成测试”。将它们分成两个项目并分别运行听起来很合理。
  • 是的,这可能是一种方式......集成测试需要在单个线程中运行

标签: rust rust-cargo


【解决方案1】:

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

#[serial]

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

【讨论】:

  • 最近你现在可以跨进程串行了。
【解决方案2】:

作为mcarton mentions in the comments,你可以使用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 中,以使代码表示它是单例的事实,并且无需记住锁定其他未使用的@987654331 @。

这确实有一个巨大的缺点,即测试中的恐慌(又名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(())
})

另见:

【讨论】:

  • 正在寻找这个。对于核心团队来说,这将是一个有趣的特性(防止一些测试并行运行)。我认为这与更大、更复杂的代码库变得更加相关。
  • @Dash83 随着即将到来的对自定义测试工具的支持,我希望这样的事情会出现在一些新的测试工具中。对于核心测试框架来说,这确实是一个小众的东西。
  • 很高兴知道。作为参考,我不认为排除测试并行运行的特性是那个利基。使用共享资源的大型代码库通常有这个要求。
【解决方案3】:

您始终可以提供自己的测试工具。您可以通过将[[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,但您不需要编写自己的测试工具逻辑。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-26
    • 2018-04-01
    • 1970-01-01
    • 2020-06-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多