#![forbid(unsafe_code)]
use ftui_harness::hdd::{Decomposable, hdd_minimize};
#[derive(Clone, Debug, PartialEq)]
struct WidgetNode {
id: u32,
kind: &'static str,
children: Vec<WidgetNode>,
}
impl WidgetNode {
fn leaf(id: u32, kind: &'static str) -> Self {
Self {
id,
kind,
children: vec![],
}
}
fn container(id: u32, kind: &'static str, children: Vec<WidgetNode>) -> Self {
Self { id, kind, children }
}
fn node_count(&self) -> usize {
1 + self.children.iter().map(|c| c.node_count()).sum::<usize>()
}
fn contains_id(&self, target: u32) -> bool {
if self.id == target {
return true;
}
self.children.iter().any(|c| c.contains_id(target))
}
fn contains_kind(&self, target: &str) -> bool {
if self.kind == target {
return true;
}
self.children.iter().any(|c| c.contains_kind(target))
}
}
impl Decomposable for WidgetNode {
fn children(&self) -> Vec<Self> {
self.children.clone()
}
fn remove_child(&mut self, idx: usize) {
self.children.remove(idx);
}
fn replace_children(&mut self, new_children: Vec<Self>) {
self.children = new_children;
}
}
fn build_balanced_tree(
counter: &mut u32,
depth: usize,
branching: usize,
kind: &'static str,
) -> WidgetNode {
let id = *counter;
*counter += 1;
if depth == 0 {
return WidgetNode::leaf(id, kind);
}
let children = (0..branching)
.map(|_| build_balanced_tree(counter, depth - 1, branching, kind))
.collect();
WidgetNode::container(id, kind, children)
}
fn build_tree_with_bug(total_target: usize, bug_depth: usize) -> (WidgetNode, u32) {
let mut counter = 0u32;
let mut root = build_balanced_tree(&mut counter, 4, 3, "panel");
let bug_id = counter;
let bug_node = WidgetNode::leaf(bug_id, "buggy");
fn insert_at_depth(node: &mut WidgetNode, bug: WidgetNode, depth: usize) -> bool {
if depth == 0 {
node.children.push(bug);
return true;
}
if let Some(child) = node.children.first_mut() {
return insert_at_depth(child, bug, depth - 1);
}
false
}
insert_at_depth(&mut root, bug_node, bug_depth);
let _ = total_target; (root, bug_id)
}
#[test]
fn single_node_tree_cannot_be_reduced() {
let tree = WidgetNode::leaf(0, "root");
let result = hdd_minimize(tree.clone(), |_| true);
assert_eq!(result, tree);
assert_eq!(result.node_count(), 1);
}
#[test]
fn large_tree_reduces_to_minimal() {
let (tree, bug_id) = build_tree_with_bug(100, 4);
assert!(
tree.node_count() > 100,
"tree should have >100 nodes, got {}",
tree.node_count()
);
assert!(tree.contains_id(bug_id), "bug node must exist in tree");
let result = hdd_minimize(tree, |t| t.contains_id(bug_id));
assert!(
result.node_count() < 10,
"reduced tree should have <10 nodes, got {}",
result.node_count()
);
assert!(
result.contains_id(bug_id),
"bug node must survive reduction"
);
}
#[test]
fn reduction_preserves_predicate_at_every_step() {
use std::cell::RefCell;
let tree = WidgetNode::container(
0,
"root",
vec![
WidgetNode::container(
1,
"panel",
vec![
WidgetNode::leaf(2, "text"),
WidgetNode::leaf(3, "buggy"),
WidgetNode::leaf(4, "text"),
],
),
WidgetNode::container(
5,
"panel",
vec![WidgetNode::leaf(6, "text"), WidgetNode::leaf(7, "text")],
),
WidgetNode::leaf(8, "text"),
],
);
let predicate_calls = RefCell::new(Vec::new());
let result = hdd_minimize(tree.clone(), |t| {
let has_bug = t.contains_kind("buggy");
predicate_calls.borrow_mut().push((t.clone(), has_bug));
has_bug
});
let calls = predicate_calls.borrow();
assert!(!calls.is_empty(), "predicate must be called at least once");
assert!(calls[0].1, "predicate must hold on original input");
assert!(result.contains_kind("buggy"));
assert!(result.node_count() < tree.node_count());
}
#[test]
fn output_is_1_minimal() {
let tree = WidgetNode::container(
0,
"root",
vec![
WidgetNode::leaf(1, "text"),
WidgetNode::container(
2,
"panel",
vec![
WidgetNode::leaf(3, "text"),
WidgetNode::leaf(4, "buggy"),
WidgetNode::leaf(5, "text"),
],
),
WidgetNode::leaf(6, "text"),
WidgetNode::container(
7,
"sidebar",
vec![WidgetNode::leaf(8, "text"), WidgetNode::leaf(9, "text")],
),
],
);
let predicate = |t: &WidgetNode| t.contains_kind("buggy");
let result = hdd_minimize(tree, predicate);
assert!(predicate(&result), "result must satisfy predicate");
fn check_1_minimal(
node: &WidgetNode,
root: &WidgetNode,
predicate: &dyn Fn(&WidgetNode) -> bool,
) {
for i in 0..node.children.len() {
let mut modified_node = node.clone();
modified_node.children.remove(i);
let modified_root = replace_subtree(root, node.id, &modified_node);
assert!(
!predicate(&modified_root),
"tree is not 1-minimal: removing child {} from node {} \
still satisfies predicate (result has {} nodes)",
i,
node.id,
modified_root.node_count()
);
}
for child in &node.children {
check_1_minimal(child, root, predicate);
}
}
check_1_minimal(&result, &result, &predicate);
}
fn replace_subtree(root: &WidgetNode, target_id: u32, replacement: &WidgetNode) -> WidgetNode {
if root.id == target_id {
return replacement.clone();
}
WidgetNode {
id: root.id,
kind: root.kind,
children: root
.children
.iter()
.map(|c| replace_subtree(c, target_id, replacement))
.collect(),
}
}
#[test]
fn multiple_required_nodes_preserved() {
let tree = WidgetNode::container(
0,
"root",
vec![
WidgetNode::leaf(1, "text"),
WidgetNode::leaf(2, "bug_a"),
WidgetNode::leaf(3, "text"),
WidgetNode::leaf(4, "bug_b"),
WidgetNode::leaf(5, "text"),
WidgetNode::leaf(6, "text"),
],
);
let result = hdd_minimize(tree, |t| {
t.contains_kind("bug_a") && t.contains_kind("bug_b")
});
assert!(result.contains_kind("bug_a"));
assert!(result.contains_kind("bug_b"));
assert_eq!(
result.children.len(),
2,
"should keep exactly the two required nodes"
);
}
#[test]
fn deep_path_reduces_to_chain() {
let tree = WidgetNode::container(
0,
"root",
vec![
WidgetNode::leaf(10, "noise"),
WidgetNode::container(
1,
"a",
vec![
WidgetNode::leaf(11, "noise"),
WidgetNode::container(
2,
"b",
vec![
WidgetNode::container(
3,
"c",
vec![
WidgetNode::container(
4,
"d",
vec![
WidgetNode::leaf(5, "buggy"),
WidgetNode::leaf(12, "noise"),
],
),
WidgetNode::leaf(13, "noise"),
],
),
WidgetNode::leaf(14, "noise"),
],
),
WidgetNode::leaf(15, "noise"),
],
),
WidgetNode::leaf(16, "noise"),
],
);
let original_count = tree.node_count();
assert_eq!(original_count, 13);
let result = hdd_minimize(tree, |t| t.contains_kind("buggy"));
assert!(result.contains_kind("buggy"));
assert!(
result.node_count() <= 7,
"expected <=7 nodes in chain, got {}",
result.node_count()
);
}