use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::Path;
use hayson_engine::database::{verify, Database, Severity, VerifyOptions};
use serde_json::json;
use tempfile::TempDir;
fn seeded() -> TempDir {
let dir = TempDir::new().unwrap();
let mut database = Database::open(dir.path()).unwrap();
database
.create_table(
"users",
json!({
"schemaVersion": 1,
"table": "users",
"primaryKey": "id",
"fields": { "id": { "type": "string" }, "name": { "type": "string" } },
}),
)
.unwrap();
let users = database.table_mut("users").unwrap();
users.put(&json!({ "id": "u1", "name": "Ania" }), false).unwrap();
users.put(&json!({ "id": "u2", "name": "Bartek" }), false).unwrap();
database.close().unwrap();
dir
}
fn codes(dir: &Path) -> Vec<&'static str> {
verify(dir, &VerifyOptions::default())
.problems
.iter()
.map(|p| p.code)
.collect()
}
fn append(path: &Path, bytes: &[u8]) {
OpenOptions::new()
.append(true)
.open(path)
.unwrap()
.write_all(bytes)
.unwrap();
}
#[test]
fn a_healthy_database_is_clean() {
let dir = seeded();
let report = verify(dir.path(), &VerifyOptions::default());
assert!(report.is_clean(), "{:?}", report.problems);
assert_eq!(report.tables_checked, 1);
assert_eq!(report.log_lines_checked, 2);
}
#[test]
fn a_missing_directory_is_a_finding() {
let dir = TempDir::new().unwrap();
assert_eq!(codes(&dir.path().join("nope")), ["directory-missing"]);
}
#[test]
fn an_empty_directory_only_warns() {
let dir = TempDir::new().unwrap();
let report = verify(dir.path(), &VerifyOptions::default());
assert_eq!(report.problems.len(), 1);
assert_eq!(report.problems[0].code, "catalog-missing");
assert_eq!(report.problems[0].severity, Severity::Warning);
}
#[test]
fn logs_without_a_catalog_are_an_error() {
let dir = seeded();
fs::remove_file(dir.path().join("hayson.json")).unwrap();
let report = verify(dir.path(), &VerifyOptions::default());
assert_eq!(report.problems[0].code, "catalog-missing");
assert_eq!(report.problems[0].severity, Severity::Error);
}
#[test]
fn a_damaged_catalog_is_a_finding_not_a_crash() {
let dir = seeded();
fs::write(dir.path().join("hayson.json"), "{ not json").unwrap();
assert_eq!(codes(dir.path()), ["catalog-invalid-json"]);
fs::write(dir.path().join("hayson.json"), "[1,2]").unwrap();
assert_eq!(codes(dir.path()), ["catalog-invalid"]);
fs::write(dir.path().join("hayson.json"), [0xff, 0xfe]).unwrap();
assert_eq!(codes(dir.path()), ["catalog-unreadable"]);
}
#[test]
fn a_corrupt_log_line_names_its_offset() {
let dir = seeded();
let log = dir.path().join("users.jsonl");
let before = fs::metadata(&log).unwrap().len();
append(&log, b"{broken\n");
let report = verify(dir.path(), &VerifyOptions::default());
let problem = &report.problems[0];
assert_eq!(problem.code, "log-corrupt-line");
assert_eq!(problem.table.as_deref(), Some("users"));
assert_eq!(problem.offset, Some(before));
}
#[test]
fn invalid_utf8_in_a_log_is_a_finding() {
let dir = seeded();
append(&dir.path().join("users.jsonl"), &[0xff, 0xfe, b'\n']);
assert_eq!(codes(dir.path()), ["log-corrupt-line"]);
}
#[test]
fn an_unfinished_tail_is_only_a_warning() {
let dir = seeded();
append(&dir.path().join("users.jsonl"), b"{\"_k\":\"u3\"");
let report = verify(dir.path(), &VerifyOptions::default());
assert_eq!(report.problems.len(), 1);
assert_eq!(report.problems[0].code, "log-truncated-tail");
assert_eq!(report.problems[0].severity, Severity::Warning);
assert!(!report.has_errors());
}
#[test]
fn a_log_the_catalog_does_not_declare_is_reported() {
let dir = seeded();
fs::write(dir.path().join("ghost.jsonl"), "").unwrap();
assert_eq!(codes(dir.path()), ["log-undeclared"]);
}
#[test]
fn verify_changes_nothing_on_disk() {
let dir = seeded();
append(&dir.path().join("users.jsonl"), b"{\"_k\":\"u3\"");
let snapshot = |p: &Path| {
let mut files: Vec<_> = fs::read_dir(p)
.unwrap()
.map(|e| e.unwrap().path())
.map(|path| (path.clone(), fs::read(&path).unwrap_or_default()))
.collect();
files.sort();
files
};
let before = snapshot(dir.path());
verify(dir.path(), &VerifyOptions::default());
assert_eq!(snapshot(dir.path()), before);
}