use std::collections::HashSet;
use std::path::PathBuf;
use super::*;
use crate::test_support::{
signed_empty_block_envelope, signed_ref_state_envelope, signed_ref_update_envelope,
unique_temp_dir,
};
use crate::{FileObjectStore, ObjectWriter, RefPublication, RefStore, verify_repository};
use prikk_object::{ObjectEnvelope, ObjectType};
fn files_under(dir: &std::path::Path) -> Result<HashSet<PathBuf>> {
let mut files = HashSet::new();
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if entry.file_type()?.is_dir() {
files.extend(files_under(&path)?);
} else {
files.insert(path);
}
}
Ok(files)
}
#[test]
fn init_allocates_every_container_index_and_generation_log_name_once() -> Result<()> {
let root = unique_temp_dir("layout-container-allocation");
let layout = RepositoryLayout::init(root.clone())?;
let mut container_paths = Vec::new();
for object_type in persisted_object_types() {
container_paths.push(layout.container_slot_path(object_type, ContainerSlot::A));
container_paths.push(layout.container_slot_path(object_type, ContainerSlot::B));
}
container_paths.push(layout.container_index_path());
container_paths.push(layout.container_generation_log_path());
assert_eq!(
container_paths.len(),
16,
"7 object types x 2 slots + index + generation log"
);
for path in &container_paths {
assert!(path.is_file(), "expected {path:?} to exist after init");
assert_eq!(
std::fs::metadata(path)?.len(),
0,
"expected {path:?} to be created empty"
);
}
let reopened = RepositoryLayout::init(root.clone())?;
for path in &container_paths {
assert!(path.is_file());
assert_eq!(std::fs::metadata(path)?.len(), 0);
}
assert_eq!(reopened.format(), RepositoryFormat::CurrentV6);
let expected: HashSet<PathBuf> = container_paths.into_iter().collect();
assert_eq!(files_under(&layout.containers_dir())?, expected);
let mut store = FileObjectStore::new(layout.clone());
for object_type in persisted_object_types() {
let schema_version = if object_type == ObjectType::Block {
2
} else {
1
};
let mut envelope =
ObjectEnvelope::unsigned(object_type, schema_version, b"acceptance-1".to_vec());
envelope.add_signature(crate::test_support::dummy_signature())?;
store.write_object(&envelope)?;
}
assert_eq!(
files_under(&layout.containers_dir())?,
expected,
"ordinary object writes must grow existing container/index files, never create a new one"
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn init_allocates_every_ref_container_name_once() -> Result<()> {
let root = unique_temp_dir("layout-ref-container-allocation");
let layout = RepositoryLayout::init(root.clone())?;
let ref_container_paths = vec![
layout.ref_log_container_slot_path(ContainerSlot::A),
layout.ref_log_container_slot_path(ContainerSlot::B),
layout.ref_pointer_index_slot_path(ContainerSlot::A),
layout.ref_pointer_index_slot_path(ContainerSlot::B),
layout.ref_pointer_index_generation_log_path(),
layout.received_index_slot_path(ContainerSlot::A),
layout.received_index_slot_path(ContainerSlot::B),
layout.received_index_generation_log_path(),
];
for path in &ref_container_paths {
assert!(path.is_file(), "expected {path:?} to exist after init");
assert_eq!(
std::fs::metadata(path)?.len(),
0,
"expected {path:?} to be created empty"
);
}
let reopened = RepositoryLayout::init(root.clone())?;
for path in &ref_container_paths {
assert!(path.is_file());
assert_eq!(std::fs::metadata(path)?.len(), 0);
}
assert_eq!(reopened.format(), RepositoryFormat::CurrentV6);
let expected: HashSet<PathBuf> = ref_container_paths.into_iter().collect();
assert_eq!(files_under(&layout.refs_containers_dir())?, expected);
let mut objects = FileObjectStore::new(layout.clone());
let target = objects.write_object(&signed_empty_block_envelope())?;
let ref_state = signed_ref_state_envelope("heads/main", None, target, 1);
let ref_state_id = ref_state.object_id();
let first = RefPublication {
ref_name: "heads/main".to_string(),
expected_previous_ref_state_id: None,
ref_update: signed_ref_update_envelope("heads/main", None, ref_state_id, target, 1),
ref_state,
};
let store = RefStore::new(layout.clone());
store.publish(&first)?;
let second_target = objects.write_object(&signed_empty_block_envelope())?;
let second_ref_state = signed_ref_state_envelope("heads/topic", None, second_target, 1);
let second_ref_state_id = second_ref_state.object_id();
store.publish(&RefPublication {
ref_name: "heads/topic".to_string(),
expected_previous_ref_state_id: None,
ref_update: signed_ref_update_envelope(
"heads/topic",
None,
second_ref_state_id,
second_target,
1,
),
ref_state: second_ref_state,
})?;
assert_eq!(
files_under(&layout.refs_containers_dir())?,
expected,
"ordinary publishes -- including minting a brand-new ref name well after init -- must grow \
existing container/index files, never create a new one"
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn init_allocates_every_trust_container_name_once() -> Result<()> {
let root = unique_temp_dir("layout-trust-container-allocation");
let layout = RepositoryLayout::init(root.clone())?;
let trust_container_paths = vec![
layout.trust_key_container_path(),
layout.trust_policy_container_slot_path(ContainerSlot::A),
layout.trust_policy_container_slot_path(ContainerSlot::B),
layout.trust_policy_generation_log_path(),
layout.author_key_container_path(),
];
for path in &trust_container_paths {
assert!(path.is_file(), "expected {path:?} to exist after init");
assert_eq!(
std::fs::metadata(path)?.len(),
0,
"expected {path:?} to be created empty"
);
}
let reopened = RepositoryLayout::init(root.clone())?;
for path in &trust_container_paths {
assert!(path.is_file());
assert_eq!(std::fs::metadata(path)?.len(), 0);
}
assert_eq!(reopened.format(), RepositoryFormat::CurrentV6);
let expected: HashSet<PathBuf> = trust_container_paths.into_iter().collect();
assert_eq!(files_under(&layout.trust_dir())?, expected);
let key = "0707070707070707070707070707070707070707070707070707070707070707";
crate::add_trusted_maintainer(&layout, "maintainer", key)?;
crate::add_trusted_maintainer(&layout, "second", key)?;
assert_eq!(
files_under(&layout.trust_dir())?,
expected,
"ordinary adoptions -- including a second key id well after init -- must grow existing \
container files, never create a new one"
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn init_allocates_every_active_default_container_name_once_excluding_the_runtime_lock() -> Result<()>
{
let root = unique_temp_dir("layout-active-default-allocation");
let layout = RepositoryLayout::init(root.clone())?;
let active_container_paths = vec![
layout.default_queue_wal_path(),
layout.default_active_ref_name_path(),
];
for path in &active_container_paths {
assert!(path.is_file(), "expected {path:?} to exist after init");
assert_eq!(
std::fs::metadata(path)?.len(),
0,
"expected {path:?} to be created empty"
);
}
let expected: HashSet<PathBuf> = active_container_paths.into_iter().collect();
assert_eq!(
files_under(&layout.default_active_dir())?,
expected,
"before any lock is ever acquired, active/default/ must contain exactly the two init-\
allocated names"
);
let lock_path = layout.default_active_lock_path();
let lock = crate::ActiveLock::acquire(&layout)?;
assert!(lock_path.is_file(), "expected the lock file to now exist");
let mut with_lock = files_under(&layout.default_active_dir())?;
assert!(
with_lock.remove(&lock_path),
"the lock must be the only addition"
);
assert_eq!(
with_lock, expected,
"excluding the runtime lock, the set is still exactly the two init-allocated names"
);
drop(lock);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn a_fresh_repository_opens_and_verifies_without_the_dead_directories() -> Result<()> {
let root = unique_temp_dir("layout-no-dead-directories");
let layout = RepositoryLayout::init(root.clone())?;
let mut absent_paths = vec![
layout.prikk_dir().join("objects"),
layout.refs_dir().join("by-id"),
layout.refs_dir().join("logs"),
layout.prikk_dir().join("quarantine"),
];
for object_type in persisted_object_types() {
absent_paths.push(
layout
.prikk_dir()
.join("objects")
.join(object_type_directory_name(object_type)),
);
}
assert_eq!(
absent_paths.len(),
4 + persisted_object_types().len(),
"objects/ + one subdirectory per persisted object type + three others"
);
for absent in &absent_paths {
assert!(
!absent.exists(),
"expected {absent:?} to not exist after init -- it is one of the removed directories"
);
}
assert!(
layout.refs_dir().join("tmp").is_dir(),
"refs/tmp/ is the one directory that stays -- refs/verify.rs's candidate scan reads it \
on every verify"
);
let mut objects = FileObjectStore::new(layout.clone());
let target = objects.write_object(&signed_empty_block_envelope())?;
let ref_state = signed_ref_state_envelope("heads/main", None, target, 1);
let ref_state_id = ref_state.object_id();
RefStore::new(layout.clone()).publish(&RefPublication {
ref_name: "heads/main".to_string(),
expected_previous_ref_state_id: None,
ref_update: signed_ref_update_envelope("heads/main", None, ref_state_id, target, 1),
ref_state,
})?;
let report = verify_repository(&layout)?;
assert!(
!report.has_stage_failure(),
"expected a clean verify on a repository that never had the removed directories, got: \
{report:?}"
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}