feat: replace event channel with broadcast channel

This makes `EventTracker` receive events immediately
instead of being moved from event emitter to event tracker
by a task spawned from `TestContext::new_internal`.

This makes `EventTracker.clear_events` reliable
as it is guaranteed to remove all events emitted
by the time it is called rather than only events
that have been moved already.
This commit is contained in:
link2xt
2024-04-19 02:19:21 +00:00
parent 72d5a387fb
commit 34f4ec02f6
11 changed files with 194 additions and 139 deletions

View File

@@ -19,7 +19,6 @@ use pretty_assertions::assert_eq;
use rand::Rng;
use tempfile::{tempdir, TempDir};
use tokio::runtime::Handle;
use tokio::sync::RwLock;
use tokio::{fs, task};
use crate::chat::{
@@ -34,7 +33,7 @@ use crate::constants::{Blocked, Chattype};
use crate::contact::{Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::events::{Event, EventType, Events};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{self, DcKey, KeyPairUse};
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
@@ -57,20 +56,19 @@ static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
/// occurred rather than grouped by context like would happen when you use separate
/// [`TestContext`]s without managing your own [`LogSink`].
pub struct TestContextManager {
log_tx: Sender<LogEvent>,
_log_sink: LogSink,
log_sink: LogSink,
}
impl TestContextManager {
pub fn new() -> Self {
let (log_tx, _log_sink) = LogSink::create();
Self { log_tx, _log_sink }
let log_sink = LogSink::new();
Self { log_sink }
}
pub async fn alice(&mut self) -> TestContext {
TestContext::builder()
.configure_alice()
.with_log_sink(self.log_tx.clone())
.with_log_sink(self.log_sink.clone())
.build()
.await
}
@@ -78,7 +76,7 @@ impl TestContextManager {
pub async fn bob(&mut self) -> TestContext {
TestContext::builder()
.configure_bob()
.with_log_sink(self.log_tx.clone())
.with_log_sink(self.log_sink.clone())
.build()
.await
}
@@ -86,7 +84,7 @@ impl TestContextManager {
pub async fn fiona(&mut self) -> TestContext {
TestContext::builder()
.configure_fiona()
.with_log_sink(self.log_tx.clone())
.with_log_sink(self.log_sink.clone())
.build()
.await
}
@@ -94,7 +92,7 @@ impl TestContextManager {
/// Creates a new unconfigured test account.
pub async fn unconfigured(&mut self) -> TestContext {
TestContext::builder()
.with_log_sink(self.log_tx.clone())
.with_log_sink(self.log_sink.clone())
.build()
.await
}
@@ -103,7 +101,8 @@ impl TestContextManager {
///
/// ========== `msg` goes here ==========
pub fn section(&self, msg: &str) {
self.log_tx
self.log_sink
.sender
.try_send(LogEvent::Section(msg.to_string()))
.expect(
"The events channel should be unbounded and not closed, so try_send() shouldn't fail",
@@ -194,7 +193,7 @@ impl TestContextManager {
#[derive(Debug, Clone, Default)]
pub struct TestContextBuilder {
key_pair: Option<KeyPair>,
log_sink: Option<Sender<LogEvent>>,
log_sink: LogSink,
}
impl TestContextBuilder {
@@ -234,8 +233,8 @@ impl TestContextBuilder {
/// using a single [`LogSink`] for both contexts. This shows the log messages in
/// sequence as they occurred rather than all messages from each context in a single
/// block.
pub fn with_log_sink(mut self, sink: Sender<LogEvent>) -> Self {
self.log_sink = Some(sink);
pub fn with_log_sink(mut self, sink: LogSink) -> Self {
self.log_sink = sink;
self
}
@@ -243,7 +242,7 @@ impl TestContextBuilder {
pub async fn build(self) -> TestContext {
let name = self.key_pair.as_ref().map(|key| key.addr.local.clone());
let test_context = TestContext::new_internal(name, self.log_sink).await;
let test_context = TestContext::new_internal(name, Some(self.log_sink.clone())).await;
if let Some(key_pair) = self.key_pair {
test_context
@@ -266,18 +265,16 @@ pub struct TestContext {
pub dir: TempDir,
pub evtracker: EventTracker,
/// Channels which should receive events from this context.
event_senders: Arc<RwLock<Vec<Sender<Event>>>>,
/// Reference to implicit [`LogSink`] so it is dropped together with the context.
///
/// Only used if no explicit `log_sender` is passed into [`TestContext::new_internal`]
/// (which is assumed to be the sending end of a [`LogSink`]).
///
/// This is a convenience in case only a single [`TestContext`] is used to avoid dealing
/// with [`LogSink`]. Never read, thus "dead code", since the only purpose is to
/// with [`LogSink`]. Never read, since the only purpose is to
/// control when Drop is invoked.
#[allow(dead_code)]
log_sink: Option<LogSink>,
_log_sink: Option<LogSink>,
}
impl TestContext {
@@ -337,7 +334,7 @@ impl TestContext {
/// `log_sender` is assumed to be the sender for a [`LogSink`]. If not supplied a new
/// [`LogSink`] will be created so that events are logged to this test when the
/// [`TestContext`] is dropped.
async fn new_internal(name: Option<String>, log_sender: Option<Sender<LogEvent>>) -> Self {
async fn new_internal(name: Option<String>, log_sink: Option<LogSink>) -> Self {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = rand::thread_rng().gen();
@@ -345,35 +342,23 @@ impl TestContext {
let mut context_names = CONTEXT_NAMES.write().unwrap();
context_names.insert(id, name);
}
let ctx = Context::new(&dbfile, id, Events::new(), StockStrings::new())
let events = Events::new();
let evtracker_receiver = events.get_emitter();
let ctx = Context::new(&dbfile, id, events, StockStrings::new())
.await
.expect("failed to create context");
let events = ctx.get_event_emitter();
let (log_sender, log_sink) = match log_sender {
Some(sender) => (sender, None),
None => {
let (sender, sink) = LogSink::create();
(sender, Some(sink))
}
let _log_sink = if let Some(log_sink) = log_sink {
// Subscribe existing LogSink and don't store reference to it.
log_sink.subscribe(ctx.get_event_emitter());
None
} else {
// Create new LogSink and store it inside the `TestContext`.
let log_sink = LogSink::new();
log_sink.subscribe(ctx.get_event_emitter());
Some(log_sink)
};
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
let event_senders = Arc::new(RwLock::new(vec![evtracker_sender]));
let senders = Arc::clone(&event_senders);
task::spawn(async move {
while let Some(event) = events.recv().await {
for sender in senders.read().await.iter() {
// Don't block because someone wanted to use a oneshot receiver, use
// an unbounded channel if you want all events.
sender.try_send(event.clone()).ok();
}
log_sender.try_send(LogEvent::Event(event.clone())).ok();
}
});
ctx.set_config(Config::SkipStartMessages, Some("1"))
.await
.unwrap();
@@ -383,8 +368,7 @@ impl TestContext {
ctx,
dir,
evtracker: EventTracker(evtracker_receiver),
event_senders,
log_sink,
_log_sink,
}
}
@@ -407,14 +391,6 @@ impl TestContext {
context_names.get(id).unwrap_or(&id.to_string()).to_string()
}
/// Adds a new [`Event`]s sender.
///
/// Once added, all events emitted by this context will be sent to this channel. This
/// is useful if you need to wait for events or make assertions on them.
pub async fn add_event_sender(&self, sink: Sender<Event>) {
self.event_senders.write().await.push(sink)
}
/// Configure as a given email address.
///
/// The context will be configured but the key will not be pre-generated so if a key is
@@ -849,22 +825,62 @@ pub enum LogEvent {
/// This sink achieves this by printing the events, in the order received, at the time it is
/// dropped. Thus to use you must only make sure this sink is dropped in the test itself.
///
/// To use this create an instance using [`LogSink::create`] and then use the
/// [`TestContextBuilder::with_log_sink`].
#[derive(Debug)]
pub struct LogSink {
events: Receiver<LogEvent>,
}
/// To use this create an instance using [`LogSink::new`] and then use the
/// [`TestContextBuilder::with_log_sink`] or use [`TestContextManager`].
#[derive(Debug, Clone, Default)]
pub struct LogSink(Arc<InnerLogSink>);
impl LogSink {
/// Creates a new [`LogSink`] and returns the attached event sink.
pub fn create() -> (Sender<LogEvent>, Self) {
let (tx, rx) = channel::unbounded();
(tx, Self { events: rx })
pub fn new() -> Self {
Default::default()
}
}
impl Drop for LogSink {
impl Deref for LogSink {
type Target = InnerLogSink;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug)]
pub struct InnerLogSink {
events: Receiver<LogEvent>,
/// Sender side of the log receiver.
///
/// It is cloned when log sink is subscribed
/// to new event emitter
/// and can be used directly from the test to
/// add "sections" to the log.
sender: Sender<LogEvent>,
}
impl Default for InnerLogSink {
fn default() -> Self {
let (tx, rx) = channel::unbounded();
Self {
events: rx,
sender: tx,
}
}
}
impl InnerLogSink {
/// Subscribes this log sink to event emitter.
pub fn subscribe(&self, event_emitter: EventEmitter) {
let sender = self.sender.clone();
task::spawn(async move {
while let Some(event) = event_emitter.recv().await {
sender.try_send(LogEvent::Event(event.clone())).ok();
}
});
}
}
impl Drop for InnerLogSink {
fn drop(&mut self) {
while let Ok(event) = self.events.try_recv() {
print_logevent(&event);
@@ -975,10 +991,10 @@ pub fn fiona_keypair() -> KeyPair {
/// be attached to a single [`TestContext`] and therefore the context is already known as
/// you will be accessing it as [`TestContext::evtracker`].
#[derive(Debug)]
pub struct EventTracker(Receiver<Event>);
pub struct EventTracker(EventEmitter);
impl Deref for EventTracker {
type Target = Receiver<Event>;
type Target = EventEmitter;
fn deref(&self) -> &Self::Target {
&self.0
@@ -1025,15 +1041,8 @@ impl EventTracker {
}
/// Clears event queue.
///
/// This spends 1 second instead of using `try_recv`
/// to avoid accidentally leaving an event that
/// was emitted right before calling `clear_events()`.
///
/// Avoid using this function if you can
/// by waiting for specific events you expect to receive.
pub async fn clear_events(&self) {
while let Ok(_ev) = tokio::time::timeout(Duration::from_secs(1), self.recv()).await {}
pub fn clear_events(&self) {
while let Ok(_ev) = self.try_recv() {}
}
}