use super::*;
use crate::native::core::ChoiceValue;
use tempfile::TempDir;
fn fresh_db() -> (DirectoryTestCaseDatabase, TempDir) {
let dir = TempDir::new().unwrap();
let db = DirectoryTestCaseDatabase::new(dir.path().to_str().unwrap());
(db, dir)
}
#[test]
fn save_then_fetch_round_trips_a_value() {
let (db, _dir) = fresh_db();
db.save(b"key", b"value");
let fetched = db.fetch(b"key");
assert_eq!(fetched, vec![b"value".to_vec()]);
}
#[test]
fn save_is_idempotent() {
let (db, _dir) = fresh_db();
db.save(b"key", b"value");
db.save(b"key", b"value");
assert_eq!(db.fetch(b"key"), vec![b"value".to_vec()]);
}
#[test]
fn delete_removes_a_saved_value() {
let (db, _dir) = fresh_db();
db.save(b"key", b"value");
db.delete(b"key", b"value");
assert!(db.fetch(b"key").is_empty());
}
#[test]
fn delete_of_absent_value_is_silent() {
let (db, _dir) = fresh_db();
db.delete(b"key", b"nonexistent"); assert!(db.fetch(b"key").is_empty());
}
#[test]
fn fetch_of_absent_key_returns_empty() {
let (db, _dir) = fresh_db();
assert!(db.fetch(b"never-saved").is_empty());
}
#[test]
fn move_value_within_same_key_is_a_resave() {
let (db, _dir) = fresh_db();
db.save(b"key", b"v");
db.move_value(b"key", b"key", b"v");
assert_eq!(db.fetch(b"key"), vec![b"v".to_vec()]);
}
#[test]
fn move_value_relocates_entry() {
let (db, _dir) = fresh_db();
db.save(b"src", b"v");
db.move_value(b"src", b"dst", b"v");
assert!(db.fetch(b"src").is_empty());
assert_eq!(db.fetch(b"dst"), vec![b"v".to_vec()]);
}
#[test]
fn move_value_creates_dst_when_absent() {
let (db, _dir) = fresh_db();
db.save(b"src", b"v");
db.move_value(b"src", b"brand-new-dst", b"v");
assert_eq!(db.fetch(b"brand-new-dst"), vec![b"v".to_vec()]);
}
#[test]
fn move_value_falls_back_to_delete_save_when_rename_fails() {
let (db, _dir) = fresh_db();
db.save(b"dst", b"existing"); db.move_value(b"src", b"dst", b"v");
let mut got = db.fetch(b"dst");
got.sort();
let mut want = vec![b"existing".to_vec(), b"v".to_vec()];
want.sort();
assert_eq!(got, want);
}
#[test]
fn round_trip_integer_choices() {
let choices = vec![
ChoiceValue::Integer(0),
ChoiceValue::Integer(-1),
ChoiceValue::Integer(i128::MAX),
ChoiceValue::Integer(i128::MIN),
];
let bytes = serialize_choices(&choices);
assert_eq!(deserialize_choices(&bytes), Some(choices));
}
#[test]
fn round_trip_mixed_choices() {
let choices = vec![
ChoiceValue::Boolean(false),
ChoiceValue::Integer(42),
ChoiceValue::Boolean(true),
];
let bytes = serialize_choices(&choices);
assert_eq!(deserialize_choices(&bytes), Some(choices));
}
#[test]
fn deserialize_returns_none_on_too_short_header() {
assert!(deserialize_choices(&[0, 0, 0]).is_none());
}
#[test]
fn deserialize_returns_none_on_missing_type_tag() {
assert!(deserialize_choices(&1u32.to_le_bytes()).is_none());
}
#[test]
fn deserialize_returns_none_on_truncated_integer_body() {
let mut bytes = 1u32.to_le_bytes().to_vec();
bytes.push(0); bytes.extend_from_slice(&[0u8; 8]); assert!(deserialize_choices(&bytes).is_none());
}
#[test]
fn deserialize_returns_none_on_truncated_boolean_body() {
let mut bytes = 1u32.to_le_bytes().to_vec();
bytes.push(1); assert!(deserialize_choices(&bytes).is_none());
}
#[test]
fn deserialize_returns_none_on_unknown_type_tag() {
let mut bytes = 1u32.to_le_bytes().to_vec();
bytes.push(42); assert!(deserialize_choices(&bytes).is_none());
}
#[test]
fn round_trip_float_choices_preserves_bit_pattern() {
let choices = vec![
ChoiceValue::Float(0.0),
ChoiceValue::Float(-0.0),
ChoiceValue::Float(1.5),
ChoiceValue::Float(-1.5),
ChoiceValue::Float(f64::INFINITY),
ChoiceValue::Float(f64::NEG_INFINITY),
ChoiceValue::Float(f64::NAN),
ChoiceValue::Float(f64::MAX),
ChoiceValue::Float(f64::MIN_POSITIVE),
];
let bytes = serialize_choices(&choices);
let round_tripped = deserialize_choices(&bytes).unwrap();
assert_eq!(round_tripped.len(), choices.len());
for (got, want) in round_tripped.iter().zip(choices.iter()) {
match (got, want) {
(ChoiceValue::Float(g), ChoiceValue::Float(w)) => {
assert_eq!(g.to_bits(), w.to_bits(), "bit pattern mismatch");
}
_ => panic!("non-float variant produced"),
}
}
}
#[test]
fn deserialize_returns_none_on_truncated_float_body() {
let mut bytes = 1u32.to_le_bytes().to_vec();
bytes.push(2); bytes.extend_from_slice(&[0u8; 4]); assert!(deserialize_choices(&bytes).is_none());
}
#[test]
fn round_trip_bytes_choices() {
let choices = vec![
ChoiceValue::Bytes(Vec::new()),
ChoiceValue::Bytes(vec![0u8]),
ChoiceValue::Bytes(vec![0xff, 0x00, 0x80, 0x7f]),
ChoiceValue::Bytes(vec![1; 1024]),
];
let bytes = serialize_choices(&choices);
assert_eq!(deserialize_choices(&bytes), Some(choices));
}
#[test]
fn deserialize_returns_none_on_truncated_bytes_length() {
let mut bytes = 1u32.to_le_bytes().to_vec();
bytes.push(3);
bytes.extend_from_slice(&[0u8; 2]); assert!(deserialize_choices(&bytes).is_none());
}
#[test]
fn deserialize_returns_none_on_truncated_bytes_body() {
let mut bytes = 1u32.to_le_bytes().to_vec();
bytes.push(3);
bytes.extend_from_slice(&5u32.to_le_bytes()); bytes.extend_from_slice(&[0u8; 2]);
assert!(deserialize_choices(&bytes).is_none());
}
#[test]
fn round_trip_string_choices() {
let choices = vec![
ChoiceValue::String(Vec::new()),
ChoiceValue::String(vec![b'0' as u32]),
ChoiceValue::String(vec![b'a' as u32, b'b' as u32, b'c' as u32]),
ChoiceValue::String(vec![0x2603, 0x1F600, 0]),
];
let bytes = serialize_choices(&choices);
assert_eq!(deserialize_choices(&bytes), Some(choices));
}
#[test]
fn deserialize_returns_none_on_truncated_string_length() {
let mut bytes = 1u32.to_le_bytes().to_vec();
bytes.push(4);
bytes.extend_from_slice(&[0u8; 2]); assert!(deserialize_choices(&bytes).is_none());
}
#[test]
fn deserialize_returns_none_on_truncated_string_body() {
let mut bytes = 1u32.to_le_bytes().to_vec();
bytes.push(4);
bytes.extend_from_slice(&3u32.to_le_bytes());
bytes.extend_from_slice(&[0u8; 4]); assert!(deserialize_choices(&bytes).is_none());
}
#[cfg(unix)]
#[test]
fn save_silently_returns_when_create_dir_all_fails() {
let dir = TempDir::new().unwrap();
let file_root = dir.path().join("im-a-file");
std::fs::write(&file_root, b"").unwrap();
let db = DirectoryTestCaseDatabase::new(file_root.to_str().unwrap());
db.save(b"key", b"value");
assert!(db.fetch(b"key").is_empty());
}
#[cfg(unix)]
#[test]
fn move_value_falls_back_to_delete_save_when_dst_dir_create_fails() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let db = DirectoryTestCaseDatabase::new(dir.path().to_str().unwrap());
db.save(b"src", b"val");
let mut perms = std::fs::metadata(dir.path()).unwrap().permissions();
perms.set_mode(0o555);
std::fs::set_permissions(dir.path(), perms.clone()).unwrap();
db.move_value(b"src", b"dst", b"val");
perms.set_mode(0o755);
std::fs::set_permissions(dir.path(), perms).unwrap();
}