obj-cli 1.0.0

Command-line tools (dump, check, stat, backup) for the obj embedded document database.
Documentation
//! End-to-end CLI smoke (issue #100).
//!
//! Mirrors `scripts/cli-smoke.sh` in pure Rust via `assert_cmd` so
//! `cargo test --workspace` exercises the same trio of paths on
//! every OS the CI matrix covers (Linux + macOS + Windows). The
//! shell script is the canonical local reproduction; this file is
//! its CI-friendly twin.

use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};

use assert_cmd::Command;
use obj::{Db, Document};
use obj_core::pager::page::PAGE_SIZE;
use predicates::str::contains;
use serde::{Deserialize, Serialize};
use tempfile::TempDir;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct SmokeUser {
    email: String,
    handle: String,
}

impl Document for SmokeUser {
    const COLLECTION: &'static str = "smoke_users";
    const VERSION: u32 = 1;
}

fn seed(path: &std::path::Path) {
    let db = Db::open(path).expect("open source");
    for i in 0..8u32 {
        db.insert(SmokeUser {
            email: format!("user{i}@example.com"),
            handle: format!("u{i}"),
        })
        .expect("insert");
    }
}

#[test]
fn cli_smoke_clean_then_corrupted() {
    let dir = TempDir::new().expect("tmp");
    let db_path = dir.path().join("smoke.obj");
    let backup_path = dir.path().join("backup.obj");
    seed(&db_path);

    // obj stat — succeeds, surfaces the seeded collection.
    Command::cargo_bin("obj")
        .expect("binary")
        .arg("stat")
        .arg(&db_path)
        .assert()
        .success()
        .stdout(contains("## smoke_users"))
        .stdout(contains("doc_count: 8"));

    // obj check — clean DB exits zero.
    Command::cargo_bin("obj")
        .expect("binary")
        .arg("check")
        .arg(&db_path)
        .assert()
        .success()
        .stdout(contains("ok:"));

    // obj backup — destination must pass its own integrity check.
    Command::cargo_bin("obj")
        .expect("binary")
        .arg("backup")
        .arg(&db_path)
        .arg(&backup_path)
        .assert()
        .success();
    Command::cargo_bin("obj")
        .expect("binary")
        .arg("check")
        .arg(&backup_path)
        .assert()
        .success()
        .stdout(contains("ok:"));

    // Hand-corrupt the primary tree root page (same recipe as
    // `examples/corrupt_one_byte.rs`, inlined here so the test
    // does not depend on the example binary being on PATH).
    corrupt_one_byte(&db_path);

    Command::cargo_bin("obj")
        .expect("binary")
        .arg("check")
        .arg(&db_path)
        .assert()
        .code(1)
        .stderr(contains("ChecksumMismatch"));
}

fn corrupt_one_byte(path: &std::path::Path) {
    checkpoint_and_close(path).expect("checkpoint");
    let target_pid = locate_primary_root(path).expect("locate primary root");
    let mut file = OpenOptions::new()
        .read(true)
        .write(true)
        .open(path)
        .expect("open for corruption");
    let target_off = target_pid
        .saturating_mul(PAGE_SIZE as u64)
        .saturating_add(64);
    file.seek(SeekFrom::Start(target_off)).expect("seek");
    file.write_all(&[0xFFu8]).expect("flip");
}

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_root(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, "smoke_users")?
        .expect("smoke_users present");
    pager.end_txn();
    Ok(descriptor.primary_root)
}