- 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.
571 lines
18 KiB
Python
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"])
|