├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── execution-order.rs ├── simple.rs └── time-manipulation.rs └── src ├── lib.rs ├── time.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diviner" 3 | description = "Diviner, a deterministic testing framework" 4 | version = "0.2.0" 5 | authors = ["Xuejie Xiao "] 6 | edition = "2018" 7 | license = "MIT" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | async-task = "1.3.1" 13 | futures = "0.3.1" 14 | pin-utils = "0.1.0-alpha.4" 15 | rand = "0.7.3" 16 | rand_chacha = "0.2.2" 17 | 18 | [dev-dependencies] 19 | byteorder = "1.3.4" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Xuejie Xiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diviner 2 | 3 | Diviner is a [FoundationDB style simulation testing](https://apple.github.io/foundationdb/testing.html) framework for Rust. It includes 2 parts: 4 | 5 | * A [Future executor](https://rust-lang.github.io/async-book/02_execution/04_executor.html) that is designed to be single threaded and deterministic. The goal here is to enable deterministic simulations in Rust. 6 | * Wrappers over existing Rust async IO libraries. When building normally, the wrappers will use the actual implementation, but when building with `simulation` feature enabled, the wrappers will use mocked implementations that integrate with above Future executor to enable deterministic testing. The wrappers mentioned here, might inclue (but are not limited to): 7 | + Time related functions, such as sleep or now; 8 | + Network related modules, such as TCPListener, TCPStream; 9 | + File IO related modules; 10 | 11 | If you find the above term confusing, [this video](https://www.youtube.com/watch?v=4fFDFbi3toc) might help explain what diviner provides. 12 | 13 | The goal here, is to enable deterministic testing on any Rust code satisfying the following rules: 14 | 15 | 1. The code is written in [async/await](https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html) style; 16 | 2. The code uses wrappers provided by diviner to perform all the necessary IOs; 17 | 18 | # Examples 19 | 20 | To illustrate what the library does, several examples are provided in the repository: 21 | 22 | * [simple](https://github.com/xxuejie/diviner/blob/f368ad6fe1cb367ef65ed21f6f220a367328c184/examples/simple.rs): a minimal piece of code written in async/await style; 23 | * [time-manipulation](https://github.com/xxuejie/diviner/blob/f368ad6fe1cb367ef65ed21f6f220a367328c184/examples/time-manipulation.rs): time manipulation example, here we are testing sleeping for a whole day, but the simulation code would manipulate time and finish immediately; 24 | * [execution-order](https://github.com/xxuejie/diviner/blob/f368ad6fe1cb367ef65ed21f6f220a367328c184/examples/execution-order.rs): a poorly implemented agent, diviner can use different seeds to test different execution orders of futures. So later you can determinisically debug the code with the seed that will trigger the errors; 25 | -------------------------------------------------------------------------------- /examples/execution-order.rs: -------------------------------------------------------------------------------- 1 | use diviner::{spawn, Environment}; 2 | use rand::prelude::*; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | struct Agent { 6 | value: u64, 7 | } 8 | 9 | impl Agent { 10 | async fn current_value(&self) -> u64 { 11 | self.value 12 | } 13 | 14 | async fn update(&mut self, value: u64) { 15 | self.value = value 16 | } 17 | } 18 | 19 | fn main() { 20 | let mut success_seed: Option = None; 21 | let mut failure_seed: Option = None; 22 | for _ in 0..1000 { 23 | let seed: u64 = random(); 24 | let e = Environment::new_with_seed(seed); 25 | let result = e.block_on(async { 26 | let agent = Arc::new(Mutex::new(Agent { value: 12 })); 27 | let _a = { 28 | let agent2 = Arc::clone(&agent); 29 | spawn(async move { agent2.lock().unwrap().update(14).await }) 30 | }; 31 | let b = { 32 | let agent2 = Arc::clone(&agent); 33 | spawn(async move { agent2.lock().unwrap().current_value().await }) 34 | }; 35 | let value = b.await.expect("Task canceled").expect("Task failed"); 36 | assert!(value == 14, "Update is not performed!"); 37 | }); 38 | match result { 39 | Ok(_) => { 40 | success_seed = Some(seed); 41 | } 42 | Err(_) => { 43 | failure_seed = Some(seed); 44 | } 45 | } 46 | if success_seed.is_some() && failure_seed.is_some() { 47 | break; 48 | } 49 | } 50 | if let Some(seed) = success_seed { 51 | println!("Task succeeded with seed: {}", seed); 52 | } 53 | if let Some(seed) = failure_seed { 54 | println!("Task failed with seed: {}", seed); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use diviner::Environment; 2 | 3 | fn main() { 4 | let result = Environment::new() 5 | .block_on(async { 6 | let f = async { 7 }; 7 | f.await 8 | }) 9 | .expect("block on failure!"); 10 | assert!(result == 7, "Returned result is not 7!"); 11 | } 12 | -------------------------------------------------------------------------------- /examples/time-manipulation.rs: -------------------------------------------------------------------------------- 1 | use diviner::{ 2 | time::{now, sleep}, 3 | Environment, 4 | }; 5 | use std::time::Duration; 6 | 7 | fn main() { 8 | let result = Environment::new() 9 | .block_on(async { 10 | let a = now(); 11 | sleep(Duration::from_secs(24 * 3600)).await; 12 | let b = now(); 13 | b.duration_since(a) 14 | .expect("duration calculation error") 15 | .as_secs() 16 | }) 17 | .expect("block_on failure"); 18 | assert!(result >= 24 * 3600, "A day must have passed since sleep!"); 19 | } 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod time; 2 | mod utils; 3 | 4 | use crate::time::{now, TimerEntry}; 5 | use futures::future::FutureExt; 6 | use rand::{Rng, SeedableRng}; 7 | use rand_chacha::ChaCha8Rng; 8 | use std::cell::RefCell; 9 | use std::collections::binary_heap::BinaryHeap; 10 | use std::future::Future; 11 | use std::panic::AssertUnwindSafe; 12 | use std::sync::{Arc, Mutex}; 13 | use std::task::{Context, Poll}; 14 | use std::thread::Result; 15 | use std::time::SystemTime; 16 | 17 | type Task = async_task::Task<()>; 18 | type JoinHandle = async_task::JoinHandle; 19 | 20 | pub struct Environment { 21 | rng: ChaCha8Rng, 22 | current_time: SystemTime, 23 | tasks: Vec, 24 | timers: BinaryHeap, 25 | } 26 | 27 | thread_local! { 28 | pub(crate) static ENV: RefCell> = RefCell::new(None); 29 | } 30 | 31 | impl Environment { 32 | pub fn new() -> Self { 33 | Environment { 34 | rng: ChaCha8Rng::from_entropy(), 35 | current_time: SystemTime::now(), 36 | tasks: vec![], 37 | timers: BinaryHeap::new(), 38 | } 39 | } 40 | 41 | pub fn new_with_seed(seed: u64) -> Self { 42 | Environment { 43 | rng: ChaCha8Rng::seed_from_u64(seed), 44 | current_time: SystemTime::now(), 45 | tasks: vec![], 46 | timers: BinaryHeap::new(), 47 | } 48 | } 49 | 50 | pub fn block_on(self, future: F) -> Result 51 | where 52 | F: Future + 'static, 53 | R: Send + 'static, 54 | { 55 | let root_future = AssertUnwindSafe(future).catch_unwind(); 56 | pin_utils::pin_mut!(root_future); 57 | ENV.with(|e| { 58 | assert!( 59 | e.borrow().is_none(), 60 | "Current thread should not have an environment when calling block_on!" 61 | ); 62 | e.replace(Some(self)); 63 | }); 64 | let root_runnable_flag = Arc::new(Mutex::new(true)); 65 | let waker = { 66 | let flag2 = Arc::clone(&root_runnable_flag); 67 | async_task::waker_fn(move || *flag2.lock().expect("root waker") = true) 68 | }; 69 | let root_cx = &mut Context::from_waker(&waker); 70 | let result = loop { 71 | let root_runnable = *root_runnable_flag.lock().expect("polling root"); 72 | let mut num = ENV.with(|e| e.borrow().as_ref().unwrap().tasks.len()); 73 | if root_runnable { 74 | num += 1; 75 | } 76 | if num > 0 { 77 | let i = ENV.with(|e| e.borrow_mut().as_mut().unwrap().rng.gen_range(0, num)); 78 | if root_runnable && i == 0 { 79 | *root_runnable_flag.lock().expect("suspending root") = false; 80 | if let Poll::Ready(output) = root_future.as_mut().poll(root_cx) { 81 | break output; 82 | } 83 | } else { 84 | let index = if root_runnable { i - 1 } else { i }; 85 | let task = ENV.with(|e| e.borrow_mut().as_mut().unwrap().tasks.remove(index)); 86 | task.run(); 87 | } 88 | continue; 89 | } 90 | if let Some(entry) = ENV.with(|e| e.borrow_mut().as_mut().unwrap().timers.pop()) { 91 | if entry.wake_time >= now() { 92 | ENV.with(|e| { 93 | e.borrow_mut().as_mut().unwrap().current_time = entry.wake_time; 94 | }); 95 | for waker in entry.wakers { 96 | waker.wake(); 97 | } 98 | continue; 99 | } 100 | } 101 | break Err(Box::new("No task is runnable!")); 102 | }; 103 | ENV.with(|e| { 104 | e.replace(None); 105 | }); 106 | result 107 | } 108 | } 109 | 110 | pub fn spawn(future: F) -> JoinHandle> 111 | where 112 | F: Future + 'static, 113 | R: Send + 'static, 114 | { 115 | let future = AssertUnwindSafe(future).catch_unwind(); 116 | let schedule = |t| { 117 | ENV.with(|e| { 118 | e.borrow_mut().as_mut().unwrap().tasks.push(t); 119 | }) 120 | }; 121 | let (task, handle) = async_task::spawn_local(future, schedule, ()); 122 | task.schedule(); 123 | 124 | handle 125 | } 126 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | use crate::{spawn, utils::Signal, ENV}; 2 | use std::cmp::{Ordering, Reverse}; 3 | use std::task::Waker; 4 | use std::time::{Duration, SystemTime}; 5 | 6 | pub async fn sleep(dur: Duration) { 7 | let wake_time = now() + dur; 8 | let attach = move |waker, waker2| { 9 | let entry = TimerEntry { 10 | wake_time, 11 | wakers: vec![waker, waker2], 12 | }; 13 | ENV.with(|e| e.borrow_mut().as_mut().unwrap().timers.push(entry)); 14 | }; 15 | spawn(Signal::new(attach)) 16 | .await 17 | .expect("Task is unexpected canceled!") 18 | .expect("Sleep task failure!") 19 | } 20 | 21 | pub fn now() -> SystemTime { 22 | ENV.with(|e| e.borrow().as_ref().unwrap().current_time) 23 | } 24 | 25 | pub(crate) struct TimerEntry { 26 | pub(crate) wake_time: SystemTime, 27 | pub(crate) wakers: Vec, 28 | } 29 | 30 | impl Ord for TimerEntry { 31 | fn cmp(&self, other: &Self) -> Ordering { 32 | Reverse(self.wake_time).cmp(&Reverse(other.wake_time)) 33 | } 34 | } 35 | 36 | impl PartialOrd for TimerEntry { 37 | fn partial_cmp(&self, other: &Self) -> Option { 38 | Some(self.cmp(other)) 39 | } 40 | } 41 | 42 | impl PartialEq for TimerEntry { 43 | fn eq(&self, _other: &Self) -> bool { 44 | false 45 | } 46 | } 47 | 48 | impl Eq for TimerEntry {} 49 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::sync::{Arc, Mutex}; 4 | use std::task::{Context, Poll, Waker}; 5 | 6 | struct SignalState { 7 | completed: bool, 8 | // TODO: this is ugly, I will need to find a way to wrap an existing waker 9 | // with additional function. Right now moving a Waker inside a closure will 10 | // make that closure FnOnce, while async_task::waker_fn takes Fn. 11 | attacher: Option>, 12 | } 13 | 14 | // This is a minimal future that will complete as soon as waker is called. 15 | // It is diviner specific since it is tailored for the single threaded 16 | // running environment of diviner. 17 | pub(crate) struct Signal { 18 | state: Arc>, 19 | } 20 | 21 | impl Signal { 22 | pub fn new(attacher: F) -> Self 23 | where 24 | F: FnOnce(Waker, Waker) + Send + 'static, 25 | { 26 | Signal { 27 | state: Arc::new(Mutex::new(SignalState { 28 | completed: false, 29 | attacher: Some(Box::new(attacher)), 30 | })), 31 | } 32 | } 33 | } 34 | 35 | impl Future for Signal { 36 | type Output = (); 37 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 38 | if self.state.lock().unwrap().completed { 39 | Poll::Ready(()) 40 | } else { 41 | if let Some(attacher) = self.state.lock().unwrap().attacher.take() { 42 | let state = Arc::clone(&self.state); 43 | attacher( 44 | async_task::waker_fn(move || { 45 | state.lock().unwrap().completed = true; 46 | }), 47 | cx.waker().clone(), 48 | ); 49 | } 50 | Poll::Pending 51 | } 52 | } 53 | } 54 | --------------------------------------------------------------------------------