use std::fmt;
#[derive(Clone)]
pub struct BoundedBuffer {
inner: String,
limit: usize,
truncated: bool,
}
impl BoundedBuffer {
#[must_use]
pub fn new(limit: usize) -> Self {
Self {
inner: String::new(),
limit,
truncated: false,
}
}
#[must_use]
pub fn limit(&self) -> usize {
self.limit
}
#[must_use]
pub fn data(&self) -> &str {
&self.inner
}
#[must_use]
pub fn into_data(self) -> String {
self.inner
}
#[must_use]
pub fn is_truncated(&self) -> bool {
self.truncated
}
#[must_use]
pub fn take(&mut self) -> Self {
Self {
inner: std::mem::take(&mut self.inner),
truncated: std::mem::take(&mut self.truncated),
limit: self.limit,
}
}
pub fn clear(&mut self) {
self.inner.clear();
self.truncated = false;
}
}
impl fmt::Write for BoundedBuffer {
fn write_str(&mut self, s: &str) -> fmt::Result {
if self.truncated {
return Err(fmt::Error);
}
let available = self.limit - self.inner.len();
if available >= s.len() {
self.inner.push_str(s);
return Ok(());
}
self.inner.push_str(&s[..s.floor_char_boundary(available)]);
self.truncated = true;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::fmt::Write as _;
use proptest::prelude::*;
use rstest::{fixture, rstest};
use super::BoundedBuffer;
const LIMIT: usize = 8;
#[fixture]
fn buffer(#[default(LIMIT)] limit: usize) -> BoundedBuffer {
BoundedBuffer::new(limit)
}
#[rstest]
#[case::empty(&[], "")]
#[case::one_write(&["abc"], "abc")]
#[case::several_writes(&["ab", "cd", "ef"], "abcdef")]
#[case::exactly_the_limit(&["abcdefgh"], "abcdefgh")]
#[case::exactly_the_limit_in_pieces(&["abcd", "efgh"], "abcdefgh")]
fn keeps_everything_that_fits(
mut buffer: BoundedBuffer,
#[case] writes: &[&str],
#[case] expected: &str,
) {
for write in writes {
assert!(buffer.write_str(write).is_ok());
}
assert_eq!(buffer.data(), expected);
assert!(!buffer.is_truncated(), "nothing was dropped, so nothing to report");
}
#[rstest]
#[case::one_oversized_write(&["abcdefghij"], "abcdefgh")]
#[case::overflows_part_way(&["abcdef", "ghij"], "abcdefgh")]
#[case::full_then_more(&["abcdefgh", "i"], "abcdefgh")]
fn reports_truncation(
mut buffer: BoundedBuffer,
#[case] writes: &[&str],
#[case] expected: &str,
) {
for write in writes {
let _ = buffer.write_str(write);
}
assert_eq!(buffer.data(), expected);
assert!(buffer.is_truncated());
assert!(buffer.data().len() <= buffer.limit());
}
#[rstest]
fn a_full_buffer_rejects_further_writes(mut buffer: BoundedBuffer) {
assert!(buffer.write_str("abcdefghij").is_ok(), "the overflowing write itself succeeds");
assert!(buffer.write_str("more").is_err());
assert_eq!(buffer.data(), "abcdefgh");
}
#[rstest]
#[case::multibyte_split(&["ab", "cdef🦀"], "abcdef")]
#[case::multibyte_does_not_fit_at_all(&["abcdefg", "🦀"], "abcdefg")]
#[case::multibyte_fits_exactly(&["abcd", "🦀"], "abcd🦀")]
fn never_splits_a_character(
mut buffer: BoundedBuffer,
#[case] writes: &[&str],
#[case] expected: &str,
) {
for write in writes {
let _ = buffer.write_str(write);
}
assert_eq!(buffer.data(), expected);
}
#[rstest]
#[case::zero(0)]
#[case::one(1)]
fn tiny_limits(#[case] limit: usize) {
let mut buffer = BoundedBuffer::new(limit);
let _ = buffer.write_str("hello");
assert_eq!(buffer.data().len(), limit);
assert!(buffer.is_truncated());
}
#[rstest]
fn take_hands_over_the_contents_and_resets(mut buffer: BoundedBuffer) {
let _ = buffer.write_str("abcdefghij");
let taken = buffer.take();
assert_eq!(taken.data(), "abcdefgh");
assert!(taken.is_truncated());
assert_eq!(buffer.data(), "");
assert!(!buffer.is_truncated());
assert_eq!(buffer.limit(), LIMIT);
assert!(buffer.write_str("xy").is_ok());
assert_eq!(buffer.data(), "xy");
}
#[rstest]
fn clear_resets_the_truncation_flag(mut buffer: BoundedBuffer) {
let _ = buffer.write_str("abcdefghij");
buffer.clear();
assert_eq!(buffer.data(), "");
assert!(!buffer.is_truncated());
assert!(buffer.write_str("xy").is_ok());
}
#[rstest]
fn into_data_returns_what_was_kept(mut buffer: BoundedBuffer) {
let _ = buffer.write_str("abcdefghij");
assert_eq!(buffer.into_data(), "abcdefgh");
}
fn filled(limit: usize, writes: &[String]) -> BoundedBuffer {
let mut buffer = BoundedBuffer::new(limit);
for write in writes {
let _ = buffer.write_str(write);
}
buffer
}
prop_compose! {
fn limit_and_writes()(
limit in 0usize..24,
writes in prop::collection::vec("(?s).{0,8}", 0..8),
) -> (usize, Vec<String>) {
(limit, writes)
}
}
proptest! {
#[test]
fn never_exceeds_the_limit((limit, writes) in limit_and_writes()) {
let buffer = filled(limit, &writes);
prop_assert!(buffer.data().len() <= limit);
prop_assert_eq!(buffer.limit(), limit);
}
#[test]
fn keeps_the_longest_prefix_that_fits((limit, writes) in limit_and_writes()) {
let all = writes.concat();
let buffer = filled(limit, &writes);
prop_assert_eq!(buffer.data(), &all[..all.floor_char_boundary(limit)]);
}
#[test]
fn chunking_does_not_matter((limit, writes) in limit_and_writes()) {
let chunked = filled(limit, &writes);
let at_once = filled(limit, &[writes.concat()]);
prop_assert_eq!(chunked.data(), at_once.data());
prop_assert_eq!(chunked.is_truncated(), at_once.is_truncated());
}
#[test]
fn reports_truncation_exactly_when_data_was_dropped((limit, writes) in limit_and_writes()) {
let all = writes.concat();
let buffer = filled(limit, &writes);
prop_assert_eq!(buffer.is_truncated(), all.len() > limit);
}
#[test]
fn writes_fail_only_after_truncation((limit, writes) in limit_and_writes()) {
let mut buffer = BoundedBuffer::new(limit);
for write in &writes {
let was_truncated = buffer.is_truncated();
prop_assert_eq!(buffer.write_str(write).is_err(), was_truncated);
}
}
#[test]
fn take_moves_the_state_out((limit, writes) in limit_and_writes()) {
let mut buffer = filled(limit, &writes);
let before = buffer.data().to_string();
let was_truncated = buffer.is_truncated();
let taken = buffer.take();
prop_assert_eq!(taken.data(), before);
prop_assert_eq!(taken.is_truncated(), was_truncated);
prop_assert_eq!(taken.limit(), limit);
prop_assert_eq!(buffer.data(), "");
prop_assert!(!buffer.is_truncated());
prop_assert_eq!(buffer.limit(), limit);
}
#[test]
fn clear_is_as_good_as_a_new_buffer((limit, writes) in limit_and_writes()) {
let mut buffer = filled(limit, &writes);
buffer.clear();
prop_assert_eq!(buffer.data(), "");
prop_assert!(!buffer.is_truncated());
prop_assert_eq!(buffer.limit(), limit);
for write in &writes {
let _ = buffer.write_str(write);
}
let fresh = filled(limit, &writes);
prop_assert_eq!(buffer.data(), fresh.data());
}
#[test]
fn into_data_matches_data((limit, writes) in limit_and_writes()) {
let buffer = filled(limit, &writes);
let seen = buffer.data().to_string();
prop_assert_eq!(buffer.into_data(), seen);
}
}
}