//! Bug-hunt reproductions. An `#[ignore]`d one is still open; un-ignoring it is //! part of the fix. Open ones: `cargo test --test bug_hunt_reproductions -- --ignored`. use improvise_io::import::csv_parser::parse_csv_bytes_with_warnings; use improvise_io::{ formula::AggFunc, persistence::{format_md, parse_md}, workbook::Workbook, }; #[test] fn grouped_item_order_survives_save() { let mut w = Workbook::new("test"); w.add_category("C").unwrap(); w.add_item_in_group("C", "first", "G").unwrap(); w.active_view_mut().set_expand_depth("C", 1); w.add_item("C", "second").unwrap(); let r = parse_md(&format_md(&w)).unwrap(); assert_eq!( r.model.category("C").unwrap().ordered_item_names(), w.model.category("C").unwrap().ordered_item_names() ); } #[test] fn aggregation_survives_save() { let mut w = Workbook::new("test"); w.add_item("_Measure", "Price").unwrap(); w.model.measure_agg.insert("Price".into(), AggFunc::Avg); let r = parse_md(&format_md(&w)).unwrap(); assert!( matches!(r.model.measure_agg.get("Price"), Some(AggFunc::Avg)), "loaded aggregation: {:?}", r.model.measure_agg ); } #[test] fn deleted_default_stays_deleted() { let mut w = Workbook::new("test"); w.create_view("Custom"); w.delete_view("Default").unwrap(); let r = parse_md(&format_md(&w)).unwrap(); assert_eq!( r.views.keys().collect::>(), w.views.keys().collect::>() ); } #[test] fn csv_nonfinite_is_not_zero() { for token in ["NaN", "inf", "-inf", "1e999"] { let (r, _) = parse_csv_bytes_with_warnings(format!("Value\n{token}\n").as_bytes()).unwrap(); assert_ne!(r[0]["Value"].as_f64(), Some(0.0), "{token} became zero"); assert_eq!( r[0]["Value"].as_str(), Some(token), "{token} must survive as its own text, not as some other number" ); } } #[test] fn duplicate_csv_headers_not_silent() { let (records, warnings) = parse_csv_bytes_with_warnings(b"A,A\n1,2\n").unwrap(); let fields = records[0].as_object().unwrap(); assert_eq!( fields.len(), 2, "both columns must survive as separate fields: {fields:?}" ); assert_eq!(fields["A"], "1", "{fields:?}"); assert_eq!(fields["A_2"], "2", "{fields:?}"); assert!( warnings .iter() .any(|warning| warning.to_lowercase().contains("duplicate") && warning.contains("A")), "the rename must be reported: {warnings:?}" ); } #[test] fn blank_measure_still_infers_numeric() { use improvise_io::import::{analyzer::FieldKind, wizard::ImportPipeline}; let (records, _) = parse_csv_bytes_with_warnings(b"Region,Revenue\nEast,10\nWest,\n").unwrap(); let p = ImportPipeline::from_csv(records); let proposal = p.proposals.iter().find(|p| p.field == "Revenue").unwrap(); assert_eq!(proposal.kind, FieldKind::Measure); } #[test] fn repeated_import_coordinates_not_silently_overwritten() { use improvise_io::import::wizard::ImportPipeline; let (records, _) = parse_csv_bytes_with_warnings(b"Region,Revenue\nEast,10\nEast,20\n").unwrap(); let mut pipeline = ImportPipeline::from_csv(records); pipeline.on_duplicate = improvise_io::import::wizard::DuplicatePolicy::Sum; let (wb, warnings) = pipeline.build_model_with_warnings().unwrap(); let values: Vec = wb .model .data .iter_cells() .filter_map(|(_, v)| v.as_f64()) .collect(); assert_eq!( values, [30.0], "the two records are one cell and neither may be dropped" ); let warning = warnings .iter() .find(|w| w.to_lowercase().contains("duplicate")) .unwrap_or_else(|| panic!("summing is a decision and is reported: {warnings:?}")); assert!(warning.contains("Region=East"), "{warning}"); assert!(warning.contains("_Measure=Revenue"), "{warning}"); } #[test] #[ignore = "improvise-55v2: known bug reproduction"] fn nonfinite_cell_value_survives_wire() { use improvise::protocol::{CellValue, Command}; let cmd = Command::SetYanked(Some(CellValue::Number(f64::INFINITY))); let json = serde_json::to_string(&cmd).unwrap(); assert!( serde_json::from_str::(&json).is_ok(), "unreadable wire: {json}" ); } #[test] #[ignore = "improvise-8l0c: known bug reproduction"] fn formula_panel_unicode_does_not_panic() { use improvise::{ protocol::FormulaCache, ui::{app::AppMode, formula_panel::FormulaContent, panel::Panel}, }; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; let cache = FormulaCache { formulas: vec!["|é| = 1".into()], }; let area = Rect::new(0, 0, 7, 5); let mut buffer = Buffer::empty(area); Panel::new( FormulaContent::new(&cache, &AppMode::Normal), &AppMode::Normal, 0, ) .render(area, &mut buffer); } #[test] #[ignore = "improvise-vw47: known bug reproduction"] fn new_view_after_delete_does_not_overwrite_survivor() { use improvise::{ command::{ cmd::{Cmd, panel::CreateAndSwitchView}, key::{KeyCode, KeyModifiers}, }, ui::app::App, }; let mut w = Workbook::new("test"); w.create_view("View 2"); w.create_view("View 3").number_format = ".4".into(); w.delete_view("View 2").unwrap(); let mut app = App::new(w, None); let effects = CreateAndSwitchView.execute(&app.cmd_context(KeyCode::Char('n'), KeyModifiers::NONE)); app.apply_effects(effects); assert_eq!(app.model_state.workbook.views["View 3"].number_format, ".4"); assert_eq!(app.model_state.workbook.views.len(), 3); } #[test] #[ignore = "improvise-cj4u: known bug reproduction"] fn label_stays_off_axes_in_new_view() { use improvise_io::view::Axis; let mut w = Workbook::new("test"); w.add_label_category("Description").unwrap(); assert_eq!(w.active_view().axis_of("Description"), Axis::None); assert_eq!(w.create_view("Other").axis_of("Description"), Axis::None); } #[test] fn long_csv_row_is_not_silently_dropped() { let (records, warnings) = parse_csv_bytes_with_warnings(b"A,B\n1,2,3\n4,5\n1,2,3,4\n").unwrap(); assert_eq!(records.len(), 3, "no row is discarded outright"); let warning = warnings .iter() .find(|w| w.contains("more fields")) .unwrap_or_else(|| panic!("extra values dropped silently: {warnings:?}")); assert!( warning.contains("2 row(s)") && warning.contains("2 header"), "the warning counts the affected rows and the header width: {warning}" ); assert!( warning.contains("widest row: 4"), "the warning says how much was dropped: {warning}" ); let (_, clean) = parse_csv_bytes_with_warnings(b"A,B\n1,2\n").unwrap(); assert!( clean.is_empty(), "control: a full row is not long: {clean:?}" ); } // Passing control: embedded carriage returns already survive persistence. #[test] fn literal_cr_name_survives_roundtrip() { let mut w = Workbook::new("test"); w.add_category("C").unwrap(); w.add_item("C", "x\ry").unwrap(); let text = format_md(&w); let r = parse_md(&text).unwrap(); assert_eq!( r.model.category("C").unwrap().ordered_item_names(), vec!["x\ry"] ); } #[test] fn scalar_cell_survives_roundtrip() { use improvise_io::model::cell::{CellKey, CellValue}; let mut w = Workbook::new("test"); w.model .set_cell(CellKey::new(vec![]), CellValue::Number(42.0)); let text = format_md(&w); assert!( parse_md(&text).is_ok(), "writer emitted unreadable document: {text}" ); } fn revenue_book() -> Workbook { use improvise_io::model::cell::{CellKey, CellValue}; let mut w = Workbook::new("T"); w.add_category("Region").unwrap(); w.add_item("Region", "East").unwrap(); w.add_item("_Measure", "Revenue").unwrap(); w.model.set_cell( CellKey::new(vec![ ("Region".into(), "East".into()), ("_Measure".into(), "Revenue".into()), ]), CellValue::Number(10.0), ); w } #[test] fn sum_of_derived_measure_agrees_with_identity_expression() { use improvise_io::{formula::parse_formula, model::cell::CellKey}; let mut w = revenue_book(); for raw in [ "Double = Revenue * 2", "A = SUM(Double)", "B = SUM(Double + 0)", ] { w.model.add_formula(parse_formula(raw, "_Measure").unwrap()); } let key = CellKey::new(vec![("Region".into(), "East".into())]); let a = w.model.evaluate(&key.clone().with("_Measure", "A")); let b = w.model.evaluate(&key.with("_Measure", "B")); assert_eq!(a, b); } #[test] fn unknown_aggregate_reference_is_not_all_measures() { use improvise_io::{ formula::parse_formula, model::cell::{CellKey, CellValue}, }; let mut w = revenue_book(); w.model .add_formula(parse_formula("Total = SUM(Typo)", "_Measure").unwrap()); let key = CellKey::new(vec![ ("Region".into(), "East".into()), ("_Measure".into(), "Total".into()), ]); assert!( matches!(w.model.evaluate(&key), Some(CellValue::Error(_))), "actual {:?}", w.model.evaluate(&key) ); } #[test] #[ignore = "improvise-3b2b: known bug reproduction"] fn category_panel_cannot_delete_required_virtual() { use improvise::{ command::{ cmd::{Cmd, panel::DeleteCategoryAtCursor}, key::{KeyCode, KeyModifiers}, }, ui::app::App, }; let mut app = App::new(revenue_book(), None); app.view_state.cat_panel_cursor = 0; let fx = DeleteCategoryAtCursor.execute(&app.cmd_context(KeyCode::Char('d'), KeyModifiers::NONE)); app.apply_effects(fx); let fx = improvise::command::cmd::grid::ToggleRecordsMode .execute(&app.cmd_context(KeyCode::Char('r'), KeyModifiers::NONE)); app.apply_effects(fx); assert!( app.layout.is_records_mode(), "deleting _Index prevents records mode; status {}", app.view_state.status_msg ); } #[test] #[ignore = "improvise-8l0c: known bug reproduction"] fn formula_panel_handles_zero_inner_width() { use improvise::{ protocol::FormulaCache, ui::{app::AppMode, formula_panel::FormulaContent, panel::Panel}, }; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; let cache = FormulaCache { formulas: vec!["A = 1".into()], }; let area = Rect::new(0, 0, 2, 5); let mut buffer = Buffer::empty(area); Panel::new( FormulaContent::new(&cache, &AppMode::Normal), &AppMode::Normal, 0, ) .render(area, &mut buffer); } #[test] fn save_with_unfinished_record_is_readable() { use improvise::{ command::{ cmd::{ Cmd, grid::{AddRecordRow, ToggleRecordsMode}, }, key::{KeyCode, KeyModifiers}, }, ui::app::App, }; let mut app = App::new(revenue_book(), None); let fx = ToggleRecordsMode.execute(&app.cmd_context(KeyCode::Char('r'), KeyModifiers::NONE)); app.apply_effects(fx); assert!(app.layout.is_records_mode()); let fx = AddRecordRow.execute(&app.cmd_context(KeyCode::Char('o'), KeyModifiers::NONE)); app.apply_effects(fx); let text = format_md(&app.model_state.workbook); assert!(parse_md(&text).is_ok(), "saved unparseable file: {text}"); } #[test] fn circular_formula_not_a_plausible_cached_number() { use improvise_io::{ formula::parse_formula, model::{ Aggregation, cell::{CellKey, CellValue}, }, }; let mut w = revenue_book(); w.model .add_formula(parse_formula("Revenue = Revenue + 1", "_Measure").unwrap()); let key = CellKey::new(vec![ ("Region".into(), "East".into()), ("_Measure".into(), "Revenue".into()), ]); assert!(matches!(w.model.evaluate(&key), Some(CellValue::Error(_)))); w.model .recompute_formulas(&["_Index".into(), "_Dim".into()]); let actual = w.model.evaluate_aggregated(&key, Aggregation::OverHidden); assert!( matches!(actual, Some(CellValue::Error(_))), "circular formula became {actual:?}" ); } #[test] fn wide_cell_clamps_without_overflow() { use improvise::{ model::cell::{CellKey, CellValue}, ui::app::App, }; // 65535 overflows the `+ 1` gap; 65536 truncates the `as u16` cast to 0. // 20 is the ordinary case the cap must leave alone, gap included. for (len, expect) in [ (20usize, 21u16), (65535, improvise::ui::viewport::MAX_COL_WIDTH), (65536, improvise::ui::viewport::MAX_COL_WIDTH), ] { let mut w = revenue_book(); w.model.set_cell( CellKey::new(vec![ ("Region".into(), "East".into()), ("_Measure".into(), "Revenue".into()), ]), CellValue::Text("x".repeat(len)), ); let app = App::new(w, None); let widths = improvise::ui::viewport::compute_col_widths( &app.model_state.workbook.model, &app.layout, &improvise::format::FormatPlan::uniform(""), ); assert_eq!( widths, vec![expect], "a {len}-column value must yield exactly one column, of width {expect}" ); } } #[test] fn ordinary_controls() { use improvise_io::import::{analyzer::FieldKind, wizard::ImportPipeline}; let (records, warnings) = parse_csv_bytes_with_warnings(b"Region,Revenue\nEast,10\nWest,20\n").unwrap(); assert!(warnings.is_empty()); let p = ImportPipeline::from_csv(records); assert_eq!( p.proposals .iter() .find(|p| p.field == "Revenue") .unwrap() .kind, FieldKind::Measure ); let (w, warnings) = p.build_model_with_warnings().unwrap(); assert!(warnings.is_empty()); assert_eq!( w.model .data .iter_cells() .filter_map(|(_, v)| v.as_f64()) .sum::(), 30.0 ); assert!(parse_md(&format_md(&w)).is_ok()); } // Passing control (improvise-nvq4): quote_name already pipe-quotes a name // containing `/`, so hidden/collapsed round-trip. The report did not reproduce. #[test] fn slash_in_hidden_name_survives_roundtrip() { let mut w = Workbook::new("test"); w.add_category("C").unwrap(); w.add_item("C", "A/B").unwrap(); w.add_item_in_group("C", "x", "G/H").unwrap(); w.active_view_mut().set_expand_depth("C", 1); w.active_view_mut().hide_item("C", "A/B"); w.active_view_mut().toggle_group_collapse("C", &["G/H"]); let text = format_md(&w); let r = parse_md(&text).unwrap_or_else(|e| panic!("unparseable: {e}\n{text}")); assert!( r.active_view().is_hidden("C", "A/B"), "hidden lost:\n{text}" ); assert!( r.active_view().is_group_collapsed("C", &["G/H"]), "collapsed lost:\n{text}" ); }