mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-25 06:00:34 +00:00
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
This commit is contained in:
parent
cf1a759fa5
commit
9da02078fe
3 changed files with 206 additions and 21 deletions
211
src/buffer.rs
211
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::<Vec<String>>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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█▇▆▅▄▃▂▁ "]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue