geolog-zeta-fork/tests/proptest_naming.rs

219 lines
6.6 KiB
Rust
Raw Normal View History

2026-02-26 11:50:51 +01:00
//! Property tests for NamingIndex (UUID ↔ Name bidirectional consistency)
mod generators;
use geolog::id::Uuid;
use geolog::naming::NamingIndex;
use proptest::prelude::*;
use std::collections::HashSet;
use tempfile::tempdir;
proptest! {
/// Insert then lookup returns the same name
#[test]
fn insert_get_roundtrip(
uuid in generators::arb_uuid(),
name in generators::arb_qualified_name()
) {
let mut index = NamingIndex::new();
index.insert(uuid, name.clone());
let retrieved = index.get(&uuid);
prop_assert_eq!(retrieved, Some(&name));
}
/// Simple name (last component) is correctly extracted
#[test]
fn simple_name_is_last_component(
uuid in generators::arb_uuid(),
name in generators::arb_qualified_name()
) {
let mut index = NamingIndex::new();
index.insert(uuid, name.clone());
let simple = index.get_simple(&uuid);
let expected = name.last().map(|s| s.as_str());
prop_assert_eq!(simple, expected);
}
/// lookup(simple_name) contains the UUID
#[test]
fn lookup_contains_uuid(
uuid in generators::arb_uuid(),
name in generators::arb_qualified_name()
) {
let mut index = NamingIndex::new();
index.insert(uuid, name.clone());
if let Some(simple) = name.last() {
let results = index.lookup(simple);
prop_assert!(!results.is_empty());
prop_assert!(results.contains(&uuid));
}
}
/// lookup_unique returns Some iff exactly one UUID has that name
#[test]
fn lookup_unique_semantics(
entries in proptest::collection::vec(
(generators::arb_uuid(), generators::arb_qualified_name()),
1..10
)
) {
// Filter to unique UUIDs
let mut seen_uuids = HashSet::new();
let unique_entries: Vec<_> = entries.into_iter()
.filter(|(uuid, _)| seen_uuids.insert(*uuid))
.collect();
let mut index = NamingIndex::new();
for (uuid, name) in &unique_entries {
index.insert(*uuid, name.clone());
}
// For each simple name, check lookup_unique semantics
let mut name_counts: std::collections::HashMap<String, Vec<Uuid>> =
std::collections::HashMap::new();
for (uuid, name) in &unique_entries {
if let Some(simple) = name.last() {
name_counts.entry(simple.clone()).or_default().push(*uuid);
}
}
for (simple_name, uuids) in name_counts {
let unique_result = index.lookup_unique(&simple_name);
if uuids.len() == 1 {
prop_assert_eq!(unique_result, Some(uuids[0]));
} else {
prop_assert_eq!(unique_result, None);
}
}
}
/// Ambiguous names (multiple UUIDs) return None for lookup_unique
#[test]
fn ambiguous_names_return_none(
uuid1 in generators::arb_uuid(),
uuid2 in generators::arb_uuid(),
shared_name in generators::arb_identifier()
) {
prop_assume!(uuid1 != uuid2);
let mut index = NamingIndex::new();
index.insert(uuid1, vec!["Theory1".to_string(), shared_name.clone()]);
index.insert(uuid2, vec!["Theory2".to_string(), shared_name.clone()]);
// lookup returns both
let results = index.lookup(&shared_name);
prop_assert_eq!(results.len(), 2);
prop_assert!(results.contains(&uuid1));
prop_assert!(results.contains(&uuid2));
// lookup_unique returns None
prop_assert_eq!(index.lookup_unique(&shared_name), None);
}
/// display_name returns the simple name if set, otherwise UUID string
#[test]
fn display_name_fallback(uuid in generators::arb_uuid()) {
let mut index = NamingIndex::new();
// Without name: should contain UUID
let display_without = index.display_name(&uuid);
let uuid_str = format!("{}", uuid);
prop_assert!(display_without.contains(&uuid_str));
// With name: should be the simple name
let name = vec!["Test".to_string(), "Element".to_string()];
index.insert(uuid, name);
let display_with = index.display_name(&uuid);
prop_assert_eq!(display_with, "Element");
}
/// Save and load preserves all mappings
#[test]
fn save_load_roundtrip(
entries in proptest::collection::vec(
(generators::arb_uuid(), generators::arb_qualified_name()),
1..15
)
) {
// Filter to unique UUIDs
let mut seen_uuids = HashSet::new();
let unique_entries: Vec<_> = entries.into_iter()
.filter(|(uuid, _)| seen_uuids.insert(*uuid))
.collect();
let dir = tempdir().unwrap();
let path = dir.path().join("names.bin");
// Save
{
let mut index = NamingIndex::with_path(&path);
for (uuid, name) in &unique_entries {
index.insert(*uuid, name.clone());
}
index.save().unwrap();
}
// Load
{
let loaded = NamingIndex::load(&path).unwrap();
for (uuid, name) in &unique_entries {
prop_assert_eq!(loaded.get(uuid), Some(name));
}
prop_assert_eq!(loaded.len(), unique_entries.len());
}
}
/// Dirty flag consistency
#[test]
fn dirty_flag_consistency(
uuid in generators::arb_uuid(),
name in generators::arb_qualified_name()
) {
let dir = tempdir().unwrap();
let path = dir.path().join("names.bin");
let mut index = NamingIndex::with_path(&path);
// Initially clean
prop_assert!(!index.is_dirty());
// Dirty after insert
index.insert(uuid, name);
prop_assert!(index.is_dirty());
// Clean after save
index.save().unwrap();
prop_assert!(!index.is_dirty());
}
/// Len reflects number of unique UUIDs
#[test]
fn len_reflects_entries(
entries in proptest::collection::vec(
(generators::arb_uuid(), generators::arb_qualified_name()),
0..20
)
) {
// Filter to unique UUIDs
let mut seen_uuids = HashSet::new();
let unique_entries: Vec<_> = entries.into_iter()
.filter(|(uuid, _)| seen_uuids.insert(*uuid))
.collect();
let mut index = NamingIndex::new();
for (uuid, name) in &unique_entries {
index.insert(*uuid, name.clone());
}
prop_assert_eq!(index.len(), unique_entries.len());
prop_assert_eq!(index.is_empty(), unique_entries.is_empty());
}
}