tokio-rs / loom

Concurrency permutation testing tool for Rust.
MIT License
2.14k stars 111 forks source link

Making assertions about multiple iterations #144

Open paulkernfeld opened 4 years ago

paulkernfeld commented 4 years ago

As I was figuring out how to get started with loom, I wanted a way to ensure that a particular access does return multiple values. This made me feel more secure that I was using loom correctly. The solution I have below is a bit different than making assertions within the loom::model closure, because I can make claims about aggregates of all loom iterations, as opposed to making a claim about a single iteration.

The solution that I have below is what I ended up with (shown on the README example). Is there a better pattern for accomplishing this?

use loom::sync::Arc;
use loom::sync::atomic::AtomicUsize;
use loom::sync::atomic::Ordering::{Acquire, Release, Relaxed};
use loom::thread;
use std::collections::HashSet;

#[test]
fn seen_values() {
    // Using std::sync instead of loom::sync b/c I don't care about permuting these... I think?
    let seen_values = std::sync::Arc::new(std::sync::Mutex::new(HashSet::new()));
    let seen_values_cloned = seen_values.clone();

    loom::model(move || {
        let num = Arc::new(AtomicUsize::new(0));

        let ths: Vec<_> = (0..2)
            .map(|_| {
                let num = num.clone();
                thread::spawn(move || {
                    let curr = num.load(Acquire);
                    num.store(curr + 1, Release);
                })
            })
            .collect();

        for th in ths {
            th.join().unwrap();
        }

        seen_values_cloned.lock().unwrap().insert(num.load(Relaxed));
    });

    let expected: HashSet<usize> = [1, 2].iter().cloned().collect();
    assert_eq!(*seen_values.lock().unwrap(), expected);
}

I don't know the architecture or future plans for loom so this suggestion might not fit, but one idea I had was: what if loom::model returned an iterator of the return value of each closure call? Then I could write something like this:

use loom::sync::Arc;
use loom::sync::atomic::AtomicUsize;
use loom::sync::atomic::Ordering::{Acquire, Release, Relaxed};
use loom::thread;
use std::collections::HashSet;

#[test]
fn seen_values() {
    let seen_values = loom::model(move || {
        let num = Arc::new(AtomicUsize::new(0));

        let ths: Vec<_> = (0..2)
            .map(|_| {
                let num = num.clone();
                thread::spawn(move || {
                    let curr = num.load(Acquire);
                    num.store(curr + 1, Release);
                })
            })
            .collect();

        for th in ths {
            th.join().unwrap();
        }

        // The closure would need to be parameterized to return a user-chosen type
        num.load(Relaxed)
    }).collect::<HashSet<usize>>();

    let expected: HashSet<usize> = [1, 2].iter().cloned().collect();
    assert_eq!(seen_values, expected);
}