难以在 Rust 中创建用于测试目的的模拟 'sink'

Difficulty creating a mock 'sink' for testing purposes in Rust

上下文

我有一个函数可以使用人造丝来并行化一些工作。每个线程都应该将其输出写入一个单独的消费者。实际上,这些消费者通常会写入(单独的)磁盘上的文件。为了测试,我希望它们附加到矢量,然后我可以对其进行断言。

问题

事实上,即使使用此代码的串行版本,我也遇到了问题。 我要测试的函数是func_under_test。我希望 get_sink 在每次调用时创建并分发一个新的 'sink'(一个 Vec<i32>),它将在 func_under_test 中。但我也想在我的 main 函数中保留对这些向量的引用,这样我就可以对它们进行断言。我试过这样写:

fn func_under_test<GetSink, F>(
    mut get_sink: GetSink
)
where
    GetSink: FnMut(usize) -> F,
    F: FnMut(i32) -> (),
{

    let idxs : Vec<usize> = (0..2).collect();
    idxs.iter().for_each(|&i| {
        let mut sink = get_sink(i);
        sink(0i32);
        sink(1i32);
        sink(2i32);
    });
}

fn main() {

    let mut sinks : Vec<&Vec<i32>> = vec![];
    let get_sink = |i: usize| {
        let mut sink : Vec<i32> = vec![];
        sinks.push(&sink);
        move |x : i32| sink.push(x)
    };

    func_under_test(get_sink);

    assert_eq!(**sinks.get(0).unwrap(), vec![0i32, 1i32, 2i32]);
}

但是我得到这个编译错误:

error[E0597]: `sink` does not live long enough
  --> src/main.rs:23:20
   |
20 |     let mut sinks : Vec<&Vec<i32>> = vec![];
   |         --------- lifetime `'1` appears in the type of `sinks`
...
23 |         sinks.push(&sink);
   |         -----------^^^^^-
   |         |          |
   |         |          borrowed value does not live long enough
   |         argument requires that `sink` is borrowed for `'1`
24 |         move |x : i32| sink.push(x)
25 |     };
   |     - `sink` dropped here while still borrowed

error[E0505]: cannot move out of `sink` because it is borrowed
  --> src/main.rs:24:9
   |
20 |     let mut sinks : Vec<&Vec<i32>> = vec![];
   |         --------- lifetime `'1` appears in the type of `sinks`
...
23 |         sinks.push(&sink);
   |         -----------------
   |         |          |
   |         |          borrow of `sink` occurs here
   |         argument requires that `sink` is borrowed for `'1`
24 |         move |x : i32| sink.push(x)
   |         ^^^^^^^^^^^^^^ ---- move occurs due to use in closure
   |         |
   |         move out of `sink` occurs here

我认为我需要在这种情况下使用类似 Rc 的想法是否正确,因为我需要对每个 Vec<i32> 有两个引用? 由于这些引用中只有一个需要可变,我是否需要在一种情况下使用 Rc<Cell<Vec<i32>>>,而在另一种情况下只需要使用 Rc<Vec<i32>>

可以使用Arc<Mutex<i32>>来共享对向量的可变访问。 (Rc 不是线程安全的。)

一个更简洁的解决方案是 typed_arena::Arena,它允许您基本上编写您尝试过的代码,但角色互换:Vec<i32>s 始终归竞技场所有(因此它func_under_tests)。然后,在 all 对竞技场的引用消失后,您可以将其转换为其元素的向量。

use typed_arena::Arena;

fn main() {
    let sinks: Arena<Vec<i32>> = Arena::new();
    let get_sink = |_i: usize| {
        let sink_ref = sinks.alloc(Vec::new());
        move |x| sink_ref.push(x)
    };

    func_under_test(get_sink);

    assert_eq!(sinks.into_vec()[0], vec![0i32, 1i32, 2i32]);
}

我想我已经找到了答案。执行测试的线程需要访问内部向量,填充它的线程也是如此。所以它们需要被包裹在一个 Arc 和一个 Mutex 中。但是外部向量没有,因为它只是被 get_sink 闭包借用。

所以 sinks 可以有类型 Vec<Arc<Mutex<Vec<i32>>>.

无论如何,我可以写这个,它确实编译和运行符合预期:

use rayon::prelude::*;
use std::sync::{Arc, Mutex};

fn func_under_test<G, S>(
    get_sink: G)
where
    G: Fn(&usize) -> S + Sync,
    S: FnMut(i32) -> (),
{
    let idxs : Vec<usize> = vec![0, 1, 2];

    idxs.par_iter().for_each(|i| {
        let mut sink : S = get_sink(i);
        for j in 0..3 {
            sink(j + 3*(*i) as i32);
        }
    });
}

fn main() {

    let sinks : Vec<Arc<Mutex<Vec<i32>>>> = vec![
        Arc::new(Mutex::new(vec![])),
        Arc::new(Mutex::new(vec![])),
        Arc::new(Mutex::new(vec![])),
    ];

    let get_sink = |i: &usize| {
        let m : Arc<Mutex<Vec<i32>>> = sinks.get(*i).unwrap().clone();
        move |x: i32| {
            m.lock().unwrap().push(x);
        }
    };
    
    func_under_test(get_sink);

    assert_eq!(*sinks.get(0).unwrap().lock().unwrap(), vec![0, 1, 2]);
    assert_eq!(*sinks.get(1).unwrap().lock().unwrap(), vec![3, 4, 5]);
    assert_eq!(*sinks.get(2).unwrap().lock().unwrap(), vec![6, 7, 8]);
}

似乎 G 在这里实现了正确的特征:它是 Sync,因为它需要在线程之间共享。但是是不是Send,因为它不需要在线程之间发送信息。