mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-12 04:01:35 +00:00
* feat(export): add opt-in metric_errors column to DistanceExporter
Add a 'metric_errors' field to compute_pairs() output that lets
downstream consumers programmatically distinguish legitimate 'no path'
(None) from computation failures (None + error name).
Usage:
rows = exporter.compute_pairs(include=[..., 'metric_errors'])
# row['metric_errors'] == '' → all metrics succeeded
# row['metric_errors'] == 'hop_count,weighted_distance' → those failed
Design decisions:
- Opt-in: column only appears when explicitly requested via include=
- Default export schema unchanged (backward compatible)
- Comma-separated metric names (not exception messages) — stable for
programmatic filtering without exposing internal error details
- Helpers now return (value, error_name | None) tuples internally
Follow-up to #879, as discussed in its review thread.
* fix: address Qodo findings — track betweenness errors and remove unused constant
1. _betweenness() now returns (dict, error) tuple like the other helpers,
so betweenness computation failures appear in metric_errors.
2. Removed unused _ERROR_COLUMNS constant (dead code).
All 77 tests in tests/export/ pass.
* docs(changelog): add entry for opt-in metric_errors column (#960)
---------
Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com>
Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com>
118 lines
4.0 KiB
Python
118 lines
4.0 KiB
Python
"""Tests for DistanceExporter's silent-exception handling (issue #874).
|
|
|
|
Each of the four private metric helpers (_betweenness, _hop_distance,
|
|
_weighted_distance, _semantic_similarity) wraps its computation in a bare
|
|
except Exception and returns None/{} with no signal, so a raised exception is
|
|
indistinguishable in the exported data from a legitimate "no path" result.
|
|
"""
|
|
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
from semantica.export.distance_exporter import DistanceExporter
|
|
|
|
|
|
class _Node:
|
|
def __init__(self, node_id):
|
|
self.node_id = node_id
|
|
self.node_type = "t"
|
|
self.content = ""
|
|
self.properties = {}
|
|
|
|
|
|
class _Graph:
|
|
def __init__(self):
|
|
self.nodes = {"a": _Node("a"), "b": _Node("b")}
|
|
self.edges = []
|
|
|
|
|
|
class _RaisingPathFinder:
|
|
def bfs_shortest_path(self, graph_dict, src, tgt):
|
|
raise RuntimeError("bfs boom")
|
|
|
|
def dijkstra_shortest_path(self, graph_dict, src, tgt):
|
|
raise RuntimeError("dijkstra boom")
|
|
|
|
|
|
class _RaisingSimilarity:
|
|
def cosine_similarity(self, graph_dict, src, tgt):
|
|
raise RuntimeError("cosine boom")
|
|
|
|
|
|
class _RaisingCentrality:
|
|
def calculate_betweenness_centrality(self, graph_dict):
|
|
raise RuntimeError("betweenness boom")
|
|
|
|
|
|
@pytest.fixture
|
|
def exporter():
|
|
exp = DistanceExporter(_Graph())
|
|
exp._path_finder = _RaisingPathFinder()
|
|
exp._similarity = _RaisingSimilarity()
|
|
exp._centrality = _RaisingCentrality()
|
|
return exp
|
|
|
|
|
|
def test_hop_distance_logs_warning_on_exception(exporter, caplog):
|
|
with caplog.at_level(logging.WARNING, logger="semantica.export.distance_exporter"):
|
|
result = exporter._hop_distance({}, "a", "b")
|
|
value, error = result
|
|
assert value is None
|
|
assert error == "hop_count"
|
|
assert any("Hop distance" in rec.message for rec in caplog.records)
|
|
|
|
|
|
def test_weighted_distance_logs_warning_on_exception(exporter, caplog):
|
|
with caplog.at_level(logging.WARNING, logger="semantica.export.distance_exporter"):
|
|
result = exporter._weighted_distance({}, "a", "b")
|
|
value, error = result
|
|
assert value is None
|
|
assert error == "weighted_distance"
|
|
assert any("Weighted distance" in rec.message for rec in caplog.records)
|
|
|
|
|
|
def test_semantic_similarity_logs_warning_on_exception(exporter, caplog):
|
|
with caplog.at_level(logging.WARNING, logger="semantica.export.distance_exporter"):
|
|
result = exporter._semantic_similarity({}, "a", "b")
|
|
value, error = result
|
|
assert value is None
|
|
assert error == "semantic_similarity"
|
|
assert any("Semantic similarity" in rec.message for rec in caplog.records)
|
|
|
|
|
|
def test_betweenness_logs_warning_on_exception(exporter, caplog):
|
|
with caplog.at_level(logging.WARNING, logger="semantica.export.distance_exporter"):
|
|
result = exporter._betweenness({})
|
|
value, error = result
|
|
assert value == {}
|
|
assert error == "betweenness"
|
|
assert any("Betweenness" in rec.message for rec in caplog.records)
|
|
|
|
|
|
def test_compute_pairs_still_produces_none_sentinels_when_metrics_raise(exporter, caplog):
|
|
"""The exported row shape is unchanged: a raised exception still yields
|
|
None/"distant", it is just no longer silent."""
|
|
with caplog.at_level(logging.WARNING, logger="semantica.export.distance_exporter"):
|
|
rows = exporter.compute_pairs()
|
|
assert len(rows) == 2
|
|
for row in rows:
|
|
assert row["hop_count"] is None
|
|
assert row["weighted_distance"] is None
|
|
assert row["semantic_similarity"] is None
|
|
assert row["distance_band"] == "distant"
|
|
assert len(caplog.records) >= 4
|
|
|
|
|
|
def test_hop_distance_no_warning_when_kg_unavailable(caplog):
|
|
"""A legitimate 'no KG backend' None (the pre-existing early-return path)
|
|
must not be confused with an exception; nothing to log there."""
|
|
exp = DistanceExporter(_Graph())
|
|
exp._path_finder = None
|
|
with caplog.at_level(logging.WARNING, logger="semantica.export.distance_exporter"):
|
|
result = exp._hop_distance({}, "a", "b")
|
|
value, error = result
|
|
assert value is None
|
|
assert error is None
|
|
assert len(caplog.records) == 0
|