Merge pull request #96 from fiddlerwoaroof/wave/gate-and-coverage

Three checks that could not fail: duplicate evidence headings, the records-mode settle, the LayoutGeometry case list
This commit is contained in:
Ed Langley
2026-09-21 11:03:45 -07:00
committed by GitHub
12 changed files with 524 additions and 87 deletions
+1
View File
@@ -21,4 +21,5 @@ unicode-width = "^0.2"
web-time = "1"
[dev-dependencies]
improvise-protocol = { path = "../improvise-protocol", features = ["test-support"] }
tempfile = "3"
+83
View File
@@ -2604,6 +2604,89 @@ mod tests {
}
}
/// The settle's `RecordsNormal` arm, driven from the mode it names. The
/// `:` crossing above enters the settle in `Normal` both ways — the evidence.
#[test]
fn a_batch_that_leaves_records_settles_out_of_records_normal() {
use crate::view::Axis;
let mut wb = Workbook::new("T");
wb.add_category("Region").unwrap();
wb.add_item("Region", "East").unwrap();
let mut app = App::new(wb, None);
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
.unwrap();
assert!(app.layout.is_records_mode(), "setup: the grid is records");
assert_eq!(
app.view_state.mode,
AppMode::RecordsNormal,
"setup: the mode handed to the settle must be RecordsNormal"
);
app.apply_effects(vec![Box::new(super::super::effect::SetAxis {
category: "_Index".into(),
axis: Axis::None,
})]);
assert!(
!app.layout.is_records_mode(),
"setup: the batch has to leave records, or nothing crossed"
);
assert_eq!(
app.view_state.mode,
AppMode::Normal,
"a batch that left records stayed in RecordsNormal (improvise-4dpj)"
);
}
/// The batch `SwitchViewAtCursor` emits, both ways across records-ness:
/// records-ness is auto-detected from the axes, so a view switch moves it.
#[test]
fn switching_views_across_records_ness_settles_against_the_view_switched_to() {
for to_records in [false, true] {
let mut wb = Workbook::new("T");
wb.add_category("Region").unwrap();
wb.add_item("Region", "East").unwrap();
let mut app = App::new(wb, None);
// `R` creates and enters `_Records`; the pivot view it left is the
// other end of every crossing below.
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
.unwrap();
let records_view = app.view_state.active_view.clone();
let (from, to) = match to_records {
true => ("Default", records_view.as_str()),
false => (records_view.as_str(), "Default"),
};
app.apply_effects(vec![Box::new(super::super::effect::SwitchView(
from.into(),
))]);
assert_eq!(
app.layout.is_records_mode(),
!to_records,
"setup: the switch has to CROSS, so it must start on the other side"
);
app.apply_effects(vec![
Box::new(super::super::effect::SwitchView(to.into())),
super::super::effect::change_mode(AppMode::Normal),
]);
assert_eq!(
app.layout.is_records_mode(),
to_records,
"setup: the switch has to land, or nothing crossed"
);
let expected = match to_records {
true => AppMode::RecordsNormal,
false => AppMode::Normal,
};
assert_eq!(
app.view_state.mode, expected,
"the batch settled against the view it LEFT, not the one it \
switched to (improvise-4dpj)"
);
}
}
/// A `:` line that really runs settles through `ReturnToNormalUnlessMoved`,
/// which an empty line never reaches. See the evidence.
#[test]
+39 -8
View File
@@ -1410,11 +1410,38 @@ mod tests {
}
}
/// Effects declaring `LayoutGeometry` must move `document_config` — the
/// production hash `model_state.dirty` is decided by (improvise-1xf4).
/// The case list below is COMPLETE over the wire vocabulary, which the
/// compiler maintains — improvise-26dm, `context/code/command.md`.
#[test]
fn layout_geometry_effects_actually_change_document_config() {
let cases: Vec<Box<dyn Effect>> = vec![
fn the_layout_geometry_cases_are_every_wire_effect_of_that_kind() {
let app = grouped_app();
let mut reachable: Vec<String> = crate::ui::reduce::tests::every_model_command()
.iter()
.flat_map(|cmd| crate::ui::reduce::model_effects(&app, cmd))
.filter(|e| e.kind() == EffectKind::Model(ModelChange::LayoutGeometry))
.map(|e| crate::ui::reduce::tests::effect_type_name(e.as_ref()))
.collect();
let mut listed: Vec<String> = layout_geometry_cases()
.iter()
.map(|e| crate::ui::reduce::tests::effect_type_name(e.as_ref()))
.collect();
reachable.sort_unstable();
reachable.dedup();
listed.sort_unstable();
listed.dedup();
assert_eq!(
listed, reachable,
"the LayoutGeometry case list and the wire vocabulary disagree. A name \
only on the LEFT means every_model_command() has a hole, not that the \
case is spurious; a name only on the RIGHT is an effect nothing here \
exercises. Read `context/code/command.md` before deleting either side"
);
}
/// The `LayoutGeometry` effects, named once so two tests can ask the same
/// list the same questions — improvise-26dm, `context/code/command.md`.
fn layout_geometry_cases() -> Vec<Box<dyn Effect>> {
vec![
Box::new(AddCategory("Region".into())),
Box::new(RemoveCategory("Month".into())),
Box::new(CreateView("Second".into())),
@@ -1443,10 +1470,14 @@ mod tests {
Box::new(CycleAxis("Type".into())),
Box::new(SetNumberFormat(",.3f".into())),
Box::new(TogglePruneEmpty),
];
// A COUNT, not a totality check: nothing forces a fourteenth here, and a
// probe variant proved it — improvise-5ubs, `context/code/command.md`.
assert_eq!(cases.len(), 13, "the LayoutGeometry effects were thirteen");
]
}
/// Effects declaring `LayoutGeometry` must move `document_config` — the
/// production hash `model_state.dirty` is decided by (improvise-1xf4).
#[test]
fn layout_geometry_effects_actually_change_document_config() {
let cases = layout_geometry_cases();
for effect in cases {
assert_eq!(
effect.kind(),
+24 -5
View File
@@ -273,7 +273,7 @@ pub fn reduce_key(app: &mut App, key: KeyEvent) -> Vec<Command> {
}
#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use crate::model::cell::CellValue;
use crate::ui::app::AppMode;
@@ -492,9 +492,9 @@ mod tests {
app
}
/// One value per `ModelCommand` variant. [`variant_name`] matches
/// exhaustively, so a new variant fails the build until it is listed here.
fn every_model_command() -> Vec<ModelCommand> {
/// One value per `ModelCommand` variant — by hand, and NOT compiler-forced:
/// [`variant_name`]'s exhaustive match only forces an arm there (PR #96).
pub(crate) fn every_model_command() -> Vec<ModelCommand> {
vec![
ModelCommand::SetCell {
cell: record_cell(&a_stored_cell()),
@@ -658,7 +658,7 @@ mod tests {
/// An effect's type name, off the leading identifier of its derived `Debug`.
/// `Effect` is a trait object, so `std::any::type_name` cannot reach it.
fn effect_type_name(effect: &dyn Effect) -> String {
pub(crate) fn effect_type_name(effect: &dyn Effect) -> String {
format!("{effect:?}")
.split(|c: char| !c.is_alphanumeric() && c != '_')
.next()
@@ -711,6 +711,25 @@ mod tests {
assert_eq!(names.len(), total, "a variant is listed twice: {names:?}");
}
/// ...and covers it WHOLLY, against serde's derive — the one enumeration the
/// compiler maintains (improvise-5ubs). `context/code/command.md`.
#[test]
fn the_enumeration_names_every_variant_the_derive_declares() {
let listed: Vec<String> = every_model_command()
.iter()
.map(|cmd| variant_name(cmd).to_string())
.collect();
let missed: Vec<String> = improvise_protocol::variants::declared::<ModelCommand>()
.into_iter()
.filter(|name| !listed.contains(name))
.collect();
assert!(
missed.is_empty(),
"every_model_command() never constructs {missed:?} — adding an arm to \
variant_name is compiler-forced, adding a VALUE to the list is not"
);
}
/// A model `Command` applied authoritatively lands where the keypress bound
/// to the same mutation lands, `Dirty` included.
#[test]
+7
View File
@@ -13,6 +13,13 @@ serde = { version = "1", features = ["derive"] }
# (`AxisFrame::header_widths`). A terminal column is a display cell, not a
# `char`, and the TUI has always measured it this way.
unicode-width = "^0.2"
# Only for `variants`, which is off unless a test build asks for it.
serde_json = { version = "1", optional = true }
[features]
# `variants` reaches OUT of this crate: improvise-command bounds its own
# ModelCommand enumeration with it. Off in every shipped build (improvise-26dm).
test-support = ["dep:serde_json"]
[dev-dependencies]
serde_json = "1"
+2 -2
View File
@@ -13,8 +13,8 @@ pub mod keymap;
mod reduce;
pub mod render;
pub mod resolve;
#[cfg(test)]
pub(crate) mod variants;
#[cfg(any(test, feature = "test-support"))]
pub mod variants;
pub mod viewmodel;
pub use basis::{Basis, BasisCache, Rejection};