From 9da02078fe217ed1b2932d69f25f77345cde1af3 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Mon, 1 May 2023 02:33:07 -0700 Subject: [PATCH] test(buffer): add assert_buffer_eq and impl Debug - The implementation of Debug is customized to make it easy to use the output (particularly the content) directly when writing tests (by surrounding it with `Buffer::with_lines(vec![])`). The styles part of the message shows the position of every style change, rather than the style of each cell, which reduces the verbosity of the detail, while still showing everything necessary to debug the buffer. ```rust Buffer { area: Rect { x: 0, y: 0, width: 12, height: 2 }, content: [ "Hello World!", "G'day World!", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, modifier: (empty), x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD, ] } ``` - The assert_buffer_eq! macro shows debug view and diff of the two buffers, which makes it easy to understand exactly where the difference is. - Also adds a unit test for buffer_set_string_multi_width_overwrite which was missing from the buffer tests --- src/buffer.rs | 211 ++++++++++++++++++++++++++++++++++++--- src/widgets/sparkline.rs | 13 ++- tests/widgets_block.rs | 3 + 3 files changed, 206 insertions(+), 21 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 9ec39e4b..ba62e560 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -3,7 +3,10 @@ use crate::{ style::{Color, Modifier, Style}, text::{Span, Spans}, }; -use std::cmp::min; +use std::{ + cmp::min, + fmt::{Debug, Formatter, Result}, +}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; @@ -105,7 +108,7 @@ impl Default for Cell { /// buf.get_mut(5, 0).set_char('x'); /// assert_eq!(buf.get(5, 0).symbol, "x"); /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Clone, PartialEq, Eq, Default)] pub struct Buffer { /// The area represented by this buffer pub area: Rect, @@ -453,6 +456,113 @@ impl Buffer { } } +/// Assert that two buffers are equal by comparing their areas and content. +/// +/// On panic, displays the areas or the content and a diff of the contents. +#[macro_export] +macro_rules! assert_buffer_eq { + ($actual_expr:expr, $expected_expr:expr) => { + match (&$actual_expr, &$expected_expr) { + (actual, expected) => { + if actual.area != expected.area { + panic!( + indoc::indoc!( + " + buffer areas not equal + expected: {:?} + actual: {:?}" + ), + expected, actual + ); + } + let diff = expected.diff(&actual); + if !diff.is_empty() { + let nice_diff = diff + .iter() + .enumerate() + .map(|(i, (x, y, cell))| { + let expected_cell = expected.get(*x, *y); + format!( + "{}: at ({}, {})\n expected: {:?}\n actual: {:?}", + i, x, y, expected_cell, cell + ) + }) + .collect::>() + .join("\n"); + panic!( + indoc::indoc!( + " + buffer contents not equal + expected: {:?} + actual: {:?} + diff: + {}" + ), + expected, actual, nice_diff + ); + } + // shouldn't get here, but this guards against future behavior + // that changes equality but not area or content + assert_eq!(actual, expected, "buffers not equal"); + } + } + }; +} + +impl Debug for Buffer { + /// Writes a debug representation of the buffer to the given formatter. + /// + /// The format is like a pretty printed struct, with the following fields: + /// area: displayed as Rect { x: 1, y: 2, width: 3, height: 4 } + /// content: displayed as a list of strings representing the content of the + /// buffer + /// styles: displayed as a list of + /// { x: 1, y: 2, fg: Color::Red, bg: Color::Blue, modifier: Modifier::BOLD } + /// only showing a value when there is a change in style. + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_fmt(format_args!( + "Buffer {{\n area: {:?},\n content: [\n", + &self.area + ))?; + let mut last_style = None; + let mut styles = vec![]; + for (y, line) in self.content.chunks(self.area.width as usize).enumerate() { + let mut overwritten = vec![]; + let mut skip: usize = 0; + f.write_str(" \"")?; + for (x, c) in line.iter().enumerate() { + if skip == 0 { + f.write_str(&c.symbol)?; + } else { + overwritten.push((x, &c.symbol)) + } + skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1); + let style = (c.fg, c.bg, c.modifier); + if last_style != Some(style) { + last_style = Some(style); + styles.push((x, y, c.fg, c.bg, c.modifier)); + } + } + if !overwritten.is_empty() { + f.write_fmt(format_args!( + "// hidden by multi-width symbols: {:?}", + overwritten + ))?; + } + f.write_str("\",\n")?; + } + f.write_str(" ],\n styles: [\n")?; + for s in styles { + f.write_fmt(format_args!( + " x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n", + s.0, s.1, s.2, s.3, s.4 + ))?; + } + f.write_str(" ]\n}")?; + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -463,6 +573,62 @@ mod tests { cell } + #[test] + fn it_implements_debug() { + let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2)); + buf.set_string(0, 0, "Hello World!", Style::default()); + buf.set_string( + 0, + 1, + "G'day World!", + Style::default() + .fg(Color::Green) + .bg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ); + assert_eq!( + format!("{:?}", buf), + indoc::indoc!( + " + Buffer { + area: Rect { x: 0, y: 0, width: 12, height: 2 }, + content: [ + \"Hello World!\", + \"G'day World!\", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, modifier: (empty), + x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD, + ] + }" + ) + ); + } + + #[test] + fn assert_buffer_eq_does_not_panic_on_equal_buffers() { + let buffer = Buffer::empty(Rect::new(0, 0, 5, 1)); + let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1)); + assert_buffer_eq!(buffer, other_buffer); + } + + #[should_panic] + #[test] + fn assert_buffer_eq_panics_on_unequal_area() { + let buffer = Buffer::empty(Rect::new(0, 0, 5, 1)); + let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1)); + assert_buffer_eq!(buffer, other_buffer); + } + + #[should_panic] + #[test] + fn assert_buffer_eq_panics_on_unequal_style() { + let buffer = Buffer::empty(Rect::new(0, 0, 5, 1)); + let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1)); + other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red)); + assert_buffer_eq!(buffer, other_buffer); + } + #[test] fn it_translates_to_and_from_coordinates() { let rect = Rect::new(200, 100, 50, 80); @@ -504,21 +670,38 @@ mod tests { // Zero-width buffer.set_stringn(0, 0, "aaa", 0, Style::default()); - assert_eq!(buffer, Buffer::with_lines(vec![" "])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "])); buffer.set_string(0, 0, "aaa", Style::default()); - assert_eq!(buffer, Buffer::with_lines(vec!["aaa "])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "])); // Width limit: buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default()); - assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "])); buffer.set_string(0, 0, "12345", Style::default()); - assert_eq!(buffer, Buffer::with_lines(vec!["12345"])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"])); // Width truncation: buffer.set_string(0, 0, "123456", Style::default()); - assert_eq!(buffer, Buffer::with_lines(vec!["12345"])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"])); + + // multi-line + buffer = Buffer::empty(Rect::new(0, 0, 5, 2)); + buffer.set_string(0, 0, "12345", Style::default()); + buffer.set_string(0, 1, "67890", Style::default()); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"])); + } + + #[test] + fn buffer_set_string_multi_width_overwrite() { + let area = Rect::new(0, 0, 5, 1); + let mut buffer = Buffer::empty(area); + + // multi-width overwrite + buffer.set_string(0, 0, "aaaaa", Style::default()); + buffer.set_string(0, 0, "称号", Style::default()); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"])); } #[test] @@ -529,12 +712,12 @@ mod tests { // Leading grapheme with zero width let s = "\u{1}a"; buffer.set_stringn(0, 0, s, 1, Style::default()); - assert_eq!(buffer, Buffer::with_lines(vec!["a"])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"])); // Trailing grapheme with zero with let s = "a\u{1}"; buffer.set_stringn(0, 0, s, 1, Style::default()); - assert_eq!(buffer, Buffer::with_lines(vec!["a"])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"])); } #[test] @@ -542,11 +725,11 @@ mod tests { let area = Rect::new(0, 0, 5, 1); let mut buffer = Buffer::empty(area); buffer.set_string(0, 0, "コン", Style::default()); - assert_eq!(buffer, Buffer::with_lines(vec!["コン "])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "])); // Only 1 space left. buffer.set_string(0, 0, "コンピ", Style::default()); - assert_eq!(buffer, Buffer::with_lines(vec!["コン "])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "])); } #[test] @@ -671,7 +854,7 @@ mod tests { Cell::default().set_symbol("2"), ); one.merge(&two); - assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"])); + assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"])); } #[test] @@ -695,7 +878,7 @@ mod tests { Cell::default().set_symbol("2"), ); one.merge(&two); - assert_eq!( + assert_buffer_eq!( one, Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"]) ); @@ -729,6 +912,6 @@ mod tests { width: 4, height: 4, }; - assert_eq!(one, merged); + assert_buffer_eq!(one, merged); } } diff --git a/src/widgets/sparkline.rs b/src/widgets/sparkline.rs index f972d5a0..36f74b6f 100644 --- a/src/widgets/sparkline.rs +++ b/src/widgets/sparkline.rs @@ -153,9 +153,8 @@ impl<'a> Widget for Sparkline<'a> { #[cfg(test)] mod tests { - use crate::buffer::Cell; - use super::*; + use crate::{assert_buffer_eq, buffer::Cell}; // Helper function to render a sparkline to a buffer with a given width // filled with x symbols to make it easier to assert on the result @@ -172,21 +171,21 @@ mod tests { fn it_does_not_panic_if_max_is_zero() { let widget = Sparkline::default().data(&[0, 0, 0]); let buffer = render(widget, 6); - assert_eq!(buffer, Buffer::with_lines(vec![" xxx"])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"])); } #[test] fn it_does_not_panic_if_max_is_set_to_zero() { let widget = Sparkline::default().data(&[0, 1, 2]).max(0); let buffer = render(widget, 6); - assert_eq!(buffer, Buffer::with_lines(vec![" xxx"])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"])); } #[test] fn it_draws() { let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]); let buffer = render(widget, 12); - assert_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"])); } #[test] @@ -195,7 +194,7 @@ mod tests { .data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]) .direction(RenderDirection::LeftToRight); let buffer = render(widget, 12); - assert_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"])); } #[test] @@ -204,6 +203,6 @@ mod tests { .data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]) .direction(RenderDirection::RightToLeft); let buffer = render(widget, 12); - assert_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "])); + assert_buffer_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "])); } } diff --git a/tests/widgets_block.rs b/tests/widgets_block.rs index 5ea6b066..bf6ca9e1 100644 --- a/tests/widgets_block.rs +++ b/tests/widgets_block.rs @@ -1,4 +1,5 @@ use ratatui::{ + assert_buffer_eq, backend::TestBackend, buffer::Buffer, layout::{Alignment, Rect}, @@ -237,6 +238,8 @@ fn widgets_block_title_alignment() { .unwrap(); terminal.backend().assert_buffer(&expected); + let area = Rect::new(1, 0, 13, 2); + assert_buffer_eq!(Buffer::empty(area), Buffer::empty(area)); }; // title top-left with all borders