From c831e5ea4a553293ee2c89843b68ed8bcfb26b69 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Tue, 22 Sep 2026 21:32:00 -0700 Subject: [PATCH] WIP: float_roundtrip on frame parsers; failing NaN quiescence test (improvise-6oy4, improvise-4w09) --- crates/improvise-command/src/ui/project.rs | 60 +++++++++++++++++++ crates/improvise-wasm-client/Cargo.toml | 4 +- .../tests/browser_round_trip.rs | 38 ++++++++++++ crates/improvise-worker-server/Cargo.toml | 4 +- crates/improvise-ws-server/Cargo.toml | 4 +- 5 files changed, 107 insertions(+), 3 deletions(-) diff --git a/crates/improvise-command/src/ui/project.rs b/crates/improvise-command/src/ui/project.rs index 441af17e..9242cf18 100644 --- a/crates/improvise-command/src/ui/project.rs +++ b/crates/improvise-command/src/ui/project.rs @@ -1280,6 +1280,66 @@ mod tests { ); } + /// `.improv` carries `nan` as a value (improvise-55v2); the NaN sits at + /// (1, 1), off the origin. Evidence file. + fn app_with_a_nan_cell() -> App { + let wb = crate::persistence::parse_md( + "v2025-04-09\n# NaN\nInitial View: Default\n\n\ + ## View: Default\nRegion: row\n_Measure: column\n_Index: none\n_Dim: none\n\n\ + ## Category: _Measure\n- Revenue, Cost\n\n\ + ## Category: Region\n- North, South\n\n\ + ## Data\n\ + Region=North, _Measure=Revenue = 1\n\ + Region=North, _Measure=Cost = 2\n\ + Region=South, _Measure=Revenue = 3\n\ + Region=South, _Measure=Cost = nan\n", + ) + .expect("a document holding nan parses"); + let mut app = App::new(wb, None); + app.rebuild_layout(); + app + } + + /// A NaN cell compares unequal to itself under `CellValue`'s `PartialEq`, + /// so the grid never quiesced (improvise-4w09). Evidence file. + #[test] + fn a_grid_holding_nan_quiesces() { + let app = app_with_a_nan_cell(); + let grid = grid_of( + &app, + Viewport { + row_offset: 0, + col_offset: 0, + rows: 10, + cols: 10, + }, + ); + assert!( + matches!(grid.cell(1, 1).and_then(|c| c.value.clone()), + Some(CellValue::Number(n)) if n.is_nan()), + "precondition: the NaN must be projected, or nothing below is tested" + ); + + assert_eq!( + grid_updates(&grid, &grid), + vec![], + "a grid diffed against itself owes the client nothing" + ); + assert!( + GridCache::folded(&grid.seed()) == grid, + "the seed must compact to the cache it came from (gotcha 32)" + ); + + let mut sessions = Sessions::default(); + sessions.connect(&app, 10, 10); + sessions.project_all(&app, Dirty::EVERYTHING); + let again = sessions.project_all(&app, Dirty::EVERYTHING); + assert!( + again.is_empty(), + "an unchanged grid must not be re-sent: {again:?}" + ); + } + /// Region on Row with `East` in a group that comes **last**, so the header is /// the final entry and nothing follows it: `axis_frame`'s trailing rule. fn app_with_a_trailing_group(collapsed: bool) -> App { diff --git a/crates/improvise-wasm-client/Cargo.toml b/crates/improvise-wasm-client/Cargo.toml index 09d40471..1abfbb14 100644 --- a/crates/improvise-wasm-client/Cargo.toml +++ b/crates/improvise-wasm-client/Cargo.toml @@ -12,7 +12,9 @@ crate-type = ["cdylib", "rlib"] [dependencies] improvise-protocol = { path = "../improvise-protocol" } serde = { version = "1", features = ["derive"] } -serde_json = "1" +# Parses frames, so it must read back the f64 that was written: per crate, because +# the wasm bundle builds alone (improvise-6oy4, context/code/protocol.md). +serde_json = { version = "1", features = ["float_roundtrip"] } wasm-bindgen = "0.2" # Deliberately NOT a dependency: `improvise-command`. A keymap binding names diff --git a/crates/improvise-wasm-client/tests/browser_round_trip.rs b/crates/improvise-wasm-client/tests/browser_round_trip.rs index 4b426a40..6bcddd75 100644 --- a/crates/improvise-wasm-client/tests/browser_round_trip.rs +++ b/crates/improvise-wasm-client/tests/browser_round_trip.rs @@ -701,6 +701,44 @@ fn a_yanked_number_crosses_the_serialized_wire_exactly() { ); } +/// The two tests around this one share a feature-unified build, so neither can +/// see ONE frame-parsing crate lacking `float_roundtrip`. Evidence file. +#[test] +fn every_build_that_parses_a_frame_enables_float_roundtrip() { + for (krate, target) in [ + ("improvise-wasm-client", Some("wasm32-unknown-unknown")), + ("improvise-worker-server", Some("wasm32-unknown-unknown")), + ("improvise-ws-server", None), + ] { + let mut cargo = std::process::Command::new(env!("CARGO")); + cargo.current_dir(env!("CARGO_MANIFEST_DIR")).args([ + "tree", + "--locked", + "--edges", + "normal,features", + "--invert", + "serde_json", + "--package", + krate, + ]); + if let Some(target) = target { + cargo.args(["--target", target]); + } + let out = cargo.output().expect("cargo tree must run"); + assert!( + out.status.success(), + "{krate}: cargo tree failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + let tree = String::from_utf8(out.stdout).expect("cargo tree prints UTF-8"); + assert!( + tree.contains(r#"serde_json feature "float_roundtrip""#), + "{krate} parses frames without float_roundtrip, so its numbers land \ + one ULP off:\n{tree}" + ); + } +} + /// A typed number is parsed exactly in the client and re-parsed from JSON by /// the host. Evidence file. #[test] diff --git a/crates/improvise-worker-server/Cargo.toml b/crates/improvise-worker-server/Cargo.toml index 600bbd8d..9a2de0fb 100644 --- a/crates/improvise-worker-server/Cargo.toml +++ b/crates/improvise-worker-server/Cargo.toml @@ -17,7 +17,9 @@ improvise-protocol = { path = "../improvise-protocol" } improvise-command = { path = "../improvise-command" } anyhow = "1" serde = { version = "1", features = ["derive"] } -serde_json = "1" +# Parses frames, so it must read back the f64 that was written: per crate, because +# the wasm bundle builds alone (improvise-6oy4, context/code/protocol.md). +serde_json = { version = "1", features = ["float_roundtrip"] } # The browser half. Only the wasm build pulls these in, so `cargo test` on the # host builds and drives `worker.rs`/`vfs.rs` with no JS anywhere near them — diff --git a/crates/improvise-ws-server/Cargo.toml b/crates/improvise-ws-server/Cargo.toml index 606c6c77..cee122cf 100644 --- a/crates/improvise-ws-server/Cargo.toml +++ b/crates/improvise-ws-server/Cargo.toml @@ -17,7 +17,9 @@ improvise-io = { path = "../improvise-io" } improvise-protocol = { path = "../improvise-protocol" } improvise-command = { path = "../improvise-command" } serde = { version = "1", features = ["derive"] } -serde_json = "1" +# Parses frames, so it must read back the f64 that was written (improvise-6oy4, +# context/code/protocol.md). +serde_json = { version = "1", features = ["float_roundtrip"] } anyhow = "1" clap = { version = "4.6.7", features = ["derive"] } tokio = { version = "1", features = ["rt", "macros", "net", "sync"] }