use chisel::{Chisel, ChiselError, Options};
use tempfile::NamedTempFile;
mod common;
use common::{open_chisel, Backing};
fn test_allocate_without_begin_errors_body(b: &Backing) {
let mut db = open_chisel(b);
let err = db.allocate(b"x").unwrap_err();
assert!(matches!(err, ChiselError::NoActiveTransaction));
}
dual_backing_test!(
test_allocate_without_begin_errors,
test_allocate_without_begin_errors_body
);
fn test_update_without_begin_errors_body(b: &Backing) {
let mut db = open_chisel(b);
let err = db.update(chisel::Handle::from(0), b"x").unwrap_err();
assert!(matches!(err, ChiselError::NoActiveTransaction));
}
dual_backing_test!(
test_update_without_begin_errors,
test_update_without_begin_errors_body
);
fn test_delete_without_begin_errors_body(b: &Backing) {
let mut db = open_chisel(b);
let err = db.delete(chisel::Handle::from(0)).unwrap_err();
assert!(matches!(err, ChiselError::NoActiveTransaction));
}
dual_backing_test!(
test_delete_without_begin_errors,
test_delete_without_begin_errors_body
);
fn test_delete_many_without_begin_errors_body(b: &Backing) {
let mut db = open_chisel(b);
let err = db
.delete_many(&[
chisel::Handle::from(0),
chisel::Handle::from(1),
chisel::Handle::from(2),
])
.unwrap_err();
assert!(matches!(err, ChiselError::NoActiveTransaction));
}
dual_backing_test!(
test_delete_many_without_begin_errors,
test_delete_many_without_begin_errors_body
);
fn test_commit_without_begin_errors_body(b: &Backing) {
let mut db = open_chisel(b);
let err = db.commit().unwrap_err();
assert!(matches!(err, ChiselError::NoActiveTransaction));
}
dual_backing_test!(
test_commit_without_begin_errors,
test_commit_without_begin_errors_body
);
fn test_rollback_without_begin_errors_body(b: &Backing) {
let mut db = open_chisel(b);
let err = db.rollback().unwrap_err();
assert!(matches!(err, ChiselError::NoActiveTransaction));
}
dual_backing_test!(
test_rollback_without_begin_errors,
test_rollback_without_begin_errors_body
);
fn test_savepoint_without_begin_errors_body(b: &Backing) {
let mut db = open_chisel(b);
let err = db.savepoint("sp").unwrap_err();
assert!(matches!(err, ChiselError::NoActiveTransaction));
}
dual_backing_test!(
test_savepoint_without_begin_errors,
test_savepoint_without_begin_errors_body
);
fn test_set_root_name_without_begin_errors_body(b: &Backing) {
let mut db = open_chisel(b);
let err = db
.set_root_name("meta", chisel::Handle::from(0))
.unwrap_err();
assert!(matches!(err, ChiselError::NoActiveTransaction));
}
dual_backing_test!(
test_set_root_name_without_begin_errors,
test_set_root_name_without_begin_errors_body
);
fn test_clear_root_name_without_begin_errors_body(b: &Backing) {
let mut db = open_chisel(b);
let err = db.clear_root_name("meta").unwrap_err();
assert!(matches!(err, ChiselError::NoActiveTransaction));
}
dual_backing_test!(
test_clear_root_name_without_begin_errors,
test_clear_root_name_without_begin_errors_body
);
fn test_begin_twice_errors_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let err = db.begin().unwrap_err();
assert!(matches!(err, ChiselError::TransactionAlreadyActive));
}
dual_backing_test!(test_begin_twice_errors, test_begin_twice_errors_body);
fn test_update_invalid_handle_errors_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let err = db
.update(chisel::Handle::from(999_999), b"nope")
.unwrap_err();
assert!(matches!(err, ChiselError::InvalidHandle(999_999)));
assert!(!db.is_poisoned());
db.rollback().unwrap();
}
dual_backing_test!(
test_update_invalid_handle_errors,
test_update_invalid_handle_errors_body
);
fn test_delete_invalid_handle_errors_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let err = db.delete(chisel::Handle::from(42)).unwrap_err();
assert!(matches!(err, ChiselError::InvalidHandle(42)));
assert!(!db.is_poisoned());
db.rollback().unwrap();
}
dual_backing_test!(
test_delete_invalid_handle_errors,
test_delete_invalid_handle_errors_body
);
fn test_read_invalid_handle_on_empty_db_errors_body(b: &Backing) {
let db = open_chisel(b);
let err = db.read(chisel::Handle::from(0)).unwrap_err();
assert!(matches!(err, ChiselError::InvalidHandle(0)));
assert!(!db.is_poisoned());
}
dual_backing_test!(
test_read_invalid_handle_on_empty_db_errors,
test_read_invalid_handle_on_empty_db_errors_body
);
fn test_read_out_of_range_handle_does_not_alias_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let mut handles = Vec::new();
for i in 0..100u64 {
handles.push(db.allocate(format!("value-{i}").as_bytes()).unwrap());
}
db.commit().unwrap();
let live = handles[5];
let aliasing = chisel::Handle::from(u64::from(live) + 510);
let err = db.read(aliasing).unwrap_err();
assert!(
matches!(err, ChiselError::InvalidHandle(_)),
"out-of-range handle must be InvalidHandle, got {err:?}"
);
assert_eq!(db.read(live).unwrap(), b"value-5");
assert!(!db.is_poisoned());
}
dual_backing_test!(
test_read_out_of_range_handle_does_not_alias,
test_read_out_of_range_handle_does_not_alias_body
);
fn test_allocate_empty_value_roundtrip_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let h = db.allocate(b"").unwrap();
db.commit().unwrap();
let got = db.read(h).unwrap();
assert!(got.is_empty());
}
dual_backing_test!(
test_allocate_empty_value_roundtrip,
test_allocate_empty_value_roundtrip_body
);
fn test_update_to_empty_value_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let h = db.allocate(b"not empty").unwrap();
db.update(h, b"").unwrap();
db.commit().unwrap();
assert!(db.read(h).unwrap().is_empty());
}
dual_backing_test!(test_update_to_empty_value, test_update_to_empty_value_body);
fn test_value_at_inline_max_boundary_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let v = vec![0x5Au8; 8162];
let h = db.allocate(&v).unwrap();
db.commit().unwrap();
assert_eq!(db.read(h).unwrap(), v);
}
dual_backing_test!(
test_value_at_inline_max_boundary,
test_value_at_inline_max_boundary_body
);
fn test_value_one_byte_over_inline_goes_overflow_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let v = vec![0x5Au8; 8163];
let h = db.allocate(&v).unwrap();
db.commit().unwrap();
assert_eq!(db.read(h).unwrap(), v);
}
dual_backing_test!(
test_value_one_byte_over_inline_goes_overflow,
test_value_one_byte_over_inline_goes_overflow_body
);
fn test_handle_stable_across_inline_overflow_transition_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let h = db.allocate(b"small").unwrap();
db.commit().unwrap();
let big = vec![0xC3u8; 20_000];
db.begin().unwrap();
db.update(h, &big).unwrap();
db.commit().unwrap();
assert_eq!(db.read(h).unwrap(), big);
db.begin().unwrap();
db.update(h, b"small again").unwrap();
db.commit().unwrap();
assert_eq!(db.read(h).unwrap(), b"small again");
}
dual_backing_test!(
test_handle_stable_across_inline_overflow_transition,
test_handle_stable_across_inline_overflow_transition_body
);
fn test_delete_many_empty_slice_is_noop_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let h = db.allocate(b"still here").unwrap();
db.delete_many(&[]).unwrap();
db.commit().unwrap();
assert_eq!(db.read(h).unwrap(), b"still here");
}
dual_backing_test!(
test_delete_many_empty_slice_is_noop,
test_delete_many_empty_slice_is_noop_body
);
fn test_delete_many_bad_handle_mid_batch_stops_at_first_error_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let h1 = db.allocate(b"a").unwrap();
let h2 = db.allocate(b"b").unwrap();
let h3 = db.allocate(b"c").unwrap();
db.commit().unwrap();
db.begin().unwrap();
let err = db
.delete_many(&[h1, chisel::Handle::from(999_999), h3])
.unwrap_err();
assert!(matches!(err, ChiselError::InvalidHandle(999_999)));
db.rollback().unwrap();
assert_eq!(db.read(h1).unwrap(), b"a");
assert_eq!(db.read(h2).unwrap(), b"b");
assert_eq!(db.read(h3).unwrap(), b"c");
}
dual_backing_test!(
test_delete_many_bad_handle_mid_batch_stops_at_first_error,
test_delete_many_bad_handle_mid_batch_stops_at_first_error_body
);
fn test_duplicate_savepoint_rejected_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
db.savepoint("sp").unwrap();
let err = db.savepoint("sp").unwrap_err();
assert!(matches!(err, ChiselError::DuplicateSavepoint(ref n) if n == "sp"));
}
dual_backing_test!(
test_duplicate_savepoint_rejected,
test_duplicate_savepoint_rejected_body
);
fn test_rollback_to_missing_savepoint_errors_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let err = db.rollback_to("nope").unwrap_err();
assert!(matches!(err, ChiselError::SavepointNotFound(ref n) if n == "nope"));
}
dual_backing_test!(
test_rollback_to_missing_savepoint_errors,
test_rollback_to_missing_savepoint_errors_body
);
fn test_release_missing_savepoint_errors_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let err = db.release("nope").unwrap_err();
assert!(matches!(err, ChiselError::SavepointNotFound(ref n) if n == "nope"));
}
dual_backing_test!(
test_release_missing_savepoint_errors,
test_release_missing_savepoint_errors_body
);
fn test_rollback_to_released_savepoint_errors_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
db.savepoint("sp").unwrap();
db.release("sp").unwrap();
let err = db.rollback_to("sp").unwrap_err();
assert!(matches!(err, ChiselError::SavepointNotFound(ref n) if n == "sp"));
}
dual_backing_test!(
test_rollback_to_released_savepoint_errors,
test_rollback_to_released_savepoint_errors_body
);
fn test_fresh_handle_is_not_poisoned_body(b: &Backing) {
let db = open_chisel(b);
assert!(!db.is_poisoned());
}
dual_backing_test!(
test_fresh_handle_is_not_poisoned,
test_fresh_handle_is_not_poisoned_body
);
#[test]
fn test_reopen_after_drop_observes_only_committed_data() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_owned();
let committed_handle;
{
let mut db = Chisel::open(&path, Options::default()).unwrap();
db.begin().unwrap();
committed_handle = db.allocate(b"committed").unwrap();
db.commit().unwrap();
db.begin().unwrap();
db.allocate(b"uncommitted").unwrap();
}
let db = Chisel::open(&path, Options::default()).unwrap();
assert!(!db.is_poisoned());
assert_eq!(db.read(committed_handle).unwrap(), b"committed");
assert_eq!(db.stats().unwrap().handle_count, 1);
}
fn test_stats_empty_db_has_zero_handles_body(b: &Backing) {
let db = open_chisel(b);
let s = db.stats().unwrap();
assert_eq!(s.handle_count, 0);
assert_eq!(s.file_size_bytes % chisel::PAGE_SIZE as u64, 0);
assert_eq!(s.file_size_bytes, s.total_pages * chisel::PAGE_SIZE as u64);
}
dual_backing_test!(
test_stats_empty_db_has_zero_handles,
test_stats_empty_db_has_zero_handles_body
);
fn test_stats_handle_count_matches_handles_len_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
for i in 0..25u32 {
db.allocate(&i.to_le_bytes()).unwrap();
}
db.commit().unwrap();
let s = db.stats().unwrap();
assert_eq!(s.handle_count, 25);
}
dual_backing_test!(
test_stats_handle_count_matches_handles_len,
test_stats_handle_count_matches_handles_len_body
);
fn test_stats_is_idempotent_without_mutation_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
db.allocate(b"x").unwrap();
db.allocate(b"y").unwrap();
db.commit().unwrap();
let s1 = db.stats().unwrap();
let s2 = db.stats().unwrap();
assert_eq!(s1.handle_count, s2.handle_count);
assert_eq!(s1.total_pages, s2.total_pages);
assert_eq!(s1.file_size_bytes, s2.file_size_bytes);
}
dual_backing_test!(
test_stats_is_idempotent_without_mutation,
test_stats_is_idempotent_without_mutation_body
);
fn test_file_size_bytes_matches_stats_body(b: &Backing) {
let mut db = open_chisel(b);
assert_eq!(
db.file_size_bytes().unwrap(),
db.stats().unwrap().file_size_bytes,
"file_size_bytes must match stats().file_size_bytes on fresh db"
);
db.begin().unwrap();
for i in 0..20u32 {
db.allocate(&i.to_le_bytes()).unwrap();
}
assert_eq!(
db.file_size_bytes().unwrap(),
db.stats().unwrap().file_size_bytes,
"file_size_bytes must match stats().file_size_bytes mid-transaction"
);
db.commit().unwrap();
assert_eq!(
db.file_size_bytes().unwrap(),
db.stats().unwrap().file_size_bytes,
"file_size_bytes must match stats().file_size_bytes post-commit"
);
}
dual_backing_test!(
test_file_size_bytes_matches_stats,
test_file_size_bytes_matches_stats_body
);