#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::error::Error;
use std::path::PathBuf;
use snss::{
decode_navigation, read_records, replay, Dialect, PickleError, SessionStore, SnssError,
SourceKind, Warning, MAGIC, SUPPORTED_VERSION,
};
mod common;
use common::build;
fn tmp_subdir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("snss-cov-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("mk tmp dir");
dir
}
#[test]
fn snss_error_display_and_source() {
let bad = SnssError::BadMagic(*b"NOPE");
assert!(format!("{bad}").contains("not an SNSS file"));
assert!(bad.source().is_none());
let unsupported = SnssError::UnsupportedVersion(99);
let msg = format!("{unsupported}");
assert!(msg.contains("unsupported SNSS version 99"));
assert!(msg.contains(&SUPPORTED_VERSION.to_string()));
assert!(unsupported.source().is_none());
let io = SnssError::Io(std::io::Error::other("boom"));
assert!(format!("{io}").contains("I/O error reading SNSS header"));
assert!(io.source().is_some());
}
#[test]
fn short_buffer_reports_bad_magic_padded() {
match read_records(&b"SN"[..]) {
Err(SnssError::BadMagic(got)) => assert_eq!(got, [b'S', b'N', 0, 0]),
other => panic!("expected BadMagic, got {other:?}"),
}
}
#[test]
fn unsupported_version_is_an_error() {
let mut bytes = MAGIC.to_vec();
bytes.extend_from_slice(&7i32.to_le_bytes()); match read_records(&bytes[..]) {
Err(SnssError::UnsupportedVersion(7)) => {}
other => panic!("expected UnsupportedVersion(7), got {other:?}"),
}
}
#[test]
fn partial_size_field_tail_is_a_truncated_warning() {
let mut bytes = MAGIC.to_vec();
bytes.extend_from_slice(&SUPPORTED_VERSION.to_le_bytes());
bytes.push(0x01); let stream = read_records(&bytes[..]).expect("header ok");
assert!(stream.records.is_empty());
assert!(stream
.warnings
.iter()
.any(|w| matches!(w, Warning::TruncatedTail { .. })));
}
#[test]
fn record_size_overrunning_eof_is_a_truncated_warning() {
let mut bytes = MAGIC.to_vec();
bytes.extend_from_slice(&SUPPORTED_VERSION.to_le_bytes());
bytes.extend_from_slice(&999u16.to_le_bytes()); bytes.push(6); let stream = read_records(&bytes[..]).expect("header ok");
assert!(stream
.warnings
.iter()
.any(|w| matches!(w, Warning::TruncatedTail { .. })));
}
#[test]
fn pickle_error_display_all_variants() {
assert!(format!("{}", PickleError::TooShort).contains("too short"));
assert!(format!(
"{}",
PickleError::BadHeader {
declared: 10,
actual: 2
}
)
.contains("declares 10"));
assert!(format!("{}", PickleError::Overrun).contains("runs past"));
assert!(format!("{}", PickleError::BadLength(-1)).contains("negative"));
}
#[test]
fn pickle_too_short_header() {
assert_eq!(decode_navigation(&[0, 1, 2]), Err(PickleError::TooShort));
}
#[test]
fn pickle_bad_header_declares_more_than_present() {
let mut p = 100u32.to_le_bytes().to_vec();
p.extend_from_slice(&[0u8; 4]);
match decode_navigation(&p) {
Err(PickleError::BadHeader { declared, actual }) => {
assert_eq!(declared, 100);
assert_eq!(actual, 4);
}
other => panic!("expected BadHeader, got {other:?}"),
}
}
#[test]
fn pickle_overrun_on_truncated_field() {
let mut p = 4u32.to_le_bytes().to_vec();
p.extend_from_slice(&0i32.to_le_bytes());
assert!(matches!(decode_navigation(&p), Err(PickleError::Overrun)));
}
#[test]
fn pickle_overrun_in_utf16_title() {
let mut body = 0i32.to_le_bytes().to_vec(); body.extend_from_slice(&0i32.to_le_bytes()); body.extend_from_slice(&0i32.to_le_bytes()); body.extend_from_slice(&50i32.to_le_bytes()); let mut p = (body.len() as u32).to_le_bytes().to_vec();
p.extend_from_slice(&body);
assert!(matches!(decode_navigation(&p), Err(PickleError::Overrun)));
}
#[test]
fn pickle_bad_length_negative_string_prefix() {
let mut body = 0i32.to_le_bytes().to_vec(); body.extend_from_slice(&0i32.to_le_bytes()); body.extend_from_slice(&(-5i32).to_le_bytes()); let mut p = (body.len() as u32).to_le_bytes().to_vec();
p.extend_from_slice(&body);
assert!(matches!(
decode_navigation(&p),
Err(PickleError::BadLength(-5))
));
}
#[test]
fn replay_surfaces_bad_navigation_as_warning() {
let bytes = build::snss(&[(6, vec![0, 1, 2])]);
let stream = read_records(&bytes[..]).unwrap();
let replayed = replay(&stream, Dialect::Session);
assert!(replayed
.warnings
.iter()
.any(|w| matches!(w, Warning::BadNavigation { .. })));
}
#[test]
fn replay_ignores_unrecognised_pod_commands() {
let bytes = build::snss(&[(12, vec![0, 0])]); let stream = read_records(&bytes[..]).unwrap();
let replayed = replay(&stream, Dialect::Session);
assert!(replayed.windows.is_empty() || replayed.windows.iter().all(|w| w.tabs.is_empty()));
}
#[test]
fn replay_skips_short_pair_and_last_active_payloads() {
let bytes = build::snss(&[(0, vec![1, 2, 3]), (21, vec![4, 5, 6, 7])]);
let stream = read_records(&bytes[..]).unwrap();
let replayed = replay(&stream, Dialect::Session);
assert!(replayed.windows.iter().all(|w| w.tabs.is_empty()));
}
#[test]
fn replay_discards_pre_epoch_last_active_time() {
let bytes = build::snss(&[
(6, build::nav(7, 0, "https://x.example", "X")),
(0, build::pair(1, 7)), (21, build::last_active(7, 100)), ]);
let stream = read_records(&bytes[..]).unwrap();
let replayed = replay(&stream, Dialect::Session);
assert!(replayed.windows.iter().all(|w| w.last_active.is_none()));
}
#[test]
fn source_kind_labels() {
assert_eq!(SourceKind::Current.label(), "Current Session");
assert_eq!(SourceKind::Last.label(), "Last Session");
assert_eq!(SourceKind::RecentlyClosed.label(), "Recently Closed");
assert_eq!(SourceKind::Apps.label(), "Apps");
}
#[test]
fn open_dir_ignores_unrelated_filenames() {
let dir = tmp_subdir("ignore");
std::fs::write(dir.join("README.txt"), b"not a session file").unwrap();
std::fs::write(dir.join("Session_1"), build::snss(&[])).unwrap();
let store = SessionStore::open_dir(&dir).expect("opens");
assert!(store
.sources()
.iter()
.any(|s| s.kind == SourceKind::Current));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn open_default_profile_errors_when_home_unset() {
let saved = std::env::var_os("HOME");
std::env::remove_var("HOME");
let result = SessionStore::open_default_profile();
if let Some(h) = saved {
std::env::set_var("HOME", h);
}
match result {
Err(SnssError::Io(_)) => {}
other => panic!("expected Io error with HOME unset, got {other:?}"),
}
}