use std::error::Error;
use crate::db::Database;
use crate::tree::TreePolicy;
use super::test_support::{
Entries, build_v1_branch, build_v1_fixture, format_version, fresh_build, is_fenced, oversized,
oversized_shards, read_all_roots, read_branch_head, read_shard_root, v2_default,
};
use super::{MigrateOptions, MigrationDirection, MigrationOutcome, migrate_chunking};
type TestResult = Result<(), Box<dyn Error>>;
#[test]
fn v1_fixture_opens_as_v1_without_rewrite() -> TestResult {
let dir = tempfile::tempdir()?;
let data = dir.path().join("db");
let shards = oversized_shards(2, 3);
let before = build_v1_fixture(&data, &shards)?;
let opened = Database::open(&data)?;
assert_eq!(
opened.tree_policy(),
TreePolicy::V1_DEFAULT,
"a v1 directory must open under the v1 policy"
);
drop(opened);
let after = read_all_roots(&data, shards.len())?;
assert_eq!(
after,
before.iter().copied().map(Some).collect::<Vec<_>>(),
"opening a v1 directory must not rewrite any shard root"
);
assert_eq!(
format_version(&data)?,
Some(1),
"open must not restamp the v1 format version"
);
Ok(())
}
#[test]
fn migrate_equals_fresh_build_canonical() -> TestResult {
let dir = tempfile::tempdir()?;
let data = dir.path().join("db");
let shards = oversized_shards(2, 3);
let v1_roots = build_v1_fixture(&data, &shards)?;
for (index, entries) in shards.iter().enumerate() {
assert_ne!(
v1_roots[index],
fresh_build(entries, v2_default())?,
"shard {index}: v2 shape must differ from v1 for the witness to bite"
);
}
let report = migrate_chunking(&MigrateOptions::migrate(data.clone()))?;
assert_eq!(report.outcome, MigrationOutcome::MigratedToV2);
assert_eq!(report.direction, MigrationDirection::Forward);
assert_eq!(
report.shards_rebuilt,
shards.len(),
"every shard re-chunked"
);
for (index, entries) in shards.iter().enumerate() {
assert_eq!(
read_shard_root(&data, index)?,
Some(fresh_build(entries, v2_default())?),
"shard {index}: migrated root must equal the fresh-build canonical v2 root"
);
}
assert!(
!is_fenced(&data)?,
"a completed migration removes the fence"
);
assert_eq!(format_version(&data)?, Some(2), "the directory is now v2");
Ok(())
}
#[test]
fn migrate_twice_is_byte_identical() -> TestResult {
let dir = tempfile::tempdir()?;
let data = dir.path().join("db");
let shards = oversized_shards(2, 3);
build_v1_fixture(&data, &shards)?;
migrate_chunking(&MigrateOptions::migrate(data.clone()))?;
let once = read_all_roots(&data, shards.len())?;
let again = migrate_chunking(&MigrateOptions::migrate(data.clone()))?;
assert_eq!(
again.outcome,
MigrationOutcome::AlreadyInTargetFormat,
"a second migrate of a v2 directory is a no-op"
);
assert_eq!(
read_all_roots(&data, shards.len())?,
once,
"migrating twice must leave the roots byte-identical"
);
Ok(())
}
#[test]
fn migrated_directory_opens_as_v2() -> TestResult {
let dir = tempfile::tempdir()?;
let data = dir.path().join("db");
build_v1_fixture(&data, &oversized_shards(2, 3))?;
migrate_chunking(&MigrateOptions::migrate(data.clone()))?;
let opened = Database::open(&data)?;
assert!(
opened.tree_policy().is_v2(),
"a migrated directory must open under the stamped v2 policy"
);
assert_eq!(opened.tree_policy(), v2_default());
Ok(())
}
#[test]
fn abort_of_completed_migration_restores_v1() -> TestResult {
let dir = tempfile::tempdir()?;
let data = dir.path().join("db");
let shards = oversized_shards(2, 3);
let v1_roots = build_v1_fixture(&data, &shards)?;
migrate_chunking(&MigrateOptions::migrate(data.clone()))?;
let report = migrate_chunking(&MigrateOptions::abort(data.clone()))?;
assert_eq!(report.outcome, MigrationOutcome::AbortedToV1);
assert_eq!(report.direction, MigrationDirection::Abort);
assert_eq!(
read_all_roots(&data, shards.len())?,
v1_roots.iter().copied().map(Some).collect::<Vec<_>>(),
"abort must restore byte-identical v1 trees"
);
assert_eq!(
format_version(&data)?,
Some(1),
"the format flips back to 1"
);
assert!(!is_fenced(&data)?, "abort removes the fence");
Ok(())
}
#[test]
fn abort_of_in_flight_migration_restores_v1() -> TestResult {
let dir = tempfile::tempdir()?;
let data = dir.path().join("db");
let shards = oversized_shards(2, 3);
let v1_roots = build_v1_fixture(&data, &shards)?;
let interrupted =
super::migrate_with_crash(&MigrateOptions::migrate(data.clone()), "after_shard_0")?;
assert!(interrupted.is_none(), "the crash must stop the run early");
assert!(is_fenced(&data)?, "a partial migration leaves the fence up");
assert_eq!(
format_version(&data)?,
Some(2),
"the format is 2 while fenced"
);
let report = migrate_chunking(&MigrateOptions::abort(data.clone()))?;
assert_eq!(report.outcome, MigrationOutcome::AbortedToV1);
assert_eq!(
read_all_roots(&data, shards.len())?,
v1_roots.iter().copied().map(Some).collect::<Vec<_>>(),
"abort of a partial migration must restore byte-identical v1 trees"
);
assert_eq!(format_version(&data)?, Some(1));
assert!(!is_fenced(&data)?);
Ok(())
}
#[test]
fn migrate_rebuilds_branch_heads_canonically() -> TestResult {
let dir = tempfile::tempdir()?;
let data = dir.path().join("db");
build_v1_fixture(&data, &oversized_shards(2, 3))?;
let puts: Entries = vec![
(b"branch-0".to_vec(), oversized(50)),
(b"branch-1".to_vec(), oversized(51)),
];
build_v1_branch(&data, 0, &puts)?;
let v1_head = fresh_build(&puts, TreePolicy::V1_DEFAULT)?;
let v2_head = fresh_build(&puts, v2_default())?;
assert_eq!(read_branch_head(&data, "main", 0)?, v1_head);
assert_ne!(
v1_head, v2_head,
"the branch head shape must change under v2"
);
let report = migrate_chunking(&MigrateOptions::migrate(data.clone()))?;
assert_eq!(
report.branch_heads_rebuilt, 1,
"the branch head must be rebuilt"
);
assert_eq!(
read_branch_head(&data, "main", 0)?,
v2_head,
"the migrated branch head must equal the fresh-build canonical v2 head"
);
Ok(())
}
#[test]
fn migrate_of_empty_shards_is_clean() -> TestResult {
let dir = tempfile::tempdir()?;
let data = dir.path().join("db");
build_v1_fixture(&data, &[Vec::new(), Vec::new()])?;
let report = migrate_chunking(&MigrateOptions::migrate(data.clone()))?;
assert_eq!(report.outcome, MigrationOutcome::MigratedToV2);
assert_eq!(report.shard_count, 2);
assert_eq!(format_version(&data)?, Some(2));
assert!(!is_fenced(&data)?);
Ok(())
}