mirror of
https://github.com/chatmail/core.git
synced 2026-04-20 06:56:29 +03:00
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:
@@ -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() {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user