glam/tests/test_temporal_validation.py
kempersc 2761857b0d Add scripts for converting OWL/Turtle ontology to Mermaid and PlantUML diagrams
- Implemented `owl_to_mermaid.py` to convert OWL/Turtle files into Mermaid class diagrams.
- Implemented `owl_to_plantuml.py` to convert OWL/Turtle files into PlantUML class diagrams.
- Added two new PlantUML files for custodian multi-aspect diagrams.
2025-11-22 23:01:13 +01:00

571 lines
18 KiB
Python

#!/usr/bin/env python3
"""
Test Suite for Temporal Consistency Validator (v0.7.0)
Tests validation rules with valid and invalid test cases.
Author: Heritage Custodian Ontology Project
Date: 2025-11-22
Schema Version: v0.7.0 (Phase 5: Validation Framework)
"""
import pytest
import tempfile
from pathlib import Path
from datetime import date
import sys
# Add parent directory to path to import validator
sys.path.insert(0, str(Path(__file__).parent.parent / 'scripts'))
from validate_temporal_consistency import (
DataLoader, TemporalValidator, parse_date, date_within_range
)
# ============================================================================
# Utility Functions
# ============================================================================
def create_temp_yaml(yaml_content: str) -> Path:
"""Create temporary YAML file for testing."""
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False)
temp_file.write(yaml_content)
temp_file.close()
return Path(temp_file.name)
# ============================================================================
# Date Utility Tests
# ============================================================================
class TestDateUtilities:
"""Test date parsing and range checking utilities."""
def test_parse_date_iso_string(self):
"""Test parsing ISO date string."""
result = parse_date("2025-11-22")
assert result == date(2025, 11, 22)
def test_parse_date_iso_with_time(self):
"""Test parsing ISO datetime string."""
result = parse_date("2025-11-22T15:30:00Z")
assert result == date(2025, 11, 22)
def test_parse_date_none(self):
"""Test parsing None returns None."""
result = parse_date(None)
assert result is None
def test_parse_date_object(self):
"""Test passing date object returns same object."""
test_date = date(2025, 11, 22)
result = parse_date(test_date)
assert result == test_date
def test_date_within_range_valid(self):
"""Test date within valid range."""
check = date(2020, 6, 15)
start = date(2020, 1, 1)
end = date(2020, 12, 31)
assert date_within_range(check, start, end) is True
def test_date_within_range_before_start(self):
"""Test date before start fails."""
check = date(2019, 12, 31)
start = date(2020, 1, 1)
end = date(2020, 12, 31)
assert date_within_range(check, start, end) is False
def test_date_within_range_after_end(self):
"""Test date after end fails."""
check = date(2021, 1, 1)
start = date(2020, 1, 1)
end = date(2020, 12, 31)
assert date_within_range(check, start, end) is False
def test_date_within_range_open_ended(self):
"""Test open-ended range (None end date)."""
check = date(2025, 11, 22)
start = date(2020, 1, 1)
end = None
assert date_within_range(check, start, end) is True
# ============================================================================
# Collection-Unit Temporal Validation Tests
# ============================================================================
class TestCollectionUnitTemporal:
"""Test collection-unit temporal consistency validation."""
def test_valid_collection_within_unit_lifetime(self):
"""Test collection custody within unit validity period (VALID)."""
yaml_content = """
---
id: "https://example.org/unit/dept-1"
unit_name: "Test Department"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: null
managed_collections:
- "https://example.org/collection/coll-1"
---
id: "https://example.org/collection/coll-1"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-1"
valid_from: "2010-01-01"
valid_to: null
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have no errors (collection starts after unit)
collection_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_TEMPORAL"]
assert len(collection_errors) == 0
finally:
yaml_file.unlink()
def test_invalid_collection_before_unit(self):
"""Test collection custody starts before unit exists (INVALID)."""
yaml_content = """
---
id: "https://example.org/unit/dept-1"
unit_name: "Test Department"
unit_type: DEPARTMENT
valid_from: "2010-01-01"
valid_to: null
managed_collections:
- "https://example.org/collection/coll-1"
---
id: "https://example.org/collection/coll-1"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-1"
valid_from: "2005-01-01"
valid_to: null
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have error: collection starts before unit
collection_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_TEMPORAL"]
assert len(collection_errors) == 1
assert "before managing unit exists" in collection_errors[0].message
finally:
yaml_file.unlink()
def test_invalid_collection_after_unit_dissolved(self):
"""Test collection custody extends beyond unit validity (INVALID)."""
yaml_content = """
---
id: "https://example.org/unit/dept-1"
unit_name: "Test Department"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: "2020-12-31"
managed_collections:
- "https://example.org/collection/coll-1"
---
id: "https://example.org/collection/coll-1"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-1"
valid_from: "2010-01-01"
valid_to: "2025-12-31"
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have error: collection extends beyond unit
collection_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_TEMPORAL"]
assert len(collection_errors) == 1
assert "extends" in collection_errors[0].message and "beyond" in collection_errors[0].message
finally:
yaml_file.unlink()
def test_warning_collection_ongoing_after_unit_dissolved(self):
"""Test warning when collection custody ongoing but unit dissolved (WARNING)."""
yaml_content = """
---
id: "https://example.org/unit/dept-1"
unit_name: "Test Department"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: "2020-12-31"
managed_collections:
- "https://example.org/collection/coll-1"
---
id: "https://example.org/collection/coll-1"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-1"
valid_from: "2010-01-01"
valid_to: null
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have warning: collection ongoing but unit dissolved
collection_warnings = [w for w in result.warnings if w.rule == "COLLECTION_UNIT_TEMPORAL"]
assert len(collection_warnings) == 1
assert "ongoing" in collection_warnings[0].message and "dissolved" in collection_warnings[0].message
finally:
yaml_file.unlink()
# ============================================================================
# Bidirectional Relationship Tests
# ============================================================================
class TestBidirectionalRelationships:
"""Test bidirectional relationship consistency validation."""
def test_valid_bidirectional_collection_unit(self):
"""Test valid bidirectional collection-unit relationship (VALID)."""
yaml_content = """
---
id: "https://example.org/unit/dept-1"
unit_name: "Test Department"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: null
managed_collections:
- "https://example.org/collection/coll-1"
---
id: "https://example.org/collection/coll-1"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-1"
valid_from: "2010-01-01"
valid_to: null
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have no bidirectional errors
bidir_errors = [e for e in result.errors if "BIDIRECTIONAL" in e.rule]
assert len(bidir_errors) == 0
finally:
yaml_file.unlink()
def test_invalid_collection_missing_reverse_relationship(self):
"""Test collection references unit but unit doesn't list collection (INVALID)."""
yaml_content = """
---
id: "https://example.org/unit/dept-1"
unit_name: "Test Department"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: null
managed_collections: []
---
id: "https://example.org/collection/coll-1"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-1"
valid_from: "2010-01-01"
valid_to: null
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have bidirectional error
bidir_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_BIDIRECTIONAL"]
assert len(bidir_errors) == 1
assert "does not list collection in managed_collections" in bidir_errors[0].message
finally:
yaml_file.unlink()
def test_invalid_unit_references_nonexistent_collection(self):
"""Test unit references collection that doesn't exist (INVALID)."""
yaml_content = """
---
id: "https://example.org/unit/dept-1"
unit_name: "Test Department"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: null
managed_collections:
- "https://example.org/collection/nonexistent"
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have bidirectional error
bidir_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_BIDIRECTIONAL"]
assert len(bidir_errors) == 1
assert "non-existent collection" in bidir_errors[0].message
finally:
yaml_file.unlink()
# ============================================================================
# Custody Transfer Continuity Tests
# ============================================================================
class TestCustodyContinuity:
"""Test custody transfer continuity validation."""
def test_valid_continuous_custody_transfer(self):
"""Test continuous custody transfer (no gap, VALID)."""
yaml_content = """
---
id: "https://example.org/unit/dept-old"
unit_name: "Old Department"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: "2020-12-31"
managed_collections:
- "https://example.org/collection/coll-v1"
---
id: "https://example.org/unit/dept-new"
unit_name: "New Department"
unit_type: DEPARTMENT
valid_from: "2021-01-01"
valid_to: null
managed_collections:
- "https://example.org/collection/coll-v2"
---
id: "https://example.org/collection/coll-v1"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-old"
valid_from: "2010-01-01"
valid_to: "2020-12-31"
---
id: "https://example.org/collection/coll-v2"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-new"
valid_from: "2021-01-01"
valid_to: null
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have no continuity errors (1 day gap is acceptable)
continuity_errors = [e for e in result.errors if e.rule == "CUSTODY_CONTINUITY"]
assert len(continuity_errors) == 0
finally:
yaml_file.unlink()
def test_warning_custody_gap(self):
"""Test custody gap between versions (WARNING)."""
yaml_content = """
---
id: "https://example.org/unit/dept-old"
unit_name: "Old Department"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: "2020-12-31"
managed_collections:
- "https://example.org/collection/coll-v1"
---
id: "https://example.org/unit/dept-new"
unit_name: "New Department"
unit_type: DEPARTMENT
valid_from: "2021-03-01"
valid_to: null
managed_collections:
- "https://example.org/collection/coll-v2"
---
id: "https://example.org/collection/coll-v1"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-old"
valid_from: "2010-01-01"
valid_to: "2020-12-31"
---
id: "https://example.org/collection/coll-v2"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-new"
valid_from: "2021-03-01"
valid_to: null
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have continuity warning (60+ day gap)
continuity_warnings = [w for w in result.warnings if w.rule == "CUSTODY_CONTINUITY"]
assert len(continuity_warnings) == 1
assert "gap" in continuity_warnings[0].message
finally:
yaml_file.unlink()
def test_error_custody_overlap(self):
"""Test overlapping custody periods (ERROR)."""
yaml_content = """
---
id: "https://example.org/unit/dept-old"
unit_name: "Old Department"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: "2020-12-31"
managed_collections:
- "https://example.org/collection/coll-v1"
---
id: "https://example.org/unit/dept-new"
unit_name: "New Department"
unit_type: DEPARTMENT
valid_from: "2020-06-01"
valid_to: null
managed_collections:
- "https://example.org/collection/coll-v2"
---
id: "https://example.org/collection/coll-v1"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-old"
valid_from: "2010-01-01"
valid_to: "2020-12-31"
---
id: "https://example.org/collection/coll-v2"
collection_name: "Test Collection"
managing_unit: "https://example.org/unit/dept-new"
valid_from: "2020-06-01"
valid_to: null
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have continuity error (overlapping custody)
continuity_errors = [e for e in result.errors if e.rule == "CUSTODY_CONTINUITY"]
assert len(continuity_errors) == 1
assert "overlapping" in continuity_errors[0].message
finally:
yaml_file.unlink()
# ============================================================================
# Integration Tests (Multiple Rules)
# ============================================================================
class TestIntegration:
"""Test multiple validation rules together."""
def test_merger_scenario_valid(self):
"""Test complete merger scenario with custody transfer (VALID)."""
yaml_content = """
---
id: "https://example.org/unit/dept-a"
unit_name: "Department A"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: "2020-02-28"
managed_collections:
- "https://example.org/collection/coll-a-v1"
---
id: "https://example.org/unit/dept-b"
unit_name: "Department B"
unit_type: DEPARTMENT
valid_from: "2000-01-01"
valid_to: "2020-02-28"
managed_collections:
- "https://example.org/collection/coll-b-v1"
---
id: "https://example.org/unit/dept-merged"
unit_name: "Merged Department"
unit_type: DIVISION
valid_from: "2020-03-01"
valid_to: null
managed_collections:
- "https://example.org/collection/coll-a-v2"
- "https://example.org/collection/coll-b-v2"
---
id: "https://example.org/collection/coll-a-v1"
collection_name: "Collection A"
managing_unit: "https://example.org/unit/dept-a"
valid_from: "2010-01-01"
valid_to: "2020-02-28"
---
id: "https://example.org/collection/coll-a-v2"
collection_name: "Collection A"
managing_unit: "https://example.org/unit/dept-merged"
valid_from: "2020-03-01"
valid_to: null
---
id: "https://example.org/collection/coll-b-v1"
collection_name: "Collection B"
managing_unit: "https://example.org/unit/dept-b"
valid_from: "2010-01-01"
valid_to: "2020-02-28"
---
id: "https://example.org/collection/coll-b-v2"
collection_name: "Collection B"
managing_unit: "https://example.org/unit/dept-merged"
valid_from: "2020-03-01"
valid_to: null
"""
yaml_file = create_temp_yaml(yaml_content)
try:
data = DataLoader(yaml_file).load()
validator = TemporalValidator(data)
result = validator.validate_all()
# Should have no errors (valid merger with continuous custody)
assert result.is_valid
assert len(result.errors) == 0
finally:
yaml_file.unlink()
# ============================================================================
# Run Tests
# ============================================================================
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])