use super::*;
use anyhow::{Result, ensure};
use rstest::rstest;
use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, registry::Registry};
fn emit_warning(writer: &StartupWriter, message: &'static str) {
let subscriber = Registry::default()
.with(LevelFilter::WARN)
.with(fmt::layer().with_writer(writer.clone()).with_ansi(false));
tracing::subscriber::with_default(subscriber, || {
tracing::warn!(target: "startup", "{message}");
});
}
#[test]
fn a_startup_warning_is_buffered_rather_than_written() -> Result<()> {
let writer = StartupWriter::buffering();
emit_warning(&writer, "locale fell back");
let buffered = String::from_utf8(writer.buffered())?;
ensure!(
buffered.contains("locale fell back"),
"the event must be recorded while buffering, got {buffered:?}"
);
Ok(())
}
#[test]
fn events_after_discarding_are_not_buffered() -> Result<()> {
let writer = StartupWriter::buffering();
writer.discard();
emit_warning(&writer, "after the decision");
ensure!(
writer.buffered().is_empty(),
"an event after discarding must not be retained"
);
Ok(())
}
#[test]
fn releasing_an_empty_buffer_succeeds() -> Result<()> {
let writer = StartupWriter::buffering();
writer.release_to_stderr()?;
ensure!(
writer.buffered().is_empty(),
"an empty buffer stays empty once released"
);
Ok(())
}
fn write_raw(writer: &StartupWriter, bytes: &[u8]) -> Result<usize> {
use std::io::Write as _;
let mut handle = writer.make_writer();
Ok(handle.write(bytes)?)
}
#[test]
fn a_write_below_the_limit_is_kept_whole() -> Result<()> {
let writer = StartupWriter::buffering();
let payload = vec![b'a'; MAX_BUFFERED_BYTES - 1];
let written = write_raw(&writer, &payload)?;
ensure!(written == payload.len(), "the whole slice must be reported");
let buffered = writer.buffered();
ensure!(buffered == payload, "the bytes must be kept unchanged");
ensure!(
!buffered.ends_with(TRUNCATION_MARKER),
"nothing was dropped, so nothing should be marked"
);
Ok(())
}
#[test]
fn a_write_at_the_limit_is_kept_whole() -> Result<()> {
let writer = StartupWriter::buffering();
let payload = vec![b'a'; MAX_BUFFERED_BYTES];
write_raw(&writer, &payload)?;
let buffered = writer.buffered();
ensure!(
buffered.len() == MAX_BUFFERED_BYTES,
"expected exactly the bound, got {}",
buffered.len()
);
ensure!(
!buffered.ends_with(TRUNCATION_MARKER),
"a write that fits exactly must not be marked as truncated"
);
Ok(())
}
#[test]
fn an_overflowing_write_keeps_the_first_bytes_and_marks_it() -> Result<()> {
let writer = StartupWriter::buffering();
let payload = vec![b'a'; MAX_BUFFERED_BYTES + 4096];
let written = write_raw(&writer, &payload)?;
ensure!(
written == payload.len(),
"dropped overflow must still report the whole slice as written"
);
let buffered = writer.buffered();
ensure!(
buffered.len() <= MAX_BUFFERED_BYTES,
"the buffer must not exceed its bound, got {}",
buffered.len()
);
ensure!(
buffered.starts_with(b"aaaa"),
"the earliest bytes are the ones kept"
);
ensure!(
buffered.ends_with(TRUNCATION_MARKER),
"an overflow must be marked"
);
Ok(())
}
#[test]
fn repeated_overflow_marks_once_and_grows_no_further() -> Result<()> {
let writer = StartupWriter::buffering();
write_raw(&writer, &vec![b'a'; MAX_BUFFERED_BYTES + 1])?;
let after_first = writer.buffered();
for _ in 0..3 {
write_raw(&writer, b"more diagnostics that must be dropped")?;
}
let after_more = writer.buffered();
ensure!(
after_more == after_first,
"writes after truncation must change nothing"
);
let marker = String::from_utf8_lossy(TRUNCATION_MARKER).into_owned();
let rendered = String::from_utf8_lossy(&after_more).into_owned();
ensure!(
rendered.matches(&marker).count() == 1,
"the marker must appear exactly once, found {}",
rendered.matches(&marker).count()
);
Ok(())
}
#[derive(Clone, Copy)]
enum Fill {
OneEvent,
PastTheBound,
}
#[derive(Clone, Copy)]
enum Settlement {
Release,
Discard,
}
#[rstest]
#[case::release_after_one_event(Fill::OneEvent, Settlement::Release)]
#[case::discard_after_one_event(Fill::OneEvent, Settlement::Discard)]
#[case::release_after_truncation(Fill::PastTheBound, Settlement::Release)]
#[case::discard_after_truncation(Fill::PastTheBound, Settlement::Discard)]
fn settling_empties_the_buffer(#[case] fill: Fill, #[case] settlement: Settlement) -> Result<()> {
let writer = StartupWriter::buffering();
match fill {
Fill::OneEvent => emit_warning(&writer, "locale fell back"),
Fill::PastTheBound => {
write_raw(&writer, &vec![b'a'; MAX_BUFFERED_BYTES + 1])?;
}
}
ensure!(
!writer.buffered().is_empty(),
"the buffer must hold something before settlement"
);
match settlement {
Settlement::Release => writer.release_to_stderr()?,
Settlement::Discard => writer.discard(),
}
ensure!(
writer.buffered().is_empty(),
"settling must leave the buffer empty"
);
Ok(())
}