Skip to content
Merged
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
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ log = "0.4"
[dev-dependencies]
rstest = "0.18.2"
wireframe_testing = { path = "./wireframe_testing" }
logtest = "^2.0"

[lints.clippy]
pedantic = "warn"
26 changes: 25 additions & 1 deletion docs/rust-testing-with-rstest-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -1237,7 +1237,31 @@ proper initialization. `rstest-log` likely provides attributes or wrappers to
ensure that logging is correctly set up before each `rstest`-generated test case
runs, making it easier to get consistent log output from tests.

### B. `test-with`: Conditional Testing with `rstest`
### B. `logtest`: Verifying Log Output

`logtest` provides a lightweight logger that records emitted log records during
tests. This makes it trivial to assert on log messages without interfering with
other tests. Add it under `[dev-dependencies]` using an explicit version range:

```toml
[dev-dependencies]
logtest = "^2.0"
```

Start a `Logger` before running the code under test:

```rust
use logtest::Logger;

let mut logger = Logger::start();
my_async_fn().await;
assert!(logger.pop().is_some());
```

This crate complements `rstest` nicely when verifying that warnings or errors
are logged under specific conditions.

### C. `test-with`: Conditional Testing with `rstest`

The `test-with` crate allows for conditional execution of tests based on various
runtime conditions, such as the presence of environment variables, the existence
Expand Down
95 changes: 95 additions & 0 deletions tests/push_policies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Tests for push queue policy behaviour.

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

use logtest::Logger;
use rstest::{fixture, rstest};
use tokio::{
runtime::Runtime,
time::{Duration, timeout},
};
use wireframe::push::{PushPolicy, PushPriority, PushQueues};

/// Handle to the global logger with exclusive access.
struct LoggerHandle {
guard: std::sync::MutexGuard<'static, Logger>,
}

impl LoggerHandle {
fn new() -> Self {
static LOGGER: OnceLock<Mutex<Logger>> = OnceLock::new();

let logger = LOGGER.get_or_init(|| Mutex::new(Logger::start()));
let guard = logger
.lock()
.expect("failed to acquire global logger lock; a previous test may still hold it");

Self { guard }
}
}

impl std::ops::Deref for LoggerHandle {
type Target = Logger;

fn deref(&self) -> &Self::Target { &self.guard }
}

impl std::ops::DerefMut for LoggerHandle {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.guard }
}

#[allow(unused_braces)]
#[fixture]
fn logger() -> LoggerHandle { LoggerHandle::new() }
Comment thread
leynos marked this conversation as resolved.

#[allow(unused_braces)]
#[fixture]
fn rt() -> Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build test runtime")
}

#[rstest]
fn drop_if_full_discards_frame(rt: Runtime, mut logger: LoggerHandle) {
rt.block_on(async {
let (mut queues, handle) = PushQueues::bounded(1, 1);
handle.push_high_priority(1u8).await.unwrap();
handle
.try_push(2u8, PushPriority::High, PushPolicy::DropIfFull)
.unwrap();
let (_, val) = queues.recv().await.unwrap();
assert_eq!(val, 1);
assert!(
timeout(Duration::from_millis(20), queues.recv())
.await
.is_err()
);

assert!(logger.pop().is_none());
});
}

#[rstest]
fn warn_and_drop_if_full_logs_warning(rt: Runtime, mut logger: LoggerHandle) {
rt.block_on(async {
let (mut queues, handle) = PushQueues::bounded(1, 1);
handle.push_low_priority(3u8).await.unwrap();
handle
.try_push(4u8, PushPriority::Low, PushPolicy::WarnAndDropIfFull)
.unwrap();
let (_, val) = queues.recv().await.unwrap();
assert_eq!(val, 3);
assert!(
timeout(Duration::from_millis(20), queues.recv())
.await
.is_err()
);

let record = logger.pop().expect("expected warning");
assert_eq!(record.level(), log::Level::Warn);
assert!(record.args().contains("push queue full"));
assert!(logger.pop().is_none());
});
}