Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions tests/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use rstest::rstest;
use tokio::time::{self, Duration};
use wireframe::push::{PushError, PushPolicy, PushPriority, PushQueues};

/// Frames are delivered to queues matching their push priority.
#[tokio::test]
async fn frames_routed_to_correct_priority_queues() {
let (mut queues, handle) = PushQueues::bounded(1, 1);
Expand All @@ -22,6 +23,7 @@ async fn frames_routed_to_correct_priority_queues() {
assert_eq!(frame2, 1);
}

/// `try_push` honours the selected queue policy when full.
#[tokio::test]
async fn try_push_respects_policy() {
let (mut queues, handle) = PushQueues::bounded(1, 1);
Expand All @@ -37,6 +39,7 @@ async fn try_push_respects_policy() {
assert_eq!(last, 3);
}

/// Push attempts return `Closed` when all queues have been shut down.
#[tokio::test]
async fn push_queues_error_on_closed() {
let (queues, handle) = PushQueues::bounded(1, 1);
Expand All @@ -50,6 +53,7 @@ async fn push_queues_error_on_closed() {
assert!(matches!(res, Err(PushError::Closed)));
}

/// A push beyond the configured rate is blocked.
#[rstest]
#[case::high(PushPriority::High)]
#[case::low(PushPriority::Low)]
Expand Down Expand Up @@ -85,6 +89,7 @@ async fn rate_limiter_blocks_when_exceeded(#[case] priority: PushPriority) {
assert_eq!((first, second), (1, 3));
}

/// Exceeding the rate limit succeeds after the window has passed.
#[tokio::test]
async fn rate_limiter_allows_after_wait() {
time::pause();
Expand All @@ -98,6 +103,7 @@ async fn rate_limiter_allows_after_wait() {
assert_eq!((a, b), (1, 2));
}

/// The limiter counts pushes from all priority queues.
#[tokio::test]
async fn rate_limiter_shared_across_priorities() {
time::pause();
Expand All @@ -118,6 +124,7 @@ async fn rate_limiter_shared_across_priorities() {
assert_eq!(frame2, 2);
}

/// Unlimited queues never block pushes.
#[tokio::test]
async fn unlimited_queues_do_not_block() {
time::pause();
Expand All @@ -131,6 +138,7 @@ async fn unlimited_queues_do_not_block() {
assert_eq!((a, b), (1, 2));
}

/// A burst up to capacity succeeds and further pushes are blocked.
#[tokio::test]
async fn rate_limiter_allows_burst_within_capacity_and_blocks_excess() {
time::pause();
Expand Down
8 changes: 8 additions & 0 deletions tests/push_policies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tokio::{
use wireframe::push::{PushPolicy, PushPriority, PushQueues};
use wireframe_testing::{LoggerHandle, logger};

/// Builds a single-thread [`Runtime`] for async tests.
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
Expand All @@ -23,6 +24,7 @@ fn rt() -> Runtime {
.expect("failed to build test runtime")
}

/// Verifies how queue policies log and drop when the queue is full.
#[rstest]
#[case::drop_if_full(PushPolicy::DropIfFull, false, "push queue full")]
#[case::warn_and_drop(PushPolicy::WarnAndDropIfFull, true, "push queue full")]
Expand Down Expand Up @@ -64,6 +66,7 @@ fn push_policy_behaviour(
});
}

/// Dropped frames are forwarded to the dead letter queue.
#[rstest]
fn dropped_frame_goes_to_dlq(rt: Runtime) {
rt.block_on(async {
Expand All @@ -82,12 +85,15 @@ fn dropped_frame_goes_to_dlq(rt: Runtime) {
});
}

/// Preloads the DLQ to simulate a full queue.
fn setup_dlq_full(tx: &mpsc::Sender<u8>, _rx: &mut Option<mpsc::Receiver<u8>>) {
tx.try_send(99).unwrap();
}

/// Drops the receiver to simulate a closed DLQ channel.
fn setup_dlq_closed(_: &mpsc::Sender<u8>, rx: &mut Option<mpsc::Receiver<u8>>) { drop(rx.take()); }

/// Asserts that one message is queued and the DLQ then reports empty.
fn assert_dlq_full(rx: &mut Option<mpsc::Receiver<u8>>) -> BoxFuture<'_, ()> {
Box::pin(async move {
let receiver = rx.as_mut().expect("receiver missing");
Expand All @@ -96,8 +102,10 @@ fn assert_dlq_full(rx: &mut Option<mpsc::Receiver<u8>>) -> BoxFuture<'_, ()> {
})
}

/// Confirms no receiver is present when the DLQ is closed.
fn assert_dlq_closed(_: &mut Option<mpsc::Receiver<u8>>) -> BoxFuture<'_, ()> { Box::pin(async {}) }

/// Parameterised checks for error logs when DLQ interactions fail.
#[rstest]
#[case::dlq_full(setup_dlq_full, PushPolicy::WarnAndDropIfFull, "DLQ", assert_dlq_full)]
#[case::dlq_closed(setup_dlq_closed, PushPolicy::DropIfFull, "closed", assert_dlq_closed)]
Expand Down
32 changes: 31 additions & 1 deletion wireframe_testing/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
//! This module provides a global, thread-safe logger handle for capturing and
//! inspecting log output during tests. The [`LoggerHandle`] ensures exclusive
//! access to prevent interference between concurrent tests.
//!
//! # Examples
//! ```
//! use wireframe_testing::logger;
//!
//! #[tokio::test]
//! async fn logs_are_collected() {
//! let mut log = logger();
//! log::info!("example");
//! assert!(log.pop().is_some());
//! }
//! ```

use std::sync::{Mutex, MutexGuard, OnceLock};

Expand All @@ -11,13 +23,30 @@ use rstest::fixture;
/// Handle to the global logger with exclusive access.
///
/// This guard ensures tests do not interfere with each other's log capture by
/// serialising access to a [`logtest::Logger`].
/// serialising access to a [`logtest::Logger`]. Acquire it using [`logger`] or
/// [`LoggerHandle::new`].
///
/// # Examples
/// ```
/// use wireframe_testing::logger;
///
/// let mut log = logger();
/// log::warn!("warned");
/// assert!(log.pop().is_some());
/// ```
pub struct LoggerHandle {
Comment on lines +26 to 37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add the missing # Examples macro import or silence the unused-imports lint.

log::warn! expands to a macro call and triggers the unused_imports lint in doctests when log is not imported explicitly. Import log (hidden with #) to satisfy the lint and keep the rendered docs clean.

 /// ```
 /// use wireframe_testing::logger;
+/// # use log::warn;
 ///
 /// let mut log = logger();
-/// log::warn!("warned");
+/// warn!("warned");
 /// assert!(log.pop().is_some());
 /// ```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// serialising access to a [`logtest::Logger`]. Acquire it using [`logger`] or
/// [`LoggerHandle::new`].
///
/// # Examples
/// ```
/// use wireframe_testing::logger;
///
/// let mut log = logger();
/// log::warn!("warned");
/// assert!(log.pop().is_some());
/// ```
pub struct LoggerHandle {
/// serialising access to a [`logtest::Logger`]. Acquire it using [`logger`] or
/// [`LoggerHandle::new`].
///
/// # Examples
///
🤖 Prompt for AI Agents
In wireframe_testing/src/logging.rs around lines 26 to 37, the doctest uses the
macro log::warn! without importing the log crate, which triggers the
unused_imports lint. Fix this by adding a hidden import of the log crate in the
doctest with `# use log::warn;` before using the warn! macro, and replace
`log::warn!` with just `warn!` to keep the documentation clean and lint-free.

guard: MutexGuard<'static, Logger>,
}

impl LoggerHandle {
/// Acquire the global [`Logger`] instance.
///
/// # Examples
/// ```
/// use wireframe_testing::LoggerHandle;
///
/// let _log = LoggerHandle::new();
/// ```
pub fn new() -> Self {
static LOGGER: OnceLock<Mutex<Logger>> = OnceLock::new();

Expand All @@ -38,6 +67,7 @@ impl std::ops::DerefMut for LoggerHandle {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.guard }
}

/// rstest fixture returning a [`LoggerHandle`] for log assertions.
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
Expand Down