forked from near/nearcore
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[TestLoop] (4/n) Add multi-instance testing support to TestLoop frame…
…work. (near#8587) Also includes some refactoring from earlier. DelaySender is now a struct wrapping an `Arc<dyn CanSendWithDelay>`, rather than a trait. This follows the Sender design (for non tests). Code is split into separate files. This also makes LoopEventHandler easier to write without having to specify some complex trait bounds (see below). Two more tests cases are added, one for timed tests (the logic of which is already supported from the previous PR), and one for multi-instance tests added here. Multi-instance tests are simply tests whose Data type is a Vec<InnerData>, and whose Event type is (usize, InnerEvent). The whole framework works the same way (it does not inherently has the notion of multi-instance tests), but we provide some helper methods so that multi-instance tests can reuse single instance test utils. Altogether, we provide the following helpers to transform LoopEventHandlers: * `.widen()`: Converts a `LoopEventHandler<Data, Event>` to one that can handle some superset Data and a superset Event. This allows reuse of the same event handler test util for different test setups that have different kinds of events and components. * `.for_index(usize)`: Converts a `LoopEventHandler<Data, Event>` to `LoopEventHandler<Vec<Data>, (usize, Event)>` to handle a specific instance in a multi-instance test. and the following helpers to transform DelaySender: * `.narrow()`: Converts a `DelaySender<A>` into a `DelaySender<B>` where B is a `Into<A>`. This is used for internal implementation of `.widen()` above. * `.for_index(usize)`: Converts a `DelaySender<(usize, Event)>` into a `DelaySender<Event>` that automatically attaches a specific index. This allows the test loop's `.sender()` to be converted to a single-instance sender to be passed into a component's constructor, so that when that component sends a message, it automatically ends up into the test loop queue with the correct instance index attached.
- Loading branch information
1 parent
099109f
commit 5da45eb
Showing
10 changed files
with
456 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,5 @@ | ||
mod multi_instance_test; | ||
mod sum_numbers; | ||
mod sum_numbers_test; | ||
mod timed_component; | ||
mod timed_component_test; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
use std::time::Duration; | ||
|
||
use derive_enum_from_into::{EnumFrom, EnumTryInto}; | ||
|
||
use crate::{ | ||
examples::sum_numbers_test::ForwardSumRequest, | ||
messaging::{CanSend, IntoSender}, | ||
test_loop::{ | ||
delay_sender::DelaySender, | ||
event_handler::{capture_events, LoopEventHandler}, | ||
TestLoopBuilder, | ||
}, | ||
}; | ||
|
||
use super::sum_numbers::{ReportSumMsg, SumNumbersComponent, SumRequest}; | ||
|
||
#[derive(derive_more::AsMut, derive_more::AsRef)] | ||
struct TestData { | ||
summer: SumNumbersComponent, | ||
sums: Vec<ReportSumMsg>, | ||
} | ||
|
||
#[derive(Debug, EnumTryInto, EnumFrom)] | ||
enum TestEvent { | ||
RemoteRequest(i64), | ||
LocalRequest(SumRequest), | ||
Sum(ReportSumMsg), | ||
} | ||
|
||
/// Let's pretend that when we send a remote request, the number gets sent to | ||
/// every other instance in the setup as a local request. | ||
pub struct ForwardRemoteRequestToOtherInstances { | ||
sender: Option<DelaySender<(usize, TestEvent)>>, | ||
} | ||
|
||
impl ForwardRemoteRequestToOtherInstances { | ||
pub fn new() -> Self { | ||
Self { sender: None } | ||
} | ||
} | ||
|
||
impl LoopEventHandler<Vec<TestData>, (usize, TestEvent)> for ForwardRemoteRequestToOtherInstances { | ||
fn init(&mut self, sender: DelaySender<(usize, TestEvent)>) { | ||
self.sender = Some(sender); | ||
} | ||
|
||
fn handle( | ||
&mut self, | ||
event: (usize, TestEvent), | ||
data: &mut Vec<TestData>, | ||
) -> Result<(), (usize, TestEvent)> { | ||
if let TestEvent::RemoteRequest(number) = event.1 { | ||
for i in 0..data.len() { | ||
if i != event.0 { | ||
self.sender | ||
.as_ref() | ||
.unwrap() | ||
.send((i, TestEvent::LocalRequest(SumRequest::Number(number)))) | ||
} | ||
} | ||
Ok(()) | ||
} else { | ||
Err(event) | ||
} | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_multi_instance() { | ||
let builder = TestLoopBuilder::<(usize, TestEvent)>::new(); | ||
// Build the SumNumberComponents so that it sends messages back to the test loop. | ||
let mut data = vec![]; | ||
for i in 0..5 { | ||
data.push(TestData { | ||
// Multi-instance sender can be converted to a single-instance sender | ||
// so we can pass it into a component's constructor. | ||
summer: SumNumbersComponent::new(builder.sender().for_index(i).into_sender()), | ||
sums: vec![], | ||
}); | ||
} | ||
let sender = builder.sender(); | ||
let mut test = builder.build(data); | ||
test.register_handler(ForwardRemoteRequestToOtherInstances::new()); | ||
for i in 0..5 { | ||
// Single-instance handlers can be reused for multi-instance tests. | ||
test.register_handler(ForwardSumRequest.widen().for_index(i)); | ||
test.register_handler(capture_events::<ReportSumMsg>().widen().for_index(i)); | ||
} | ||
|
||
// Send a RemoteRequest from each instance. | ||
sender.send((0, TestEvent::RemoteRequest(1))); | ||
sender.send((1, TestEvent::RemoteRequest(2))); | ||
sender.send((2, TestEvent::RemoteRequest(3))); | ||
sender.send((3, TestEvent::RemoteRequest(4))); | ||
sender.send((4, TestEvent::RemoteRequest(5))); | ||
|
||
// Then send a GetSum request for each instance; we use a delay so that we can ensure | ||
// these messages arrive later. (In a real test we wouldn't do this - the component would | ||
// automatically emit some events and we would assert on these events. But for this | ||
// contrived test we'll do it manually as a demonstration.) | ||
for i in 0..5 { | ||
sender.send_with_delay( | ||
(i, TestEvent::LocalRequest(SumRequest::GetSum)), | ||
Duration::from_millis(1), | ||
); | ||
} | ||
test.run(Duration::from_millis(2)); | ||
assert_eq!(test.data[0].sums, vec![ReportSumMsg(14)]); | ||
assert_eq!(test.data[1].sums, vec![ReportSumMsg(13)]); | ||
assert_eq!(test.data[2].sums, vec![ReportSumMsg(12)]); | ||
assert_eq!(test.data[3].sums, vec![ReportSumMsg(11)]); | ||
assert_eq!(test.data[4].sums, vec![ReportSumMsg(10)]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
use crate::messaging::Sender; | ||
|
||
pub(crate) struct TimedComponent { | ||
buffered_messages: Vec<String>, | ||
message_sender: Sender<Vec<String>>, | ||
} | ||
|
||
/// Mimics a component that has a specific function that is supposed to be | ||
/// triggered by a timer. | ||
impl TimedComponent { | ||
pub fn new(message_sender: Sender<Vec<String>>) -> Self { | ||
Self { buffered_messages: vec![], message_sender } | ||
} | ||
|
||
pub fn send_message(&mut self, msg: String) { | ||
self.buffered_messages.push(msg); | ||
} | ||
|
||
/// This is supposed to be triggered by a timer so it flushes the | ||
/// messages every tick. | ||
pub fn flush(&mut self) { | ||
if self.buffered_messages.is_empty() { | ||
return; | ||
} | ||
self.message_sender.send(self.buffered_messages.clone()); | ||
self.buffered_messages.clear(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
use std::time::Duration; | ||
|
||
use derive_enum_from_into::{EnumFrom, EnumTryInto}; | ||
|
||
use crate::{ | ||
messaging::IntoSender, | ||
test_loop::event_handler::{capture_events, periodic_interval, LoopEventHandler}, | ||
}; | ||
|
||
use super::timed_component::TimedComponent; | ||
|
||
#[derive(Debug, Clone, PartialEq)] | ||
struct Flush; | ||
|
||
#[derive(Debug, EnumTryInto, EnumFrom)] | ||
enum TestEvent { | ||
SendMessage(String), | ||
Flush(Flush), | ||
MessageSent(Vec<String>), | ||
} | ||
|
||
#[derive(derive_more::AsMut, derive_more::AsRef)] | ||
struct TestData { | ||
component: TimedComponent, | ||
messages_sent: Vec<Vec<String>>, | ||
} | ||
|
||
struct ForwardSendMessage; | ||
|
||
impl LoopEventHandler<TimedComponent, String> for ForwardSendMessage { | ||
fn handle(&mut self, event: String, data: &mut TimedComponent) -> Result<(), String> { | ||
data.send_message(event); | ||
Ok(()) | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_timed_component() { | ||
let builder = crate::test_loop::TestLoopBuilder::<TestEvent>::new(); | ||
let data = TestData { | ||
component: TimedComponent::new(builder.sender().into_sender()), | ||
messages_sent: vec![], | ||
}; | ||
let sender = builder.sender(); | ||
let mut test = builder.build(data); | ||
test.register_handler(ForwardSendMessage.widen()); | ||
test.register_handler( | ||
periodic_interval(Duration::from_millis(100), Flush, |data: &mut TimedComponent| { | ||
data.flush() | ||
}) | ||
.widen(), | ||
); | ||
test.register_handler(capture_events::<Vec<String>>().widen()); | ||
|
||
sender.send_with_delay("Hello".to_string().into(), Duration::from_millis(10)); | ||
sender.send_with_delay("World".to_string().into(), Duration::from_millis(20)); | ||
// The timer fires at 100ms here and flushes "Hello" and "World". | ||
sender.send_with_delay("!".to_string().into(), Duration::from_millis(110)); | ||
// The timer fires again at 200ms here and flushes "!"". | ||
// Further timer events do not send messages. | ||
|
||
test.run(Duration::from_secs(1)); | ||
assert_eq!( | ||
test.data.messages_sent, | ||
vec![vec!["Hello".to_string(), "World".to_string()], vec!["!".to_string()]] | ||
); | ||
} |
Oops, something went wrong.