Day 31: They grow up so fast
Today I made progress implementing the tree drawing algorithm I described yesterday. I haven’t yet gotten to the most interesting bit, where I overwrite the previous output, but I have a passing test for a single parent-child relationship. In doing this, I was tempted by a neat reformulation of the problem which doesn’t quite work out in the details, which I’ll share here.
Testing in Ratatui
First, a brief overview of how testing works in Ratatui. The only automated testing strategy documented on the main documentation site is to use Insta. Insta is a library for snapshot testing, a strategy where you serialize the output of some function, store that in your repository, and later ensure that the output remains the same.
I’m not a huge fan of snapshot testing. Comparing against snapshots is a good way of identifying regressions, but I use testing as a way to help ensure correctness. The first tool for that in Rust is to verify correct behaviour using the type system if possible. Baring that, I prefer randomized testing strategies like Property-based testing or Fuzzing. These bypass the biases of the author (who frequently doesn’t really want to break their own code) and can help discover unknown unknowns.
If you ignore the documentation site and instead read the API docs, they recommend testing widgets by having them render to a buffer whose value you inspect. The API for constructing these buffers is a bit verbose, but it works. Here’s what my test looks like:
#[test]
fn parent_and_child() {
let mut buffer = Buffer::empty(Rect {
x: 0,
y: 0,
width: 8,
height: 2
});
let test_tree = TestTree {
label: "a",
children: vec![
TestTree {
label: "b",
children: Vec::new()
}
],
};
Tree(&test_tree).render(
*buffer.area(),
&mut buffer,
);
let expected_buffer = {
let mut buffer = buffer.clone();
buffer.set_string(0, 0, "a", Style::default());
buffer.set_string(0, 1, "╰── b", Style::default());
buffer
};
assert_eq!(buffer, expected_buffer);
}
Children need room to grow
While implementing the code to make the above test pass, I considered what I thought would be an elegant recursive
definition of tree rendering. Each tree would render its children by simply recursively rendering another Tree
widget with a modified position. Something like this:
fn render(self, area: Rect, mut buf: &mut Buffer)
where
Self: Sized {
self.0.render_node(Rect { height: 1, ..area }, &mut buf);
for (child_index, child) in self.0.children().enumerate() {
buf.set_stringn(area.x, child_y, "╰── ", area.width as usize, Style::default());
Tree(&child).render(Rect { x: area.x + 4, y: area.y + child_index + 1, ..area }, &mut buf)
}
}
This doesn’t account for the sibling junction-rewriting trick mentioned in the last update, but that could come later. I liked this idea because it neatly represented the recursive structure of the tree in the code. Unfortunately it doesn’t quite work.
The above doesn’t work with trees of a depth greater than two, in other words when children
have children of their own. We calculate the y position as area.y + child_index + 1, but that presumes that each child
only adds one row. To illustrate, this means that if we have this tree:
a
├── b
│ ╰── c
╰── d
╰── e
then we would accidentally render it like this:
a
├── b
╰── d── c
╰── e
We could work around this problem by having each tree report back its number of descendants, but that requires going beyond
the API exposed with a simple recursive render() call.
That’s all, folks
With that, I have concluded my December Adventure! This was a lot of fun, and I plan to write a larger retrospective in the new year.
On that note, happy new year all!