//! Targeted tests that kill mutants surfaced by `cargo mutants`. //! //! Each test below corresponds to a specific mutant that proptest and //! the canonical vectors failed to catch. Keep them — deleting one //! unlocks the corresponding mutant. #![allow(clippy::unwrap_used, clippy::panic)] use donglora_protocol::framing::{CobsDecoder, MAX_FRAME, cobs_encode_response}; use donglora_protocol::{Bandwidth, Command, RADIO_CONFIG_SIZE, RadioConfig, Response}; use heapless::Vec as HVec; fn base_config() -> RadioConfig { RadioConfig { freq_hz: 915_000_000, bw: Bandwidth::Khz125, sf: 7, cr: 5, sync_word: 0x1234, tx_power_dbm: 22, preamble_len: 16, cad: 1, } } // ── RadioConfig::cad_enabled ──────────────────────────────────────── #[test] fn cad_enabled_is_true_for_non_zero() { // Kills: replace cad_enabled with `true`/`false`, replace `!=` with `==`. for cad in [1u8, 2, 127, 255] { let cfg = RadioConfig { cad, ..base_config() }; assert!(cfg.cad_enabled(), "cad={cad} must report enabled"); } } #[test] fn cad_enabled_is_false_for_zero() { let cfg = RadioConfig { cad: 0, ..base_config() }; assert!(!cfg.cad_enabled()); } // ── Command::from_bytes Transmit with-config boundary ────────────── #[test] fn transmit_with_config_requires_more_than_radio_config_size() { // The parser checks `rest.len() > RADIO_CONFIG_SIZE` so there is at // least one byte past the config for the `len` field (actually two, // caught by a later check). Swapping `>` for `>=` or `&&` for `||` // silently breaks the boundary — these inputs pin it. // has_config=1 with exactly RADIO_CONFIG_SIZE bytes of config and // nothing else: must reject (no room for the len field). let mut just_config = [0u8; 2 + RADIO_CONFIG_SIZE]; just_config[0] = 5; // Transmit tag just_config[1] = 1; // has_config = true base_config().write_to(&mut just_config[2..]); assert!( Command::from_bytes(&just_config).is_none(), "has_config=1 with no len bytes must reject", ); // has_config=2 is a sentinel the parser must reject outright. // Swapping `&&` for `||` would accept it (high byte count would make // the second half true). let mut bad_flag = [0u8; 32]; bad_flag[0] = 5; bad_flag[1] = 2; assert!( Command::from_bytes(&bad_flag).is_none(), "has_config must be strictly 0 or 1", ); } // ── CobsDecoder::reset ───────────────────────────────────────────── #[test] fn cobs_decoder_reset_discards_partial_frame() { let mut decoder = CobsDecoder::new(); // Feed half a frame (no trailing 0x00 yet) then reset. The reset must // discard the accumulator so the next (different) frame decodes correctly. decoder.feed(&[0x02, 0x99], |_| panic!("no command yet")); decoder.reset(); // Now feed a complete, properly-framed Ping (tag 0). let mut raw = [0u8; 1]; raw[0] = 0; let mut framed = [0u8; 8]; let n = ucobs::encode(&raw[..1], &mut framed).unwrap(); framed[n] = 0x00; let mut got = None; decoder.feed(&framed[..n + 1], |c| got = Some(c)); assert_eq!(got, Some(Command::Ping)); } // ── CobsDecoder::feed — empty-frame handling ─────────────────────── #[test] fn cobs_decoder_treats_back_to_back_zeros_as_empty_frames() { // Kills: `if self.len > 0` → `>=`. With `>=` we would attempt to // decode an empty buffer on every stray 0x00 byte, which ucobs::decode // rejects — so the behaviour would be a silent change in error-handling // rather than a functional break. This test pins the current contract: // feeding only sentinel bytes never yields a Command and never panics. let mut decoder = CobsDecoder::new(); let mut yielded = 0; decoder.feed(&[0, 0, 0, 0, 0], |_| yielded += 1); assert_eq!(yielded, 0); } // ── cobs_encode_response — buffer-full boundary ──────────────────── #[test] fn cobs_encode_response_rejects_overflow() { // A maximum-payload RxPacket produces 263 raw bytes + 3 COBS overhead // ≈ 266 bytes — well under MAX_FRAME (512). To exercise the overflow // branch we fabricate the condition by encoding into a too-small // buffer via the same function on a synthetic oversized response. // // This test uses the real function with real buffers. The overflow // branch fires only when `encoded_len + 1 > encode_buf.len()`, which // for MAX_FRAME = 512 cannot happen with any valid Response. So we // rely on proptest / fuzz to exercise sub-threshold cases and pin the // happy path here: the happy path must always produce a trailing 0x00 // sentinel and leave the encoded-length byte in bounds. let mut payload: HVec = HVec::new(); for i in 0..256u16 { payload.push((i & 0xFF) as u8).unwrap(); } let mut write = [0u8; MAX_FRAME]; let mut encode = [0u8; MAX_FRAME]; let framed = cobs_encode_response( Response::RxPacket { rssi: -1, snr: -2, payload, }, &mut write, &mut encode, ) .unwrap(); // Last byte is the sentinel. assert_eq!(*framed.last().unwrap(), 0x00); // Encoded length sits strictly inside the buffer; the `< buf.len()` // check must be strict, not `<=`. assert!(framed.len() <= MAX_FRAME); }