335 lines
11 KiB
Rust
335 lines
11 KiB
Rust
//! Property tests for Patch algebra (diff/apply roundtrips)
|
|
|
|
mod generators;
|
|
|
|
use generators::{StructureParams, check_structure_invariants, structures_equivalent};
|
|
use geolog::core::Structure;
|
|
use geolog::naming::NamingIndex;
|
|
use geolog::patch::{Patch, apply_patch, diff, to_initial_patch};
|
|
use geolog::universe::Universe;
|
|
use proptest::prelude::*;
|
|
use std::collections::HashSet;
|
|
|
|
proptest! {
|
|
/// Empty patch is identity: apply_patch(s, empty) == s
|
|
#[test]
|
|
fn empty_patch_is_identity(
|
|
(structure, mut universe) in generators::arb_structure(StructureParams {
|
|
num_sorts: 3,
|
|
max_elements_per_sort: 8,
|
|
})
|
|
) {
|
|
let empty_patch = Patch::new(None, structure.num_sorts(), structure.num_functions(), structure.relations.len());
|
|
let mut naming = NamingIndex::new();
|
|
|
|
let result = apply_patch(&structure, &empty_patch, &mut universe, &mut naming);
|
|
prop_assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
prop_assert_eq!(result.len(), structure.len());
|
|
prop_assert_eq!(result.num_sorts(), structure.num_sorts());
|
|
|
|
// Check same UUIDs
|
|
prop_assert!(structures_equivalent(&result, &structure, &universe, &universe));
|
|
}
|
|
|
|
/// diff(s, s) produces empty patch
|
|
#[test]
|
|
fn diff_same_is_empty(
|
|
(structure, universe) in generators::arb_structure(StructureParams {
|
|
num_sorts: 3,
|
|
max_elements_per_sort: 8,
|
|
})
|
|
) {
|
|
let naming = NamingIndex::new();
|
|
|
|
let patch = diff(&structure, &structure, &universe, &naming, &naming);
|
|
|
|
prop_assert!(patch.is_empty());
|
|
}
|
|
|
|
/// to_initial_patch creates patch that builds structure from empty
|
|
#[test]
|
|
fn initial_patch_builds_from_empty(
|
|
(structure, mut universe) in generators::arb_structure(StructureParams {
|
|
num_sorts: 3,
|
|
max_elements_per_sort: 8,
|
|
})
|
|
) {
|
|
// Build naming for structure elements
|
|
let mut naming = NamingIndex::new();
|
|
for &luid in &structure.luids {
|
|
if let Some(uuid) = universe.get(luid) {
|
|
naming.insert(uuid, vec![format!("elem_{}", luid)]);
|
|
}
|
|
}
|
|
|
|
let patch = to_initial_patch(&structure, &universe, &naming);
|
|
|
|
// Apply to empty structure
|
|
let empty = Structure::new(structure.num_sorts());
|
|
let mut result_naming = NamingIndex::new();
|
|
let result = apply_patch(&empty, &patch, &mut universe, &mut result_naming);
|
|
|
|
prop_assert!(result.is_ok());
|
|
let result = result.unwrap();
|
|
|
|
// Should have same number of elements
|
|
prop_assert_eq!(result.len(), structure.len());
|
|
|
|
// Should have same UUIDs
|
|
prop_assert!(structures_equivalent(&result, &structure, &universe, &universe));
|
|
}
|
|
|
|
/// Element additions are tracked in patch
|
|
#[test]
|
|
fn additions_tracked(
|
|
num_elements in 1usize..10,
|
|
) {
|
|
let mut universe = Universe::new();
|
|
let mut naming = NamingIndex::new();
|
|
|
|
let old = Structure::new(2);
|
|
let mut new = Structure::new(2);
|
|
|
|
for i in 0..num_elements {
|
|
let (_, luid) = new.add_element(&mut universe, i % 2);
|
|
if let Some(uuid) = universe.get(luid) {
|
|
naming.insert(uuid, vec![format!("elem_{}", i)]);
|
|
}
|
|
}
|
|
|
|
let old_naming = NamingIndex::new();
|
|
let patch = diff(&old, &new, &universe, &old_naming, &naming);
|
|
|
|
prop_assert_eq!(patch.elements.additions.len(), num_elements);
|
|
prop_assert!(patch.elements.deletions.is_empty());
|
|
}
|
|
|
|
/// Element deletions are tracked in patch
|
|
#[test]
|
|
fn deletions_tracked(
|
|
num_elements in 1usize..10,
|
|
) {
|
|
let mut universe = Universe::new();
|
|
let mut old_naming = NamingIndex::new();
|
|
|
|
let mut old = Structure::new(2);
|
|
for i in 0..num_elements {
|
|
let (_, luid) = old.add_element(&mut universe, i % 2);
|
|
if let Some(uuid) = universe.get(luid) {
|
|
old_naming.insert(uuid, vec![format!("elem_{}", i)]);
|
|
}
|
|
}
|
|
|
|
let new = Structure::new(2);
|
|
let new_naming = NamingIndex::new();
|
|
|
|
let patch = diff(&old, &new, &universe, &old_naming, &new_naming);
|
|
|
|
prop_assert_eq!(patch.elements.deletions.len(), num_elements);
|
|
prop_assert!(patch.elements.additions.is_empty());
|
|
}
|
|
|
|
/// Element patch has disjoint additions and deletions
|
|
#[test]
|
|
fn element_patch_disjoint(
|
|
(old, universe) in generators::arb_structure(StructureParams {
|
|
num_sorts: 2,
|
|
max_elements_per_sort: 5,
|
|
}),
|
|
(new, _) in generators::arb_structure(StructureParams {
|
|
num_sorts: 2,
|
|
max_elements_per_sort: 5,
|
|
})
|
|
) {
|
|
let old_naming = NamingIndex::new();
|
|
let new_naming = NamingIndex::new();
|
|
|
|
let patch = diff(&old, &new, &universe, &old_naming, &new_naming);
|
|
|
|
// Additions and deletions should be disjoint
|
|
let additions: HashSet<_> = patch.elements.additions.keys().collect();
|
|
let deletions: HashSet<_> = patch.elements.deletions.iter().collect();
|
|
|
|
let intersection: Vec<_> = additions.intersection(&deletions).collect();
|
|
prop_assert!(intersection.is_empty());
|
|
}
|
|
|
|
/// NamingPatch tracks name additions for new elements
|
|
#[test]
|
|
fn naming_patch_additions(
|
|
num_elements in 1usize..8,
|
|
) {
|
|
let mut universe = Universe::new();
|
|
let mut naming = NamingIndex::new();
|
|
|
|
let old = Structure::new(2);
|
|
let mut new = Structure::new(2);
|
|
|
|
for i in 0..num_elements {
|
|
let (_, luid) = new.add_element(&mut universe, 0);
|
|
if let Some(uuid) = universe.get(luid) {
|
|
naming.insert(uuid, vec![format!("elem_{}", i)]);
|
|
}
|
|
}
|
|
|
|
let old_naming = NamingIndex::new();
|
|
let patch = diff(&old, &new, &universe, &old_naming, &naming);
|
|
|
|
// Naming patch should have names for new elements
|
|
prop_assert_eq!(patch.names.additions.len(), num_elements);
|
|
}
|
|
|
|
/// Patch inversion swaps additions/deletions
|
|
#[test]
|
|
fn inversion_swaps_elements(
|
|
(old, mut universe) in generators::arb_structure(StructureParams {
|
|
num_sorts: 2,
|
|
max_elements_per_sort: 4,
|
|
})
|
|
) {
|
|
// Create a new structure with some different elements
|
|
let mut new = Structure::new(2);
|
|
new.add_element(&mut universe, 0);
|
|
new.add_element(&mut universe, 1);
|
|
|
|
let old_naming = NamingIndex::new();
|
|
let new_naming = NamingIndex::new();
|
|
|
|
let patch = diff(&old, &new, &universe, &old_naming, &new_naming);
|
|
let inverted = patch.invert();
|
|
|
|
// Inverted patch swaps source/target commits
|
|
prop_assert_eq!(inverted.source_commit, Some(patch.target_commit));
|
|
|
|
// Additions become deletions (by key count)
|
|
prop_assert_eq!(
|
|
inverted.elements.deletions.len(),
|
|
patch.elements.additions.len()
|
|
);
|
|
}
|
|
|
|
/// Double inversion preserves target_commit (but creates new source)
|
|
#[test]
|
|
fn double_inversion_target_preserved(
|
|
(structure, universe) in generators::arb_structure(StructureParams {
|
|
num_sorts: 2,
|
|
max_elements_per_sort: 3,
|
|
})
|
|
) {
|
|
let naming = NamingIndex::new();
|
|
let patch = to_initial_patch(&structure, &universe, &naming);
|
|
|
|
let inverted = patch.invert();
|
|
let double_inverted = inverted.invert();
|
|
|
|
// Original target becomes source after double inversion
|
|
// (because each inversion swaps source ↔ target)
|
|
prop_assert_eq!(double_inverted.source_commit, Some(inverted.target_commit));
|
|
}
|
|
|
|
/// Result of apply_patch maintains structure invariants
|
|
#[test]
|
|
fn apply_patch_maintains_invariants(
|
|
(old, mut universe) in generators::arb_structure(StructureParams {
|
|
num_sorts: 3,
|
|
max_elements_per_sort: 5,
|
|
}),
|
|
(new, _) in generators::arb_structure(StructureParams {
|
|
num_sorts: 3,
|
|
max_elements_per_sort: 5,
|
|
})
|
|
) {
|
|
let old_naming = NamingIndex::new();
|
|
let new_naming = NamingIndex::new();
|
|
|
|
let patch = diff(&old, &new, &universe, &old_naming, &new_naming);
|
|
let mut result_naming = NamingIndex::new();
|
|
|
|
let result = apply_patch(&old, &patch, &mut universe, &mut result_naming);
|
|
prop_assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
prop_assert!(check_structure_invariants(&result).is_ok());
|
|
}
|
|
}
|
|
|
|
// More focused roundtrip tests
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(256))]
|
|
|
|
/// THE KEY PROPERTY: diff then apply is identity
|
|
/// diff(old, new) |> apply_patch(old, _) ≈ new
|
|
///
|
|
/// We test this by starting with a structure and modifying it (adding/removing elements)
|
|
/// to create `new`, ensuring both share the same Universe.
|
|
#[test]
|
|
fn diff_apply_roundtrip(
|
|
(base, mut universe) in generators::arb_structure(StructureParams {
|
|
num_sorts: 2,
|
|
max_elements_per_sort: 4,
|
|
}),
|
|
additions in proptest::collection::vec(0usize..2, 0..4),
|
|
deletions_count in 0usize..3,
|
|
) {
|
|
// Create `old` as a clone of base
|
|
let old = base.clone();
|
|
|
|
// Build naming for old structure
|
|
let mut old_naming = NamingIndex::new();
|
|
for &luid in &old.luids {
|
|
if let Some(uuid) = universe.get(luid) {
|
|
old_naming.insert(uuid, vec![format!("old_elem_{}", luid)]);
|
|
}
|
|
}
|
|
|
|
// Create `new` by modifying base: add some elements, potentially skip some old ones
|
|
let mut new = Structure::new(base.num_sorts());
|
|
let mut new_naming = NamingIndex::new();
|
|
|
|
// Keep some elements from old (skip the first `deletions_count`)
|
|
let keep_count = base.len().saturating_sub(deletions_count);
|
|
for slid in 0..keep_count {
|
|
let luid = base.luids[slid];
|
|
let sort_id = base.sorts[slid];
|
|
new.add_element_with_luid(luid, sort_id);
|
|
|
|
if let Some(uuid) = universe.get(luid) {
|
|
new_naming.insert(uuid, vec![format!("kept_elem_{}", luid)]);
|
|
}
|
|
}
|
|
|
|
// Add new elements
|
|
for sort_id in additions {
|
|
let (_, luid) = new.add_element(&mut universe, sort_id);
|
|
if let Some(uuid) = universe.get(luid) {
|
|
new_naming.insert(uuid, vec![format!("new_elem_{}", luid)]);
|
|
}
|
|
}
|
|
|
|
// Now diff and apply
|
|
let patch = diff(&old, &new, &universe, &old_naming, &new_naming);
|
|
let mut result_naming = NamingIndex::new();
|
|
|
|
let result = apply_patch(&old, &patch, &mut universe, &mut result_naming);
|
|
prop_assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
// Result should have same number of elements as new
|
|
prop_assert_eq!(result.len(), new.len());
|
|
|
|
// Result should have same UUIDs as new (both use the same universe)
|
|
let result_uuids: HashSet<_> = result.luids.iter()
|
|
.filter_map(|&luid| universe.get(luid))
|
|
.collect();
|
|
let new_uuids: HashSet<_> = new.luids.iter()
|
|
.filter_map(|&luid| universe.get(luid))
|
|
.collect();
|
|
|
|
prop_assert_eq!(result_uuids, new_uuids);
|
|
}
|
|
}
|