use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};
use assert_cmd::Command;
use obj::{Db, Document, IndexSpec};
use obj_core::pager::page::PAGE_SIZE;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use serde::{Deserialize, Serialize};
use tempfile::TempDir;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct User {
email: String,
handle: String,
}
impl Document for User {
const COLLECTION: &'static str = "users";
const VERSION: u32 = 1;
fn indexes() -> Vec<IndexSpec> {
vec![IndexSpec::unique("by_email", "email").expect("static spec")]
}
}
fn populate(path: &std::path::Path, n: u32) {
let db = Db::open(path).expect("open");
for i in 0..n {
db.insert(User {
email: format!("user{i}@example.com"),
handle: format!("u{i}"),
})
.expect("insert");
}
}
#[test]
fn check_clean_db_exits_zero() {
let dir = TempDir::new().expect("tmp");
let path = dir.path().join("clean.obj");
populate(&path, 16);
Command::cargo_bin("obj")
.expect("binary")
.arg("check")
.arg(&path)
.assert()
.success()
.stdout(contains("ok:"));
}
#[test]
fn check_corrupted_db_exits_one() {
let dir = TempDir::new().expect("tmp");
let path = dir.path().join("corrupted.obj");
populate(&path, 32);
checkpoint_and_close(&path).expect("checkpoint");
let target_pid = locate_primary_page(&path).expect("primary");
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.expect("open for corruption");
let target_off = (target_pid * PAGE_SIZE as u64) + 64;
file.seek(SeekFrom::Start(target_off)).expect("seek");
file.write_all(&[0xFFu8]).expect("flip");
drop(file);
Command::cargo_bin("obj")
.expect("binary")
.arg("check")
.arg(&path)
.assert()
.code(1)
.stderr(
contains("ChecksumMismatch")
.or(contains("BTreeSortViolation"))
.or(contains("Corruption")),
);
}
#[test]
fn check_missing_path_exits_two() {
let dir = TempDir::new().expect("tmp");
let missing = dir.path().join("no-such-dir").join("does-not-exist.obj");
Command::cargo_bin("obj")
.expect("binary")
.arg("check")
.arg(&missing)
.assert()
.code(2)
.stderr(contains("error:"));
}
fn checkpoint_and_close(path: &std::path::Path) -> obj::Result<()> {
use obj_core::pager::{Config, Pager};
use obj_core::platform::FileHandle;
let pager = Pager::<FileHandle>::open(path, Config::default())?;
pager.close()
}
fn locate_primary_page(path: &std::path::Path) -> obj::Result<u64> {
use obj_core::pager::{Config, Pager};
use obj_core::platform::FileHandle;
use obj_core::Catalog;
let mut pager = Pager::<FileHandle>::open(path, Config::default())?;
pager.begin_txn();
let catalog = Catalog::<FileHandle>::open_or_init(&mut pager)?;
let descriptor = catalog.get(&mut pager, "users")?.expect("users present");
pager.end_txn();
Ok(descriptor.primary_root)
}