use anyhow::{Context, Result};
use db185::{Db, Writer};
use tempfile::TempDir;
#[test]
fn empty_round_trip() -> Result<()> {
let dir = TempDir::new()?;
let path = dir.path().join("empty.db");
Writer::create_new(&path)?.finish()?;
let db = Db::open(&path)?;
assert!(
db.iter().next().is_none(),
"fresh db should yield no entries"
);
assert!(db.get(b"any\0")?.is_none(), "fresh db should have no keys");
Ok(())
}
#[test]
fn put_round_trip_single_entry() -> Result<()> {
let dir = TempDir::new()?;
let path = dir.path().join("one.db");
let mut w = Writer::create_new(&path)?;
assert!(w.put(b"hello\0", b"world\0")?);
w.finish()?;
let db = Db::open(&path)?;
let val = db.get(b"hello\0")?.context("hello should be present")?;
assert_eq!(val.as_ref(), b"world\0");
assert!(db.get(b"missing\0")?.is_none());
Ok(())
}
#[test]
fn put_no_overwrite() -> Result<()> {
let dir = TempDir::new()?;
let path = dir.path().join("dup.db");
let mut w = Writer::create_new(&path)?;
assert!(w.put(b"k\0", b"v1\0")?);
assert!(!w.put(b"k\0", b"v2\0")?);
w.finish()?;
let db = Db::open(&path)?;
let val = db.get(b"k\0")?.context("k should be present")?;
assert_eq!(val.as_ref(), b"v1\0", "first put wins under R_NOOVERWRITE");
Ok(())
}
#[test]
fn matches_pkg_admin_single_entry() -> Result<()> {
const REFERENCE: &[u8] = include_bytes!("data/single_entry.db");
let dir = TempDir::new()?;
let path = dir.path().join("ours.db");
let mut w = Writer::create_new(&path)?;
assert!(w.put(b"/tmp/hello\0", b"foo-1.0\0")?);
w.finish()?;
let ours = std::fs::read(&path)?;
assert_eq!(
ours, REFERENCE,
"writer output differs from pkg_admin rebuild reference"
);
Ok(())
}
#[test]
fn matches_pkg_admin_one_split() -> Result<()> {
const REFERENCE: &[u8] = include_bytes!("data/one_split.db");
const PREFIX: &str = "/tmp/onesplit-files/path/to/some/longer/directory/";
let dir = TempDir::new()?;
let path = dir.path().join("ours.db");
let mut w = Writer::create_new(&path)?;
for i in 1..=39 {
let mut key = PREFIX.as_bytes().to_vec();
key.extend_from_slice(format!("file_with_a_somewhat_long_name_{i:03}").as_bytes());
key.push(0);
assert!(w.put(&key, b"big-1.0\0")?);
}
w.finish()?;
let ours = std::fs::read(&path)?;
assert_eq!(ours.len(), REFERENCE.len(), "writer output length differs");
assert_eq!(
ours, REFERENCE,
"writer output differs from pkg_admin rebuild reference"
);
Ok(())
}
#[test]
fn delete_then_split_via_reader() -> Result<()> {
let dir = TempDir::new()?;
let path = dir.path().join("del_then_split.db");
let mut w = Writer::create_new(&path)?;
let prefix = "/tmp/del-split/path/to/some/directory/file_";
let mut keys: Vec<Vec<u8>> = Vec::new();
for i in 0..200 {
let mut k = prefix.as_bytes().to_vec();
k.extend_from_slice(format!("{i:04}_with_padding_to_force_splits").as_bytes());
k.push(0);
assert!(w.put(&k, b"pkg-1.0\0")?);
keys.push(k);
}
for (i, k) in keys.iter().enumerate() {
if i % 7 == 0 {
assert!(w.del(k)?);
}
}
for (i, k) in keys.iter().enumerate() {
if i % 7 == 0 {
assert!(w.put(k, b"pkg-2.0-replacement\0")?);
}
}
w.finish()?;
let db = Db::open(&path)?;
let mut prev: Option<Vec<u8>> = None;
let mut count = 0usize;
for entry in &db {
let entry = entry?;
if let Some(p) = prev.as_ref() {
assert!(p.as_slice() < entry.key(), "keys must iterate sorted");
}
prev = Some(entry.key().to_vec());
count += 1;
}
assert_eq!(count, 200, "delete + re-insert preserves count");
Ok(())
}
#[test]
fn sorted_append_grows_rightmost_leaf_chain() -> Result<()> {
let dir = TempDir::new()?;
let path = dir.path().join("sorted.db");
let mut w = Writer::create_new(&path)?;
for i in 0..2000u32 {
let key = format!("/sorted/{i:08}_some_padding_to_take_real_space\0");
let val = format!("pkg-{i}\0");
assert!(w.put(key.as_bytes(), val.as_bytes())?);
}
w.finish()?;
let db = Db::open(&path)?;
let mut last: Option<u32> = None;
let mut count = 0usize;
for entry in &db {
let entry = entry?;
let key = std::str::from_utf8(entry.key())?.trim_end_matches('\0');
let idx: u32 = key
.trim_start_matches("/sorted/")
.get(..8)
.context("8-digit index field")?
.parse()
.context("parse idx")?;
if let Some(p) = last {
assert_eq!(idx, p + 1, "sorted iteration must visit indices in order");
}
last = Some(idx);
count += 1;
}
assert_eq!(count, 2000);
Ok(())
}
#[test]
fn put_oversize_entry_returns_clean_error() -> Result<()> {
use db185::Error;
let dir = TempDir::new()?;
let path = dir.path().join("toobig.db");
let mut w = Writer::create_new(&path)?;
let huge = vec![b'x'; 4080];
let err = w.put(&huge, b"v\0").unwrap_err();
assert!(
matches!(err, Error::EntryTooLarge { .. }),
"expected EntryTooLarge, got {err:?}",
);
w.finish()?;
Ok(())
}
#[test]
fn put_leaf_fittable_but_separator_too_large_errors() -> Result<()> {
use db185::Error;
let dir = TempDir::new()?;
let path = dir.path().join("sep.db");
let mut w = Writer::create_new(&path)?;
let huge_key = vec![b'k'; 4060];
let err = w.put(&huge_key, b"v\0").unwrap_err();
assert!(
matches!(err, Error::EntryTooLarge { .. }),
"expected EntryTooLarge for separator-overflowing key, got {err:?}",
);
w.finish()?;
Ok(())
}