Make Model's Debug exhaustive and the Debug fingerprint canonical across instances (improvise-djcn)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
@@ -920,13 +920,47 @@ pub(crate) mod tests {
|
||||
fn a_model_command_lands_where_its_keypress_lands() {
|
||||
/// Two categories on different axes: a one-category fixture cannot tell
|
||||
/// a transpose from a cycle (`doctrine(check_the_checker)`).
|
||||
/// Cells and multi-entry view maps too: two `App`s are compared, so every
|
||||
/// `HashMap` the fingerprint walks must hold 2+ entries (improvise-djcn).
|
||||
fn pivoted() -> App {
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
let mut app = app();
|
||||
app.model_state.workbook.add_category("Month").unwrap();
|
||||
app.model_state.workbook.add_item("Month", "Jan").unwrap();
|
||||
let view = app.model_state.workbook.active_view_mut();
|
||||
let wb = &mut app.model_state.workbook;
|
||||
let items = [
|
||||
("Region", ["South", "East", "West"]),
|
||||
("Month", ["Jan", "Feb", "Mar"]),
|
||||
("Channel", ["Web", "Store", "Phone"]),
|
||||
("Year", ["2024", "2025", "2026"]),
|
||||
];
|
||||
for (category, names) in items {
|
||||
wb.add_category(category).ok();
|
||||
names.iter().for_each(|n| drop(wb.add_item(category, *n)));
|
||||
}
|
||||
for (n, (region, month)) in [("North", "Jan"), ("South", "Feb"), ("East", "Jan")]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let key = CellKey::new(vec![
|
||||
("Region".into(), region.into()),
|
||||
("Month".into(), month.into()),
|
||||
("Channel".into(), "Web".into()),
|
||||
("Year".into(), "2024".into()),
|
||||
]);
|
||||
wb.model.set_cell(key, CellValue::Number(n as f64 + 1.0));
|
||||
}
|
||||
let view = wb.active_view_mut();
|
||||
view.set_axis("Region", Axis::Row);
|
||||
view.set_axis("Month", Axis::Column);
|
||||
view.set_axis("Channel", Axis::Page);
|
||||
view.set_axis("Year", Axis::Page);
|
||||
view.set_page_selection("Channel", "Web");
|
||||
view.set_page_selection("Year", "2024");
|
||||
view.hide_item("Region", "West");
|
||||
view.hide_item("Region", "South");
|
||||
view.hide_item("Month", "Mar");
|
||||
view.toggle_group_collapse("Region", "Coastal");
|
||||
view.toggle_group_collapse("Region", "Inland");
|
||||
view.toggle_group_collapse("Month", "Q1");
|
||||
app.rebuild_layout();
|
||||
app
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ impl RecordEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DataStore {
|
||||
/// Primary storage: interned keys, insertion-ordered so records mode can
|
||||
/// show rows in entry order. That order is correctness — core.md §8.
|
||||
@@ -208,6 +208,37 @@ pub struct DataStore {
|
||||
records: HashMap<RecordHash, RecordEntry>,
|
||||
}
|
||||
|
||||
/// Resolved through the symbol table and sorted wherever the storage is a
|
||||
/// `HashMap`, so two equal stores print alike (improvise-djcn).
|
||||
impl std::fmt::Debug for DataStore {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let DataStore {
|
||||
cells,
|
||||
symbols,
|
||||
index,
|
||||
records,
|
||||
} = self;
|
||||
let cells: Vec<_> = cells
|
||||
.iter()
|
||||
.map(|(key, value)| (self.to_cell_key(key), value))
|
||||
.collect();
|
||||
let index: std::collections::BTreeMap<_, std::collections::BTreeSet<_>> = index
|
||||
.iter()
|
||||
.map(|((cat, item), keys)| {
|
||||
let pair = (symbols.resolve(*cat), symbols.resolve(*item));
|
||||
(pair, keys.iter().map(|k| self.to_cell_key(k).0).collect())
|
||||
})
|
||||
.collect();
|
||||
let records: std::collections::BTreeMap<_, _> = records.iter().collect();
|
||||
f.debug_struct("DataStore")
|
||||
.field("cells", &cells)
|
||||
.field("symbols", symbols)
|
||||
.field("index", &index)
|
||||
.field("records", &records)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl DataStore {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
|
||||
@@ -5,12 +5,22 @@ use std::collections::HashMap;
|
||||
pub struct Symbol(u64);
|
||||
|
||||
/// Bidirectional string ↔ Symbol mapping.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SymbolTable {
|
||||
to_id: HashMap<String, Symbol>,
|
||||
to_str: Vec<String>,
|
||||
}
|
||||
|
||||
/// The interned strings, sorted: which `Symbol` each got is interning order,
|
||||
/// not content, so two equal stores print alike (improvise-djcn).
|
||||
impl std::fmt::Debug for SymbolTable {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let SymbolTable { to_id, to_str: _ } = self;
|
||||
let interned: std::collections::BTreeSet<&String> = to_id.keys().collect();
|
||||
f.debug_set().entries(interned).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SymbolTable {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
|
||||
@@ -317,16 +317,25 @@ pub struct Model {
|
||||
/// cache in hash order, which is not a change (improvise-pc3b, gotcha 18).
|
||||
impl std::fmt::Debug for Model {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut aggs: Vec<_> = self.measure_agg.iter().collect();
|
||||
aggs.sort_by(|a, b| a.0.cmp(b.0));
|
||||
let mut cache: Vec<_> = self.formula_cache.iter().collect();
|
||||
// Destructured, so a new field is a compile error here (improvise-djcn).
|
||||
let Model {
|
||||
name,
|
||||
categories,
|
||||
data,
|
||||
formulas,
|
||||
next_category_id,
|
||||
measure_agg,
|
||||
formula_cache,
|
||||
} = self;
|
||||
let aggs: std::collections::BTreeMap<_, _> = measure_agg.iter().collect();
|
||||
let mut cache: Vec<_> = formula_cache.iter().collect();
|
||||
cache.sort_by(|a, b| a.0.0.cmp(&b.0.0));
|
||||
f.debug_struct("Model")
|
||||
.field("name", &self.name)
|
||||
.field("categories", &self.categories)
|
||||
.field("data", &self.data)
|
||||
.field("formulas", &self.formulas)
|
||||
.field("next_category_id", &self.next_category_id)
|
||||
.field("name", name)
|
||||
.field("categories", categories)
|
||||
.field("data", data)
|
||||
.field("formulas", formulas)
|
||||
.field("next_category_id", next_category_id)
|
||||
.field("measure_agg", &aggs)
|
||||
.field("formula_cache", &cache)
|
||||
.finish()
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn unused_view_name<'a>(existing: impl IntoIterator<Item = &'a str>) -> Stri
|
||||
.expect("a name is free below usize::MAX, there being fewer views")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct View {
|
||||
pub name: String,
|
||||
/// Axis assignment for each category
|
||||
@@ -44,6 +44,38 @@ pub struct View {
|
||||
pub prune_empty: bool,
|
||||
}
|
||||
|
||||
/// Sorted wherever the storage is a `HashMap`, so two equal views print alike;
|
||||
/// destructured, so a new field is a compile error here (improvise-djcn).
|
||||
impl std::fmt::Debug for View {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
let View {
|
||||
name,
|
||||
category_axes,
|
||||
page_selections,
|
||||
hidden_items,
|
||||
collapsed_groups,
|
||||
number_format,
|
||||
prune_empty,
|
||||
} = self;
|
||||
let sorted = |m: &HashMap<String, HashSet<String>>| -> BTreeMap<String, BTreeSet<String>> {
|
||||
m.iter()
|
||||
.map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
|
||||
.collect()
|
||||
};
|
||||
let pages: BTreeMap<_, _> = page_selections.iter().collect();
|
||||
f.debug_struct("View")
|
||||
.field("name", name)
|
||||
.field("category_axes", category_axes)
|
||||
.field("page_selections", &pages)
|
||||
.field("hidden_items", &sorted(hidden_items))
|
||||
.field("collapsed_groups", &sorted(collapsed_groups))
|
||||
.field("number_format", number_format)
|
||||
.field("prune_empty", prune_empty)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// True for the on-demand scratch views, rebuilt rather than persisted.
|
||||
pub fn is_virtual(&self) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user