308 lines
10 KiB
Python
308 lines
10 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Phase 5.2 Graph Analytics tests."""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent / "ontology_platform"))
|
||
|
|
|
||
|
|
from ont_platform.core.graph.graph_analytics import GraphAnalytics, Community
|
||
|
|
|
||
|
|
|
||
|
|
class MockAdapter:
|
||
|
|
"""Mock Neo4j adapter for testing."""
|
||
|
|
|
||
|
|
async def execute_cypher(self, cypher: str, params=None):
|
||
|
|
"""Mock Cypher execution."""
|
||
|
|
params = params or {}
|
||
|
|
|
||
|
|
# Degree centrality
|
||
|
|
if "size((" in cypher and "out_degree" not in cypher:
|
||
|
|
return [
|
||
|
|
{"result": {"entity_id": 1, "label": "Hub", "centrality_score": 10, "type": "degree"}},
|
||
|
|
{"result": {"entity_id": 2, "label": "Node_2", "centrality_score": 5, "type": "degree"}},
|
||
|
|
{"result": {"entity_id": 3, "label": "Node_3", "centrality_score": 3, "type": "degree"}},
|
||
|
|
]
|
||
|
|
|
||
|
|
# Pagerank centrality
|
||
|
|
if "out_degree" in cypher or "in_degree" in cypher:
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"result": {
|
||
|
|
"entity_id": 1,
|
||
|
|
"label": "Hub",
|
||
|
|
"centrality_score": 0.45,
|
||
|
|
"in_degree": 8,
|
||
|
|
"out_degree": 2,
|
||
|
|
"type": "pagerank",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"result": {
|
||
|
|
"entity_id": 2,
|
||
|
|
"label": "Node_2",
|
||
|
|
"centrality_score": 0.30,
|
||
|
|
"in_degree": 5,
|
||
|
|
"out_degree": 3,
|
||
|
|
"type": "pagerank",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
]
|
||
|
|
|
||
|
|
# Communities (Louvain)
|
||
|
|
if "communityId" in cypher or "algo.louvain" in cypher:
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"result": {
|
||
|
|
"community_id": 0,
|
||
|
|
"entities": [1, 2, 3, 4],
|
||
|
|
"labels": ["Entity_1", "Entity_2", "Entity_3", "Entity_4"],
|
||
|
|
"size": 4,
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"result": {
|
||
|
|
"community_id": 1,
|
||
|
|
"entities": [5, 6, 7],
|
||
|
|
"labels": ["Entity_5", "Entity_6", "Entity_7"],
|
||
|
|
"size": 3,
|
||
|
|
}
|
||
|
|
},
|
||
|
|
]
|
||
|
|
|
||
|
|
# Graph statistics
|
||
|
|
if "node_count" in cypher or "edge_count" in cypher:
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"stats": {
|
||
|
|
"total_nodes": 20,
|
||
|
|
"total_edges": 45,
|
||
|
|
"avg_degree": 4.5,
|
||
|
|
"density": 0.118,
|
||
|
|
"max_possible_edges": 190,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
# Diameter
|
||
|
|
if "diameter" in cypher:
|
||
|
|
return [{"diameter": 5}]
|
||
|
|
|
||
|
|
# Components
|
||
|
|
if "componentId" in cypher or "algo.unionFind" in cypher:
|
||
|
|
return [{"num_components": 1}]
|
||
|
|
|
||
|
|
return []
|
||
|
|
|
||
|
|
|
||
|
|
async def test_calculate_centrality_degree():
|
||
|
|
"""Test degree centrality calculation."""
|
||
|
|
print("\n[TEST 1] Degree Centrality")
|
||
|
|
|
||
|
|
adapter = MockAdapter()
|
||
|
|
analytics = GraphAnalytics(adapter)
|
||
|
|
|
||
|
|
entities = await analytics.calculate_centrality(centrality_type="degree", top_n=10)
|
||
|
|
|
||
|
|
assert len(entities) > 0, "Should find entities"
|
||
|
|
assert all("entity_id" in e and "centrality_score" in e for e in entities), "Should have required fields"
|
||
|
|
assert all(e["centrality_score"] > 0 for e in entities), "Centrality scores should be positive"
|
||
|
|
|
||
|
|
print(f" [OK] Found {len(entities)} entities by degree")
|
||
|
|
top_entity = entities[0]
|
||
|
|
print(f" [OK] Top entity: {top_entity['label']} (degree={top_entity['centrality_score']})")
|
||
|
|
print(" [PASS]")
|
||
|
|
|
||
|
|
|
||
|
|
async def test_calculate_centrality_pagerank():
|
||
|
|
"""Test PageRank centrality calculation."""
|
||
|
|
print("\n[TEST 2] PageRank Centrality")
|
||
|
|
|
||
|
|
adapter = MockAdapter()
|
||
|
|
analytics = GraphAnalytics(adapter)
|
||
|
|
|
||
|
|
entities = await analytics.calculate_centrality(centrality_type="pagerank", top_n=10)
|
||
|
|
|
||
|
|
assert len(entities) > 0, "Should find entities"
|
||
|
|
assert all(0 < e["centrality_score"] <= 1 for e in entities), "PageRank should be 0-1"
|
||
|
|
|
||
|
|
print(f" [OK] Found {len(entities)} entities by PageRank")
|
||
|
|
top_entity = entities[0]
|
||
|
|
print(f" [OK] Top entity: {top_entity['label']} (score={top_entity['centrality_score']:.3f})")
|
||
|
|
print(" [PASS]")
|
||
|
|
|
||
|
|
|
||
|
|
async def test_invalid_centrality_type():
|
||
|
|
"""Test invalid centrality type."""
|
||
|
|
print("\n[TEST 3] Invalid Centrality Type")
|
||
|
|
|
||
|
|
adapter = MockAdapter()
|
||
|
|
analytics = GraphAnalytics(adapter)
|
||
|
|
|
||
|
|
result = await analytics.calculate_centrality(centrality_type="invalid", top_n=10)
|
||
|
|
|
||
|
|
assert isinstance(result, dict) and "error" in result, "Should return error"
|
||
|
|
print(f" [OK] Correctly rejects invalid type: {result['error']}")
|
||
|
|
print(" [PASS]")
|
||
|
|
|
||
|
|
|
||
|
|
async def test_detect_communities():
|
||
|
|
"""Test community detection."""
|
||
|
|
print("\n[TEST 4] Community Detection")
|
||
|
|
|
||
|
|
adapter = MockAdapter()
|
||
|
|
analytics = GraphAnalytics(adapter)
|
||
|
|
|
||
|
|
communities = await analytics.detect_communities(algorithm="louvain")
|
||
|
|
|
||
|
|
assert isinstance(communities, list), "Should return list"
|
||
|
|
if communities:
|
||
|
|
assert all("community_id" in c and "entities" in c for c in communities), "Should have required fields"
|
||
|
|
assert all(isinstance(c["size"], int) for c in communities), "Should have size"
|
||
|
|
|
||
|
|
print(f" [OK] Detected {len(communities)} communities")
|
||
|
|
for comm in communities:
|
||
|
|
print(f" Community {comm['community_id']}: {comm['size']} entities")
|
||
|
|
|
||
|
|
print(" [PASS]")
|
||
|
|
|
||
|
|
|
||
|
|
async def test_get_graph_statistics():
|
||
|
|
"""Test graph statistics."""
|
||
|
|
print("\n[TEST 5] Graph Statistics")
|
||
|
|
|
||
|
|
adapter = MockAdapter()
|
||
|
|
analytics = GraphAnalytics(adapter)
|
||
|
|
|
||
|
|
stats = await analytics.get_graph_statistics()
|
||
|
|
|
||
|
|
if stats:
|
||
|
|
assert "total_nodes" in stats, "Should have total_nodes"
|
||
|
|
assert "total_edges" in stats, "Should have total_edges"
|
||
|
|
assert "avg_degree" in stats, "Should have avg_degree"
|
||
|
|
assert "density" in stats, "Should have density"
|
||
|
|
|
||
|
|
print(f" [OK] Total nodes: {stats['total_nodes']}")
|
||
|
|
print(f" [OK] Total edges: {stats['total_edges']}")
|
||
|
|
print(f" [OK] Average degree: {stats['avg_degree']:.2f}")
|
||
|
|
print(f" [OK] Density: {stats['density']:.4f}")
|
||
|
|
print(f" [OK] Is connected: {stats.get('is_connected', False)}")
|
||
|
|
|
||
|
|
print(" [PASS]")
|
||
|
|
|
||
|
|
|
||
|
|
async def test_find_influential_entities():
|
||
|
|
"""Test influential entity detection."""
|
||
|
|
print("\n[TEST 6] Influential Entities")
|
||
|
|
|
||
|
|
adapter = MockAdapter()
|
||
|
|
analytics = GraphAnalytics(adapter)
|
||
|
|
|
||
|
|
influential = await analytics.find_influential_entities(top_n=10)
|
||
|
|
|
||
|
|
assert isinstance(influential, list), "Should return list"
|
||
|
|
if influential:
|
||
|
|
assert all("entity_id" in e and "composite_score" in e for e in influential), "Should have required fields"
|
||
|
|
assert all(0 <= e["composite_score"] <= 1 for e in influential), "Scores should be 0-1"
|
||
|
|
|
||
|
|
print(f" [OK] Found {len(influential)} influential entities")
|
||
|
|
for idx, entity in enumerate(influential[:3], 1):
|
||
|
|
print(
|
||
|
|
f" {idx}. {entity['label']} (score={entity['composite_score']:.3f})"
|
||
|
|
)
|
||
|
|
|
||
|
|
print(" [PASS]")
|
||
|
|
|
||
|
|
|
||
|
|
async def test_community_object():
|
||
|
|
"""Test Community data class."""
|
||
|
|
print("\n[TEST 7] Community Object")
|
||
|
|
|
||
|
|
community = Community(
|
||
|
|
community_id=1,
|
||
|
|
entities=[1, 2, 3, 4, 5],
|
||
|
|
size=5,
|
||
|
|
density=0.75,
|
||
|
|
modularity=0.42,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert community.community_id == 1, "ID should be preserved"
|
||
|
|
assert len(community.entities) == 5, "Should have 5 entities"
|
||
|
|
assert community.size == 5, "Size should be 5"
|
||
|
|
|
||
|
|
community_dict = community.to_dict()
|
||
|
|
assert "community_id" in community_dict, "Dict should have community_id"
|
||
|
|
assert community_dict["size"] == 5, "Dict should have size"
|
||
|
|
|
||
|
|
print(" [OK] Community object creation and conversion")
|
||
|
|
print(" [PASS]")
|
||
|
|
|
||
|
|
|
||
|
|
async def test_centrality_ranking():
|
||
|
|
"""Test that centrality results are ranked."""
|
||
|
|
print("\n[TEST 8] Centrality Ranking")
|
||
|
|
|
||
|
|
adapter = MockAdapter()
|
||
|
|
analytics = GraphAnalytics(adapter)
|
||
|
|
|
||
|
|
entities = await analytics.calculate_centrality(centrality_type="degree", top_n=10)
|
||
|
|
|
||
|
|
if len(entities) > 1:
|
||
|
|
assert all("rank" in e for e in entities), "Should have rank field"
|
||
|
|
assert entities[0]["rank"] == 1, "Top entity should have rank 1"
|
||
|
|
assert entities[1]["rank"] == 2, "Second entity should have rank 2"
|
||
|
|
|
||
|
|
print(f" [OK] Entities ranked correctly")
|
||
|
|
for entity in entities[:3]:
|
||
|
|
print(f" Rank {entity['rank']}: {entity['label']}")
|
||
|
|
|
||
|
|
print(" [PASS]")
|
||
|
|
|
||
|
|
|
||
|
|
async def main():
|
||
|
|
"""Run all tests."""
|
||
|
|
print("=" * 70)
|
||
|
|
print("Phase 5.2 Graph Analytics Tests")
|
||
|
|
print("=" * 70)
|
||
|
|
|
||
|
|
try:
|
||
|
|
await test_calculate_centrality_degree()
|
||
|
|
await test_calculate_centrality_pagerank()
|
||
|
|
await test_invalid_centrality_type()
|
||
|
|
await test_detect_communities()
|
||
|
|
await test_get_graph_statistics()
|
||
|
|
await test_find_influential_entities()
|
||
|
|
await test_community_object()
|
||
|
|
await test_centrality_ranking()
|
||
|
|
|
||
|
|
print("\n" + "=" * 70)
|
||
|
|
print("All tests passed!")
|
||
|
|
print("=" * 70)
|
||
|
|
print("\nPhase 5.2 Graph Analytics capabilities:")
|
||
|
|
print(" [OK] Degree centrality calculation")
|
||
|
|
print(" [OK] PageRank centrality calculation")
|
||
|
|
print(" [OK] Community detection (Louvain)")
|
||
|
|
print(" [OK] Graph statistics (density, diameter, components)")
|
||
|
|
print(" [OK] Influential entity detection")
|
||
|
|
print(" [OK] Input validation")
|
||
|
|
|
||
|
|
return True
|
||
|
|
except AssertionError as e:
|
||
|
|
print(f"\nTest failed: {e}")
|
||
|
|
import traceback
|
||
|
|
|
||
|
|
traceback.print_exc()
|
||
|
|
return False
|
||
|
|
except Exception as e:
|
||
|
|
print(f"\nUnexpected error: {e}")
|
||
|
|
import traceback
|
||
|
|
|
||
|
|
traceback.print_exc()
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
success = asyncio.run(main())
|
||
|
|
sys.exit(0 if success else 1)
|