pub mod data;
pub mod futures;
pub mod pending_events_sender;
pub mod sender;
use data::TestLoopData;
use futures::{TestLoopAsyncComputationSpawner, TestLoopFutureSpawner};
use near_time::{Clock, Duration, FakeClock};
use parking_lot::Mutex;
use pending_events_sender::{CallbackEvent, PendingEventsSender, RawPendingEventsSender};
use serde::Serialize;
use std::collections::{BinaryHeap, HashSet};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::panicking;
use time::ext::InstantExt;
pub struct TestLoopV2 {
pub data: TestLoopData,
raw_pending_events_sender: RawPendingEventsSender,
events: BinaryHeap<EventInHeap>,
pending_events: Arc<Mutex<InFlightEvents>>,
next_event_index: usize,
current_time: Duration,
clock: near_time::FakeClock,
shutting_down: Arc<AtomicBool>,
every_event_callback: Option<Box<dyn FnMut(&TestLoopData)>>,
denylisted_identifiers: HashSet<String>,
}
struct EventInHeap {
event: CallbackEvent,
due: Duration,
id: usize,
}
impl PartialEq for EventInHeap {
fn eq(&self, other: &Self) -> bool {
self.due == other.due && self.id == other.id
}
}
impl Eq for EventInHeap {}
impl PartialOrd for EventInHeap {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for EventInHeap {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.due, self.id).cmp(&(other.due, other.id)).reverse()
}
}
struct InFlightEvents {
events: Vec<CallbackEvent>,
event_loop_thread_id: std::thread::ThreadId,
}
impl InFlightEvents {
fn new() -> Self {
Self { events: Vec::new(), event_loop_thread_id: std::thread::current().id() }
}
fn add(&mut self, event: CallbackEvent) {
if std::thread::current().id() != self.event_loop_thread_id {
panic!(
"Event was sent from the wrong thread. TestLoop tests should be single-threaded. \
Check if there's any code that spawns computation on another thread such as \
rayon::spawn, and convert it to AsyncComputationSpawner or FutureSpawner. \
Event: {}",
event.description
);
}
self.events.push(event);
}
}
#[derive(Serialize)]
struct EventStartLogOutput {
current_index: usize,
total_events: usize,
identifier: String,
current_event: String,
current_time_ms: u64,
event_ignored: bool,
}
#[derive(Serialize)]
struct EventEndLogOutput {
total_events: usize,
}
impl TestLoopV2 {
pub fn new() -> Self {
let pending_events = Arc::new(Mutex::new(InFlightEvents::new()));
let pending_events_clone = pending_events.clone();
let raw_pending_events_sender = RawPendingEventsSender::new(move |callback_event| {
let mut pending_events = pending_events_clone.lock();
pending_events.add(callback_event);
});
let shutting_down = Arc::new(AtomicBool::new(false));
tracing::info!(target: "test_loop", "TEST_LOOP_INIT");
Self {
data: TestLoopData::new(raw_pending_events_sender.clone(), shutting_down.clone()),
events: BinaryHeap::new(),
pending_events,
raw_pending_events_sender,
next_event_index: 0,
current_time: Duration::ZERO,
clock: FakeClock::default(),
shutting_down,
every_event_callback: None,
denylisted_identifiers: HashSet::new(),
}
}
pub fn future_spawner(&self, identifier: &str) -> TestLoopFutureSpawner {
self.raw_pending_events_sender.for_identifier(identifier)
}
pub fn async_computation_spawner(
&self,
identifier: &str,
artificial_delay: impl Fn(&str) -> Duration + Send + Sync + 'static,
) -> TestLoopAsyncComputationSpawner {
TestLoopAsyncComputationSpawner::new(
self.raw_pending_events_sender.for_identifier(identifier),
artificial_delay,
)
}
pub fn send_adhoc_event(
&self,
description: String,
callback: impl FnOnce(&mut TestLoopData) + Send + 'static,
) {
self.send_adhoc_event_with_delay(description, Duration::ZERO, callback)
}
pub fn send_adhoc_event_with_delay(
&self,
description: String,
delay: Duration,
callback: impl FnOnce(&mut TestLoopData) + Send + 'static,
) {
self.raw_pending_events_sender.for_identifier("Adhoc").send_with_delay(
description,
Box::new(callback),
delay,
);
}
pub fn remove_events_with_identifier(&mut self, identifier: &str) {
self.denylisted_identifiers.insert(identifier.to_string());
}
pub fn clock(&self) -> Clock {
self.clock.clock()
}
pub fn set_every_event_callback(&mut self, callback: impl FnMut(&TestLoopData) + 'static) {
self.every_event_callback = Some(Box::new(callback));
}
fn queue_received_events(&mut self) {
for event in self.pending_events.lock().events.drain(..) {
self.events.push(EventInHeap {
due: self.current_time + event.delay,
id: self.next_event_index,
event,
});
self.next_event_index += 1;
}
}
fn advance_till_next_event(
&mut self,
decider: &mut impl FnMut(Option<Duration>, &mut TestLoopData) -> AdvanceDecision,
) -> Option<EventInHeap> {
loop {
self.queue_received_events();
let next_timestamp = {
let next_event_timestamp = self.events.peek().map(|event| event.due);
let next_future_waiter_timestamp = self
.clock
.first_waiter()
.map(|time| time.signed_duration_since(self.clock.now() - self.current_time));
next_event_timestamp
.map(|t1| next_future_waiter_timestamp.map(|t2| t2.min(t1)).unwrap_or(t1))
.or(next_future_waiter_timestamp)
};
if next_timestamp == Some(self.current_time) {
let event = self.events.pop().expect("Programming error in TestLoop");
assert_eq!(event.due, self.current_time);
return Some(event);
}
let decision = decider(next_timestamp, &mut self.data);
match decision {
AdvanceDecision::AdvanceToNextEvent => {
let next_timestamp = next_timestamp.unwrap();
self.clock.advance(next_timestamp - self.current_time);
self.current_time = next_timestamp;
continue;
}
AdvanceDecision::AdvanceToAndStop(target) => {
self.clock.advance(target - self.current_time);
self.current_time = target;
return None;
}
AdvanceDecision::Stop => {
return None;
}
}
}
}
fn process_event(&mut self, event: EventInHeap) {
if self.shutting_down.load(Ordering::Relaxed) {
return;
}
let event_ignored = self.denylisted_identifiers.contains(&event.event.identifier);
let start_json = serde_json::to_string(&EventStartLogOutput {
current_index: event.id,
total_events: self.next_event_index,
identifier: event.event.identifier.clone(),
current_event: event.event.description,
current_time_ms: event.due.whole_milliseconds() as u64,
event_ignored,
})
.unwrap();
tracing::info!(target: "test_loop", "TEST_LOOP_EVENT_START {}", start_json);
assert_eq!(self.current_time, event.due);
if !event_ignored {
if let Some(callback) = &mut self.every_event_callback {
callback(&self.data);
}
let callback = event.event.callback;
callback(&mut self.data);
}
self.queue_received_events();
let end_json =
serde_json::to_string(&EventEndLogOutput { total_events: self.next_event_index })
.unwrap();
tracing::info!(target: "test_loop", "TEST_LOOP_EVENT_END {}", end_json);
}
pub fn run_for(&mut self, duration: Duration) {
let deadline = self.current_time + duration;
while let Some(event) = self.advance_till_next_event(&mut |next_time, _| {
if let Some(next_time) = next_time {
if next_time <= deadline {
return AdvanceDecision::AdvanceToNextEvent;
}
}
AdvanceDecision::AdvanceToAndStop(deadline)
}) {
self.process_event(event);
}
}
pub fn run_until(
&mut self,
mut condition: impl FnMut(&mut TestLoopData) -> bool,
maximum_duration: Duration,
) {
let deadline = self.current_time + maximum_duration;
let mut decider = move |next_time, data: &mut TestLoopData| {
if condition(data) {
return AdvanceDecision::Stop;
}
if let Some(next_time) = next_time {
if next_time <= deadline {
return AdvanceDecision::AdvanceToNextEvent;
}
}
panic!("run_until did not fulfill the condition within the given deadline");
};
while let Some(event) = self.advance_till_next_event(&mut decider) {
self.process_event(event);
}
}
pub fn shutdown_and_drain_remaining_events(mut self, maximum_duration: Duration) {
self.shutting_down.store(true, Ordering::Relaxed);
self.run_for(maximum_duration);
}
pub fn run_instant(&mut self) {
self.run_for(Duration::ZERO);
}
}
impl Drop for TestLoopV2 {
fn drop(&mut self) {
self.queue_received_events();
if let Some(event) = self.events.pop() {
self.events.clear();
if !panicking() {
panic!(
"Event scheduled at {} is not handled at the end of the test: {}.
Consider calling `test.shutdown_and_drain_remaining_events(...)`.",
event.due, event.event.description
);
}
}
tracing::info!(target: "test_loop", "TEST_LOOP_SHUTDOWN");
}
}
enum AdvanceDecision {
AdvanceToNextEvent,
AdvanceToAndStop(Duration),
Stop,
}
#[cfg(test)]
mod tests {
use crate::futures::FutureSpawnerExt;
use crate::test_loop::TestLoopV2;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use time::Duration;
#[test]
fn test_futures() {
let mut test_loop = TestLoopV2::new();
let clock = test_loop.clock();
let start_time = clock.now();
let finished = Arc::new(AtomicUsize::new(0));
let clock1 = clock.clone();
let finished1 = finished.clone();
test_loop.future_spawner("adhoc future spawner").spawn("test1", async move {
assert_eq!(clock1.now(), start_time);
clock1.sleep(Duration::seconds(10)).await;
assert_eq!(clock1.now(), start_time + Duration::seconds(10));
clock1.sleep(Duration::seconds(5)).await;
assert_eq!(clock1.now(), start_time + Duration::seconds(15));
finished1.fetch_add(1, Ordering::Relaxed);
});
test_loop.run_for(Duration::seconds(2));
let clock2 = clock;
let finished2 = finished.clone();
test_loop.future_spawner("adhoc future spawner").spawn("test2", async move {
assert_eq!(clock2.now(), start_time + Duration::seconds(2));
clock2.sleep(Duration::seconds(3)).await;
assert_eq!(clock2.now(), start_time + Duration::seconds(5));
clock2.sleep(Duration::seconds(20)).await;
assert_eq!(clock2.now(), start_time + Duration::seconds(25));
finished2.fetch_add(1, Ordering::Relaxed);
});
test_loop.run_for(Duration::seconds(30));
assert_eq!(finished.load(Ordering::Relaxed), 2);
}
}