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:
@@ -21,4 +21,5 @@ unicode-width = "^0.2"
|
||||
web-time = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
improvise-protocol = { path = "../improvise-protocol", features = ["test-support"] }
|
||||
tempfile = "3"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user