//! Does `PrintReport` actually parse what real printers really send? //! //! The fixtures in `tests/fixtures/` are verbatim `print.push_status` //! payloads captured off real hardware on 2026-10-03 via //! `cargo run --example live_reports` (hardware identifiers scrubbed): //! //! - `push_status_v1_p1p_idle.json` - a P1P on BambuMQTT v1, idle. //! - `push_status_v2_h2c_printing.json` - an H2C on BambuMQTT v2, 13% into //! a real job. //! //! These exist because the struct was originally built from a *truncated* //! example payload, and the gap between that and reality turned out to be //! the cause of two live misbehaviours (silently empty `print_status()`, and //! bogus "sequence gap" warnings). A fixture is the only way to keep that //! from regressing without a printer plugged in. use continuum_proxy::printer::PrintReport; fn load(name: &str) -> serde_json::Value { let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/"); let text = std::fs::read_to_string(format!("{path}{name}")).expect("fixture should exist"); serde_json::from_str(&text).expect("fixture should be valid JSON") } /// The failure mode that mattered: `handle_incoming_report` does /// `if let Ok(report) = serde_json::from_value::(..)`, so a /// parse error doesn't just lose that one update - it's invisible. Assert on /// the error text, not just `is_ok()`, so a regression says *what* broke. fn assert_parses(name: &str) { let value = load(name); match serde_json::from_value::(value) { Ok(_) => {} Err(err) => panic!("{name} failed to parse as PrintReport: {err}"), } } #[test] fn parses_v1_idle_push_status() { assert_parses("push_status_v1_p1p_idle.json"); } #[test] fn parses_v2_printing_push_status() { assert_parses("push_status_v2_h2c_printing.json"); } /// The whole point of consuming these at all: a print farm needs progress /// and temperatures. If the struct parses but drops these, it's no more /// useful than not parsing. #[test] fn v2_printing_report_carries_progress_and_temperatures() { let value = load("push_status_v2_h2c_printing.json"); let report: PrintReport = serde_json::from_value(value).expect("should parse"); assert_eq!(report.gcode_state.as_deref(), Some("RUNNING")); assert_eq!(report.mc_percent, Some(13)); assert_eq!(report.layer_num, Some(2)); assert_eq!(report.total_layer_num, Some(50)); assert_eq!(report.mc_remaining_time, Some(102)); assert_eq!(report.nozzle_temper, Some(230.0)); assert_eq!(report.subtask_name.as_deref(), Some("\u{15b}wieczka-24.stl")); } /// An idle printer still has to produce a coherent report - just with /// nothing printing. #[test] fn v1_idle_report_reads_as_idle() { let value = load("push_status_v1_p1p_idle.json"); let report: PrintReport = serde_json::from_value(value).expect("should parse"); assert_eq!(report.gcode_state.as_deref(), Some("IDLE")); assert_eq!(report.mc_percent, Some(0)); } /// v1 and v2 disagree about tray field names (`tray_temp`/`tray_time` vs /// `drying_temp`/`drying_time`) - this is the specific mismatch that made /// every v1 push carrying AMS data fail. Both spellings have to work. #[test] fn both_protocol_generations_parse_their_ams_trays() { for name in ["push_status_v1_p1p_idle.json", "push_status_v2_h2c_printing.json"] { let value = load(name); let report: PrintReport = serde_json::from_value(value).unwrap_or_else(|err| { panic!("{name}: {err}"); }); let ams = report.ams.expect("fixture should carry AMS state"); assert!(!ams.ams.is_empty(), "{name}: expected at least one AMS unit"); } } fn parse(name: &str) -> PrintReport { serde_json::from_value(load(name)).unwrap_or_else(|err| panic!("{name}: {err}")) } /// The P1P sends an external spool; pin the fields a farm would show. #[test] fn v1_report_carries_the_external_spool() { let report = parse("push_status_v1_p1p_idle.json"); let vt = report.vt_tray.expect("v1 fixture carries vt_tray"); assert_eq!(vt.id, "254"); assert_eq!(vt.tray_info_idx.as_deref(), Some("P9599a25")); assert_eq!(vt.tray_type.as_deref(), Some("PETG")); assert_eq!(vt.tray_color.as_deref(), Some("D3C5A3FF")); assert_eq!(vt.cali_idx, Some(-1)); assert_eq!(vt.n, Some(1)); } /// The H2C has no `vt_tray` key at all. That is the observed v1/v2 /// divergence, NOT a parse failure: if a change makes `vt_tray` required, /// `parse` panics here instead of every v2 report silently vanishing in /// `handle_incoming_report`. #[test] fn v2_report_has_no_external_spool_and_still_parses() { let report = parse("push_status_v2_h2c_printing.json"); assert!(report.vt_tray.is_none()); } /// `stg_cur` is what tells "preparing" from "stalled" while `layer_num` is 0. #[test] fn stage_fields_parse_on_both_generations() { let v1 = parse("push_status_v1_p1p_idle.json"); assert_eq!(v1.stg_cur, Some(0)); assert_eq!(v1.stg, Some(vec![])); let v2 = parse("push_status_v2_h2c_printing.json"); assert_eq!(v2.stg_cur, Some(0)); assert_eq!(v2.stg, Some(vec![64, 29, 13, 74, 72, 4, 54, 39, 8, 14, 1, 3])); } /// A v1 partial push without these must not erase what a full one set. #[test] fn merge_keeps_stage_and_spool_when_a_partial_update_omits_them() { let mut base = parse("push_status_v1_p1p_idle.json"); let mut partial = parse("push_status_v1_p1p_idle.json"); partial.stg_cur = None; partial.stg = None; partial.vt_tray = None; base.stg_cur = Some(54); base.stg = Some(vec![54, 14, 13]); base.merge_from(partial); assert_eq!(base.stg_cur, Some(54)); assert_eq!(base.stg, Some(vec![54, 14, 13])); assert!(base.vt_tray.is_some()); }