diff --git a/Cargo.lock b/Cargo.lock index a4de047ff..f88410147 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4819,6 +4819,7 @@ dependencies = [ name = "tor-rtmock" version = "0.8.2" dependencies = [ + "amplify", "async-trait", "educe", "futures", diff --git a/crates/tor-rtmock/Cargo.toml b/crates/tor-rtmock/Cargo.toml index c86bb4a4c..9c2a5d7af 100644 --- a/crates/tor-rtmock/Cargo.toml +++ b/crates/tor-rtmock/Cargo.toml @@ -12,6 +12,7 @@ categories = ["asynchronous"] repository = "https://gitlab.torproject.org/tpo/core/arti.git/" [dependencies] +amplify = { version = "4", default-features = false, features = ["derive"] } async-trait = "0.1.54" educe = "0.4.6" futures = "0.3.14" diff --git a/crates/tor-rtmock/src/lib.rs b/crates/tor-rtmock/src/lib.rs index a85d5946f..4d356422c 100644 --- a/crates/tor-rtmock/src/lib.rs +++ b/crates/tor-rtmock/src/lib.rs @@ -51,7 +51,9 @@ pub mod task; pub mod time; mod net_runtime; +mod runtime; mod sleep_runtime; pub use net_runtime::MockNetRuntime; +pub use runtime::MockRuntime; pub use sleep_runtime::MockSleepRuntime; diff --git a/crates/tor-rtmock/src/runtime.rs b/crates/tor-rtmock/src/runtime.rs new file mode 100644 index 000000000..201ce32e4 --- /dev/null +++ b/crates/tor-rtmock/src/runtime.rs @@ -0,0 +1,129 @@ +//! Completely mock runtime + +use amplify::Getters; + +use crate::util::impl_runtime_prelude::*; + +use crate::net::MockNetProvider; +use crate::task::MockExecutor; +use crate::time::MockSleepProvider; + +/// Completely mock runtime +/// +/// Suitable for test cases that wish to completely control +/// the environment experienced by the code under test. +/// +/// # Restrictions +/// +/// The test case must advance the mock time explicitly as desired. +/// There is not currently any facility for automatically +/// making progress by advancing the mock time by the right amounts +/// for the timeouts set by the futures under test. +// ^ I think such a facility could be provided. `MockSleepProvider` would have to +// provide a method to identify the next interesting time event. +// The waitfor machinery in MockSleepProvider and MockSleepRuntime doesn't seem suitable. +/// +/// Tests that use this runtime *must not* interact with the outside world; +/// everything must go through this runtime (and its pieces). +/// +/// Use of inter-future communication facilities from `futures` +/// or other runtime-agnostic crates is permitted. +#[derive(Debug, Default, Clone, Getters)] +#[getter(prefix = "mock_")] +pub struct MockRuntime { + /// Tasks + task: MockExecutor, + /// Time provider + sleep: MockSleepProvider, + /// Net provider + net: MockNetProvider, +} + +/// Builder for a manually-configured `MockRuntime` +#[derive(Debug, Default, Clone)] +pub struct MockRuntimeBuilder { + /// starting wall clock time + starting_wallclock: Option, +} + +impl_runtime! { + [ ] MockRuntime, + spawn: task, + block: task, + sleep: sleep: MockSleepProvider, + net: net: MockNetProvider, +} + +impl MockRuntime { + /// Create a new `MockRuntime` with default parameters + pub fn new() -> Self { + Self::default() + } + + /// Return a builder, for creating a `MockRuntime` with some parameters manually configured + pub fn builder() -> MockRuntimeBuilder { + Default::default() + } + + /// Run tasks in the current executor until every task except this one is waiting + /// + /// Calls [`MockExecutor::progress_until_stalled()`]. + /// + /// # Restriction - no automatic time advance + /// + /// The mocked time will *not* be automatically advanced. + /// + /// Usually + /// (and especially if the tasks under test are waiting for timeouts or periodic events) + /// you must use + /// [`advance()`](MockRuntime::advance) + /// or + /// [`jump_to()`](MockRuntime::jump_to) + /// to ensure the simulated time progresses as required. + /// + /// # Panics + /// + /// Might malfunction or panic if more than one such call is running at once. + /// + /// (Ie, you must `.await` or drop the returned `Future` + /// before calling this method again.) + pub async fn progress_until_stalled(&self) { + self.task.progress_until_stalled().await; + } + + /// See [`MockSleepProvider::advance()`] + pub async fn advance(&self, dur: Duration) { + self.sleep.advance(dur).await; + } + /// See [`MockSleepProvider::jump_to()`] + pub fn jump_to(&self, new_wallclock: SystemTime) { + self.sleep.jump_to(new_wallclock); + } +} + +impl MockRuntimeBuilder { + /// Set the starting wall clock time + pub fn starting_wallclock(mut self, starting_wallclock: SystemTime) -> Self { + self.starting_wallclock = Some(starting_wallclock); + self + } + + /// Build the runtime + pub fn build(self) -> MockRuntime { + let MockRuntimeBuilder { + scheduling, + starting_wallclock, + } = self; + + let sleep = if let Some(starting_wallclock) = starting_wallclock { + MockSleepProvider::new(starting_wallclock) + } else { + MockSleepProvider::default() + }; + + MockRuntime { + sleep, + ..Default::default() + } + } +}