use std::fmt;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, PartialEq)]
pub enum DeltaEntry<K, V> {
Insert { key: K, value: V, logical_time: u64 },
Delete { key: K, logical_time: u64 },
}
impl<K, V> DeltaEntry<K, V> {
pub fn key(&self) -> &K {
match self {
DeltaEntry::Insert { key, .. } => key,
DeltaEntry::Delete { key, .. } => key,
}
}
pub fn logical_time(&self) -> u64 {
match self {
DeltaEntry::Insert { logical_time, .. } => *logical_time,
DeltaEntry::Delete { logical_time, .. } => *logical_time,
}
}
pub fn weight(&self) -> i8 {
match self {
DeltaEntry::Insert { .. } => 1,
DeltaEntry::Delete { .. } => -1,
}
}
pub fn is_insert(&self) -> bool {
matches!(self, DeltaEntry::Insert { .. })
}
}
#[derive(Debug, Clone)]
pub struct DeltaBatch<K, V> {
pub epoch: u64,
pub entries: Vec<DeltaEntry<K, V>>,
}
impl<K: Eq + Hash, V> DeltaBatch<K, V> {
pub fn new(epoch: u64) -> Self {
Self {
epoch,
entries: Vec::new(),
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn insert(&mut self, key: K, value: V, logical_time: u64) {
self.entries.push(DeltaEntry::Insert {
key,
value,
logical_time,
});
}
pub fn delete(&mut self, key: K, logical_time: u64) {
self.entries.push(DeltaEntry::Delete { key, logical_time });
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ViewId(pub u32);
impl fmt::Display for ViewId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "V{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ViewDomain {
Style,
Layout,
Render,
FilteredList,
Custom,
}
impl ViewDomain {
pub fn as_str(&self) -> &'static str {
match self {
ViewDomain::Style => "style",
ViewDomain::Layout => "layout",
ViewDomain::Render => "render",
ViewDomain::FilteredList => "filtered_list",
ViewDomain::Custom => "custom",
}
}
}
#[derive(Debug, Clone)]
pub struct PropagationResult {
pub view_id: ViewId,
pub domain: ViewDomain,
pub input_delta_size: usize,
pub output_delta_size: usize,
pub fell_back_to_full: bool,
pub materialized_size: usize,
pub duration_us: u64,
}
#[derive(Debug, Clone)]
pub struct EpochEvidence {
pub epoch: u64,
pub views_processed: usize,
pub views_recomputed: usize,
pub total_delta_size: usize,
pub total_materialized_size: usize,
pub duration_us: u64,
pub per_view: Vec<PropagationResult>,
}
impl EpochEvidence {
pub fn delta_ratio(&self) -> f64 {
if self.total_materialized_size == 0 {
0.0
} else {
self.total_delta_size as f64 / self.total_materialized_size as f64
}
}
pub fn to_jsonl(&self) -> String {
format!(
"{{\"type\":\"ivm_epoch\",\"epoch\":{},\"views_processed\":{},\"views_recomputed\":{},\"total_delta_size\":{},\"total_materialized_size\":{},\"delta_ratio\":{:.4},\"duration_us\":{}}}",
self.epoch,
self.views_processed,
self.views_recomputed,
self.total_delta_size,
self.total_materialized_size,
self.delta_ratio(),
self.duration_us,
)
}
}
#[derive(Debug, Clone)]
pub struct FallbackPolicy {
pub ratio_threshold: f64,
pub min_delta_for_fallback: usize,
}
impl Default for FallbackPolicy {
fn default() -> Self {
Self {
ratio_threshold: 0.5,
min_delta_for_fallback: 10,
}
}
}
impl FallbackPolicy {
pub fn should_fallback(&self, delta_size: usize, materialized_size: usize) -> bool {
if delta_size < self.min_delta_for_fallback {
return false;
}
if materialized_size == 0 {
return true; }
(delta_size as f64 / materialized_size as f64) > self.ratio_threshold
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DagEdge {
pub from: ViewId,
pub to: ViewId,
}
#[derive(Debug, Clone)]
pub struct DagTopology {
pub views: Vec<ViewDescriptor>,
pub edges: Vec<DagEdge>,
pub topo_order: Vec<ViewId>,
}
#[derive(Debug, Clone)]
pub struct ViewDescriptor {
pub id: ViewId,
pub label: String,
pub domain: ViewDomain,
pub fallback_policy: FallbackPolicy,
}
impl DagTopology {
pub fn new() -> Self {
Self {
views: Vec::new(),
edges: Vec::new(),
topo_order: Vec::new(),
}
}
pub fn add_view(&mut self, label: impl Into<String>, domain: ViewDomain) -> ViewId {
let id = ViewId(self.views.len() as u32);
self.views.push(ViewDescriptor {
id,
label: label.into(),
domain,
fallback_policy: FallbackPolicy::default(),
});
id
}
pub fn add_edge(&mut self, from: ViewId, to: ViewId) {
assert!(
!self.can_reach(to, from),
"IVM DAG cycle detected: {} -> {} would create a cycle",
from,
to
);
self.edges.push(DagEdge { from, to });
}
fn can_reach(&self, from: ViewId, to: ViewId) -> bool {
let mut visited = vec![false; self.views.len()];
let mut stack = vec![from];
while let Some(current) = stack.pop() {
if current == to {
return true;
}
let idx = current.0 as usize;
if idx >= visited.len() || visited[idx] {
continue;
}
visited[idx] = true;
for edge in &self.edges {
if edge.from == current && !visited[edge.to.0 as usize] {
stack.push(edge.to);
}
}
}
false
}
pub fn compute_topo_order(&mut self) {
let n = self.views.len();
let mut in_degree = vec![0usize; n];
let mut adj: Vec<Vec<ViewId>> = vec![Vec::new(); n];
for edge in &self.edges {
in_degree[edge.to.0 as usize] += 1;
adj[edge.from.0 as usize].push(edge.to);
}
let mut queue: Vec<ViewId> = (0..n)
.filter(|&i| in_degree[i] == 0)
.map(|i| ViewId(i as u32))
.collect();
queue.sort();
let mut order = Vec::with_capacity(n);
while let Some(v) = queue.pop() {
order.push(v);
let mut neighbors = adj[v.0 as usize].clone();
neighbors.sort();
for next in neighbors {
in_degree[next.0 as usize] -= 1;
if in_degree[next.0 as usize] == 0 {
let pos = queue.partition_point(|&x| x > next);
queue.insert(pos, next);
}
}
}
assert_eq!(
order.len(),
n,
"IVM DAG has a cycle: only {} of {} views in topo order",
order.len(),
n
);
self.topo_order = order;
}
pub fn downstream(&self, view_id: ViewId) -> Vec<ViewId> {
self.edges
.iter()
.filter(|e| e.from == view_id)
.map(|e| e.to)
.collect()
}
pub fn upstream(&self, view_id: ViewId) -> Vec<ViewId> {
self.edges
.iter()
.filter(|e| e.to == view_id)
.map(|e| e.from)
.collect()
}
pub fn view_count(&self) -> usize {
self.views.len()
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
}
impl Default for DagTopology {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct StyleKey(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct LayoutKey(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CellKey {
pub row: u16,
pub col: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedStyleValue {
pub style_hash: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayoutValue {
pub rects_hash: u64,
pub rect_count: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellValue {
pub cell_hash: u64,
}
#[derive(Debug, Clone)]
pub struct IvmConfig {
pub force_full: bool,
pub default_fallback: FallbackPolicy,
pub emit_evidence: bool,
}
impl Default for IvmConfig {
fn default() -> Self {
Self {
force_full: false,
default_fallback: FallbackPolicy::default(),
emit_evidence: true,
}
}
}
impl IvmConfig {
pub fn from_env() -> Self {
let force = std::env::var("FRANKENTUI_FULL_RECOMPUTE")
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
.unwrap_or(false);
Self {
force_full: force,
..Default::default()
}
}
}
pub fn fx_hash<T: Hash>(value: &T) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
value.hash(&mut h);
h.finish()
}
pub trait IncrementalView<K: Clone + Eq + Hash, V: Clone + PartialEq> {
fn apply_delta(&mut self, batch: &DeltaBatch<K, V>) -> DeltaBatch<K, V>;
fn full_recompute(&self) -> Vec<(K, V)>;
fn materialized_size(&self) -> usize;
fn domain(&self) -> ViewDomain;
fn label(&self) -> &str;
}
pub struct StyleResolutionView {
label: String,
base_hash: u64,
overrides: std::collections::HashMap<StyleKey, u64>,
resolved: std::collections::HashMap<StyleKey, ResolvedStyleValue>,
}
impl StyleResolutionView {
pub fn new(label: impl Into<String>, base_hash: u64) -> Self {
Self {
label: label.into(),
base_hash,
overrides: std::collections::HashMap::new(),
resolved: std::collections::HashMap::new(),
}
}
pub fn set_base(&mut self, base_hash: u64) {
self.base_hash = base_hash;
}
pub fn base_hash(&self) -> u64 {
self.base_hash
}
fn resolve(&self, key: &StyleKey) -> ResolvedStyleValue {
let override_hash = self.overrides.get(key).copied().unwrap_or(0);
ResolvedStyleValue {
style_hash: self.base_hash ^ override_hash,
}
}
}
impl IncrementalView<StyleKey, ResolvedStyleValue> for StyleResolutionView {
fn apply_delta(
&mut self,
batch: &DeltaBatch<StyleKey, ResolvedStyleValue>,
) -> DeltaBatch<StyleKey, ResolvedStyleValue> {
let mut output = DeltaBatch::new(batch.epoch);
let mut time = 0u64;
for entry in &batch.entries {
match entry {
DeltaEntry::Insert {
key,
value,
logical_time: _,
} => {
self.overrides.insert(*key, value.style_hash);
let new_resolved = self.resolve(key);
let old = self.resolved.insert(*key, new_resolved);
if old.as_ref() != Some(&new_resolved) {
output.insert(*key, new_resolved, time);
time += 1;
}
}
DeltaEntry::Delete {
key,
logical_time: _,
} => {
self.overrides.remove(key);
if self.resolved.remove(key).is_some() {
output.delete(*key, time);
time += 1;
}
}
}
}
output
}
fn full_recompute(&self) -> Vec<(StyleKey, ResolvedStyleValue)> {
self.overrides
.keys()
.map(|key| (*key, self.resolve(key)))
.collect()
}
fn materialized_size(&self) -> usize {
self.resolved.len()
}
fn domain(&self) -> ViewDomain {
ViewDomain::Style
}
fn label(&self) -> &str {
&self.label
}
}
type FilterPredicate<K, V> = dyn Fn(&K, &V) -> bool;
pub struct FilteredListView<K: Clone + Eq + Hash, V: Clone + PartialEq> {
label: String,
filter: Box<FilterPredicate<K, V>>,
all_items: std::collections::HashMap<K, V>,
visible: std::collections::HashMap<K, V>,
}
impl<K: Clone + Eq + Hash, V: Clone + PartialEq> FilteredListView<K, V> {
pub fn new(label: impl Into<String>, filter: impl Fn(&K, &V) -> bool + 'static) -> Self {
Self {
label: label.into(),
filter: Box::new(filter),
all_items: std::collections::HashMap::new(),
visible: std::collections::HashMap::new(),
}
}
pub fn total_count(&self) -> usize {
self.all_items.len()
}
pub fn visible_count(&self) -> usize {
self.visible.len()
}
}
impl<K: Clone + Eq + Hash, V: Clone + PartialEq> IncrementalView<K, V> for FilteredListView<K, V> {
fn apply_delta(&mut self, batch: &DeltaBatch<K, V>) -> DeltaBatch<K, V> {
let mut output = DeltaBatch::new(batch.epoch);
let mut time = 0u64;
for entry in &batch.entries {
match entry {
DeltaEntry::Insert {
key,
value,
logical_time: _,
} => {
let was_visible = self.visible.contains_key(key);
self.all_items.insert(key.clone(), value.clone());
let now_visible = (self.filter)(key, value);
if now_visible {
let old = self.visible.insert(key.clone(), value.clone());
if !was_visible || old.as_ref() != Some(value) {
output.insert(key.clone(), value.clone(), time);
time += 1;
}
} else if was_visible {
self.visible.remove(key);
output.delete(key.clone(), time);
time += 1;
}
}
DeltaEntry::Delete {
key,
logical_time: _,
} => {
self.all_items.remove(key);
if self.visible.remove(key).is_some() {
output.delete(key.clone(), time);
time += 1;
}
}
}
}
output
}
fn full_recompute(&self) -> Vec<(K, V)> {
self.all_items
.iter()
.filter(|(k, v)| (self.filter)(k, v))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
fn materialized_size(&self) -> usize {
self.visible.len()
}
fn domain(&self) -> ViewDomain {
ViewDomain::FilteredList
}
fn label(&self) -> &str {
&self.label
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delta_entry_insert_fields() {
let entry: DeltaEntry<u32, String> = DeltaEntry::Insert {
key: 42,
value: "hello".into(),
logical_time: 1,
};
assert_eq!(*entry.key(), 42);
assert_eq!(entry.logical_time(), 1);
assert_eq!(entry.weight(), 1);
assert!(entry.is_insert());
}
#[test]
fn delta_entry_delete_fields() {
let entry: DeltaEntry<u32, String> = DeltaEntry::Delete {
key: 42,
logical_time: 2,
};
assert_eq!(*entry.key(), 42);
assert_eq!(entry.logical_time(), 2);
assert_eq!(entry.weight(), -1);
assert!(!entry.is_insert());
}
#[test]
fn batch_operations() {
let mut batch = DeltaBatch::new(1);
assert!(batch.is_empty());
assert_eq!(batch.len(), 0);
assert_eq!(batch.epoch, 1);
batch.insert(1u32, "a".to_string(), 1);
batch.insert(2, "b".to_string(), 2);
batch.delete(1, 3);
assert_eq!(batch.len(), 3);
assert!(!batch.is_empty());
}
#[test]
fn fallback_below_min() {
let policy = FallbackPolicy {
ratio_threshold: 0.5,
min_delta_for_fallback: 10,
};
assert!(!policy.should_fallback(5, 100));
}
#[test]
fn fallback_above_threshold() {
let policy = FallbackPolicy {
ratio_threshold: 0.5,
min_delta_for_fallback: 10,
};
assert!(policy.should_fallback(60, 100));
}
#[test]
fn fallback_below_threshold() {
let policy = FallbackPolicy {
ratio_threshold: 0.5,
min_delta_for_fallback: 10,
};
assert!(!policy.should_fallback(20, 100));
}
#[test]
fn fallback_empty_view() {
let policy = FallbackPolicy::default();
assert!(policy.should_fallback(10, 0));
}
#[test]
fn empty_dag() {
let dag = DagTopology::new();
assert_eq!(dag.view_count(), 0);
assert_eq!(dag.edge_count(), 0);
}
#[test]
fn add_views_and_edges() {
let mut dag = DagTopology::new();
let style = dag.add_view("style", ViewDomain::Style);
let layout = dag.add_view("layout", ViewDomain::Layout);
let render = dag.add_view("render", ViewDomain::Render);
dag.add_edge(style, layout);
dag.add_edge(layout, render);
assert_eq!(dag.view_count(), 3);
assert_eq!(dag.edge_count(), 2);
}
#[test]
fn topological_order_linear() {
let mut dag = DagTopology::new();
let a = dag.add_view("style", ViewDomain::Style);
let b = dag.add_view("layout", ViewDomain::Layout);
let c = dag.add_view("render", ViewDomain::Render);
dag.add_edge(a, b);
dag.add_edge(b, c);
dag.compute_topo_order();
assert_eq!(dag.topo_order, vec![a, b, c]);
}
#[test]
fn topological_order_diamond() {
let mut dag = DagTopology::new();
let a = dag.add_view("source", ViewDomain::Style);
let b = dag.add_view("branch_b", ViewDomain::Layout);
let c = dag.add_view("branch_c", ViewDomain::Layout);
let d = dag.add_view("sink", ViewDomain::Render);
dag.add_edge(a, b);
dag.add_edge(a, c);
dag.add_edge(b, d);
dag.add_edge(c, d);
dag.compute_topo_order();
assert_eq!(dag.topo_order[0], a);
assert_eq!(dag.topo_order[3], d);
let middle: Vec<ViewId> = dag.topo_order[1..3].to_vec();
assert!(middle.contains(&b));
assert!(middle.contains(&c));
}
#[test]
#[should_panic(expected = "cycle")]
fn cycle_detection() {
let mut dag = DagTopology::new();
let a = dag.add_view("a", ViewDomain::Style);
let b = dag.add_view("b", ViewDomain::Layout);
dag.add_edge(a, b);
dag.add_edge(b, a); }
#[test]
fn downstream_upstream() {
let mut dag = DagTopology::new();
let a = dag.add_view("a", ViewDomain::Style);
let b = dag.add_view("b", ViewDomain::Layout);
let c = dag.add_view("c", ViewDomain::Render);
dag.add_edge(a, b);
dag.add_edge(a, c);
let down = dag.downstream(a);
assert_eq!(down.len(), 2);
assert!(down.contains(&b));
assert!(down.contains(&c));
let up = dag.upstream(b);
assert_eq!(up, vec![a]);
}
#[test]
fn epoch_evidence_delta_ratio() {
let ev = EpochEvidence {
epoch: 1,
views_processed: 3,
views_recomputed: 0,
total_delta_size: 10,
total_materialized_size: 200,
duration_us: 42,
per_view: vec![],
};
assert!((ev.delta_ratio() - 0.05).abs() < 0.001);
}
#[test]
fn epoch_evidence_jsonl() {
let ev = EpochEvidence {
epoch: 5,
views_processed: 3,
views_recomputed: 1,
total_delta_size: 25,
total_materialized_size: 500,
duration_us: 100,
per_view: vec![],
};
let jsonl = ev.to_jsonl();
assert!(jsonl.contains("\"type\":\"ivm_epoch\""));
assert!(jsonl.contains("\"epoch\":5"));
assert!(jsonl.contains("\"views_recomputed\":1"));
assert!(jsonl.contains("\"delta_ratio\":0.0500"));
}
#[test]
fn epoch_evidence_empty_materialized() {
let ev = EpochEvidence {
epoch: 1,
views_processed: 0,
views_recomputed: 0,
total_delta_size: 0,
total_materialized_size: 0,
duration_us: 0,
per_view: vec![],
};
assert!((ev.delta_ratio() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn cell_key_ordering() {
let a = CellKey { row: 0, col: 5 };
let b = CellKey { row: 0, col: 10 };
let c = CellKey { row: 1, col: 0 };
assert!(a < b);
assert!(b < c);
}
#[test]
fn style_key_hash() {
let a = StyleKey(42);
let b = StyleKey(42);
assert_eq!(fx_hash(&a), fx_hash(&b));
}
#[test]
fn config_defaults() {
let config = IvmConfig::default();
assert!(!config.force_full);
assert!(config.emit_evidence);
assert!((config.default_fallback.ratio_threshold - 0.5).abs() < f64::EPSILON);
}
#[test]
fn config_from_env_default() {
let config = IvmConfig::from_env();
assert_eq!(config.default_fallback.min_delta_for_fallback, 10);
}
#[test]
fn view_domain_labels() {
assert_eq!(ViewDomain::Style.as_str(), "style");
assert_eq!(ViewDomain::Layout.as_str(), "layout");
assert_eq!(ViewDomain::Render.as_str(), "render");
assert_eq!(ViewDomain::FilteredList.as_str(), "filtered_list");
assert_eq!(ViewDomain::Custom.as_str(), "custom");
}
#[test]
fn three_stage_pipeline_topology() {
let mut dag = DagTopology::new();
let style = dag.add_view("StyleView", ViewDomain::Style);
let layout = dag.add_view("LayoutView", ViewDomain::Layout);
let render = dag.add_view("RenderView", ViewDomain::Render);
dag.add_edge(style, layout);
dag.add_edge(layout, render);
dag.compute_topo_order();
assert_eq!(dag.topo_order, vec![style, layout, render]);
assert_eq!(dag.downstream(style), vec![layout]);
assert_eq!(dag.downstream(layout), vec![render]);
assert!(dag.downstream(render).is_empty());
assert!(dag.upstream(style).is_empty());
assert_eq!(dag.upstream(layout), vec![style]);
assert_eq!(dag.upstream(render), vec![layout]);
}
#[test]
fn multi_source_pipeline() {
let mut dag = DagTopology::new();
let theme_style = dag.add_view("ThemeStyle", ViewDomain::Style);
let content = dag.add_view("Content", ViewDomain::Custom);
let layout = dag.add_view("Layout", ViewDomain::Layout);
let render = dag.add_view("Render", ViewDomain::Render);
dag.add_edge(theme_style, layout);
dag.add_edge(content, layout);
dag.add_edge(layout, render);
dag.compute_topo_order();
let layout_pos = dag.topo_order.iter().position(|&v| v == layout).unwrap();
let render_pos = dag.topo_order.iter().position(|&v| v == render).unwrap();
let theme_pos = dag
.topo_order
.iter()
.position(|&v| v == theme_style)
.unwrap();
let content_pos = dag.topo_order.iter().position(|&v| v == content).unwrap();
assert!(theme_pos < layout_pos);
assert!(content_pos < layout_pos);
assert!(layout_pos < render_pos);
}
#[test]
fn filtered_list_side_branch() {
let mut dag = DagTopology::new();
let style = dag.add_view("Style", ViewDomain::Style);
let content = dag.add_view("Content", ViewDomain::Custom);
let filtered = dag.add_view("FilteredList", ViewDomain::FilteredList);
let layout = dag.add_view("Layout", ViewDomain::Layout);
let render = dag.add_view("Render", ViewDomain::Render);
dag.add_edge(style, layout);
dag.add_edge(content, filtered);
dag.add_edge(filtered, layout);
dag.add_edge(layout, render);
dag.compute_topo_order();
let filtered_pos = dag.topo_order.iter().position(|&v| v == filtered).unwrap();
let layout_pos = dag.topo_order.iter().position(|&v| v == layout).unwrap();
assert!(filtered_pos < layout_pos);
}
#[test]
fn propagation_result_construction() {
let result = PropagationResult {
view_id: ViewId(0),
domain: ViewDomain::Style,
input_delta_size: 5,
output_delta_size: 3,
fell_back_to_full: false,
materialized_size: 100,
duration_us: 15,
};
assert!(!result.fell_back_to_full);
assert_eq!(result.output_delta_size, 3);
}
#[test]
fn propagation_result_fallback() {
let result = PropagationResult {
view_id: ViewId(1),
domain: ViewDomain::Layout,
input_delta_size: 80,
output_delta_size: 100,
fell_back_to_full: true,
materialized_size: 100,
duration_us: 200,
};
assert!(result.fell_back_to_full);
}
#[test]
fn style_view_apply_insert() {
let mut view = StyleResolutionView::new("test_style", 0xABCD);
let mut batch = DeltaBatch::new(1);
batch.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x1234 }, 0);
let output = view.apply_delta(&batch);
assert_eq!(output.len(), 1);
assert!(output.entries[0].is_insert());
assert_eq!(view.materialized_size(), 1);
}
#[test]
fn style_view_deduplicates_unchanged() {
let mut view = StyleResolutionView::new("test", 0x0);
let mut batch1 = DeltaBatch::new(1);
batch1.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x42 }, 0);
view.apply_delta(&batch1);
let mut batch2 = DeltaBatch::new(2);
batch2.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x42 }, 0);
let output = view.apply_delta(&batch2);
assert!(output.is_empty(), "same style should produce no delta");
}
#[test]
fn style_view_delete() {
let mut view = StyleResolutionView::new("test", 0x0);
let mut batch1 = DeltaBatch::new(1);
batch1.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x42 }, 0);
view.apply_delta(&batch1);
assert_eq!(view.materialized_size(), 1);
let mut batch2 = DeltaBatch::new(2);
batch2.delete(StyleKey(0), 0);
let output = view.apply_delta(&batch2);
assert_eq!(output.len(), 1);
assert!(!output.entries[0].is_insert()); assert_eq!(view.materialized_size(), 0);
}
#[test]
fn style_view_full_recompute_matches_incremental() {
let mut view = StyleResolutionView::new("test", 0xFF00);
let mut batch = DeltaBatch::new(1);
batch.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x00FF }, 0);
batch.insert(StyleKey(1), ResolvedStyleValue { style_hash: 0x0F0F }, 1);
view.apply_delta(&batch);
let full = view.full_recompute();
assert_eq!(full.len(), 2);
for (key, value) in &full {
assert_eq!(value, view.resolved.get(key).unwrap());
}
}
#[test]
fn style_view_base_change_affects_all() {
let mut view = StyleResolutionView::new("test", 0x0);
let mut batch = DeltaBatch::new(1);
batch.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x42 }, 0);
batch.insert(StyleKey(1), ResolvedStyleValue { style_hash: 0x99 }, 1);
view.apply_delta(&batch);
view.set_base(0xFFFF);
let full = view.full_recompute();
for (_key, value) in &full {
assert_ne!(value.style_hash, 0);
}
}
#[test]
fn style_view_domain_and_label() {
let view = StyleResolutionView::new("ThemeResolver", 0);
assert_eq!(view.domain(), ViewDomain::Style);
assert_eq!(view.label(), "ThemeResolver");
}
#[test]
fn filtered_view_insert_visible() {
let mut view = FilteredListView::new("evens", |_k: &u32, v: &i32| *v % 2 == 0);
let mut batch = DeltaBatch::new(1);
batch.insert(1u32, 4i32, 0); batch.insert(2u32, 3i32, 1);
let output = view.apply_delta(&batch);
assert_eq!(output.len(), 1); assert_eq!(view.visible_count(), 1);
assert_eq!(view.total_count(), 2);
}
#[test]
fn filtered_view_insert_then_filter_out() {
let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v > 10);
let mut batch1 = DeltaBatch::new(1);
batch1.insert(1u32, 20i32, 0); view.apply_delta(&batch1);
assert_eq!(view.visible_count(), 1);
let mut batch2 = DeltaBatch::new(2);
batch2.insert(1u32, 5i32, 0); let output = view.apply_delta(&batch2);
assert_eq!(output.len(), 1);
assert!(!output.entries[0].is_insert()); assert_eq!(view.visible_count(), 0);
}
#[test]
fn filtered_view_delete() {
let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v > 0);
let mut batch1 = DeltaBatch::new(1);
batch1.insert(1u32, 5i32, 0);
view.apply_delta(&batch1);
assert_eq!(view.visible_count(), 1);
let mut batch2 = DeltaBatch::new(2);
batch2.delete(1u32, 0);
let output = view.apply_delta(&batch2);
assert_eq!(output.len(), 1);
assert!(!output.entries[0].is_insert()); assert_eq!(view.visible_count(), 0);
assert_eq!(view.total_count(), 0);
}
#[test]
fn filtered_view_full_recompute() {
let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v % 2 == 0);
let mut batch = DeltaBatch::new(1);
batch.insert(1u32, 2i32, 0);
batch.insert(2u32, 3i32, 1);
batch.insert(3u32, 4i32, 2);
batch.insert(4u32, 5i32, 3);
view.apply_delta(&batch);
let full = view.full_recompute();
assert_eq!(full.len(), 2); let keys: Vec<u32> = full.iter().map(|(k, _)| *k).collect();
assert!(keys.contains(&1));
assert!(keys.contains(&3));
}
#[test]
fn filtered_view_deduplicates_unchanged() {
let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v > 0);
let mut batch1 = DeltaBatch::new(1);
batch1.insert(1u32, 5i32, 0);
view.apply_delta(&batch1);
let mut batch2 = DeltaBatch::new(2);
batch2.insert(1u32, 5i32, 0);
let output = view.apply_delta(&batch2);
assert!(output.is_empty(), "same value should produce no delta");
}
#[test]
fn filtered_view_domain_and_label() {
let view: FilteredListView<u32, i32> =
FilteredListView::new("EvenFilter", |_k: &u32, v: &i32| *v % 2 == 0);
assert_eq!(view.domain(), ViewDomain::FilteredList);
assert_eq!(view.label(), "EvenFilter");
}
#[test]
fn filtered_view_correctness_invariant() {
let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v > 0);
let mut batch1 = DeltaBatch::new(1);
batch1.insert(1u32, 5i32, 0);
batch1.insert(2u32, -3i32, 1);
batch1.insert(3u32, 10i32, 2);
view.apply_delta(&batch1);
let mut batch2 = DeltaBatch::new(2);
batch2.delete(1u32, 0);
batch2.insert(4u32, 7i32, 1);
batch2.insert(2u32, 8i32, 2); view.apply_delta(&batch2);
let full = view.full_recompute();
let mut full_map: std::collections::HashMap<u32, i32> = full.into_iter().collect();
assert_eq!(full_map.len(), view.visible_count());
for (k, v) in &view.visible {
assert_eq!(full_map.remove(k).as_ref(), Some(v));
}
assert!(full_map.is_empty());
}
#[test]
fn filtered_view_delete_nonexistent_is_noop() {
let mut view: FilteredListView<u32, i32> =
FilteredListView::new("test", |_k: &u32, _v: &i32| true);
let mut batch = DeltaBatch::new(1);
batch.delete(999u32, 0);
let output = view.apply_delta(&batch);
assert!(output.is_empty());
}
#[test]
fn trait_object_dispatch() {
let view: Box<dyn IncrementalView<StyleKey, ResolvedStyleValue>> =
Box::new(StyleResolutionView::new("dynamic", 0x42));
assert_eq!(view.materialized_size(), 0);
assert_eq!(view.domain(), ViewDomain::Style);
assert_eq!(view.label(), "dynamic");
}
}