Skip to content

Commit 8d40ba5

Browse files
committed
hardened correctness tests, avoid crashes
1 parent 533a0ba commit 8d40ba5

2 files changed

Lines changed: 57 additions & 7 deletions

File tree

src/hyperbase/correctness.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@
1010
from hyperbase.hyperedge import Atom, Hyperedge
1111

1212

13+
def _safe_mtype(edge: Hyperedge) -> str:
14+
"""Main type of ``edge``, or the sentinel ``'?'`` when it cannot be determined.
15+
16+
A malformed-but-parseable subedge (e.g. a concept used as a connector) makes
17+
:meth:`Hyperedge.type` raise. The correctness checker must *report* that as a
18+
bad-type error rather than crash, so an undeterminable type is returned as
19+
``'?'`` — a value no valid-type set accepts, so it surfaces as a bad-type
20+
error at whichever connector/argument slot it occupies.
21+
"""
22+
try:
23+
return edge.mtype()
24+
except (RuntimeError, IndexError):
25+
return "?"
26+
27+
1328
def check_correctness(
1429
edge: Hyperedge, strict: bool = False
1530
) -> dict[Hyperedge, list[tuple[str, str, int]]]:
@@ -31,7 +46,7 @@ def _check_atom(atom: Atom) -> dict[Hyperedge, list[tuple[str, str, int]]]:
3146
output: dict[Hyperedge, list[tuple[str, str, int]]] = {}
3247
errors: list[tuple[str, str]] = []
3348

34-
at = atom.mtype()
49+
at = _safe_mtype(atom)
3550
if at not in {
3651
EdgeType.CONCEPT,
3752
EdgeType.PREDICATE,
@@ -54,7 +69,7 @@ def _check_edge(
5469
output: dict[Hyperedge, list[tuple[str, str, int]]] = {}
5570
errors: list[tuple[str, str]] = []
5671

57-
ct = edge[0].mtype()
72+
ct = _safe_mtype(edge[0])
5873
# check if connector has valid type
5974
if ct not in {
6075
EdgeType.PREDICATE,
@@ -82,7 +97,7 @@ def _check_edge(
8297
("build-2-args", f"builder edge '{edge}' can only have two arguments")
8398
)
8499
for arg in edge[1:]:
85-
at = arg.mtype()
100+
at = _safe_mtype(arg)
86101
if at != EdgeType.CONCEPT:
87102
e = f"builder argument '{arg}' of '{edge}' has incorrect type: {at}"
88103
errors.append(("build-arg-bad-type", e))
@@ -93,14 +108,14 @@ def _check_edge(
93108
("trig-1-arg", f"trigger edge '{edge}' can only have one argument")
94109
)
95110
for arg in edge[1:]:
96-
at = arg.mtype()
111+
at = _safe_mtype(arg)
97112
if at not in {EdgeType.CONCEPT, EdgeType.RELATION}:
98113
e = f"trigger argument '{arg}' of '{edge}' has incorrect type: {at}"
99114
errors.append(("trig-bad-arg-type", e))
100115
# check if predicate structure is correct
101116
elif ct == EdgeType.PREDICATE:
102117
for arg in edge[1:]:
103-
at = arg.mtype()
118+
at = _safe_mtype(arg)
104119
if at not in {EdgeType.CONCEPT, EdgeType.RELATION, EdgeType.SPECIFIER}:
105120
e = f"predicate argument '{arg}' of '{edge}' has incorrect type: {at}"
106121
errors.append(("pred-arg-bad-type", e))
@@ -111,17 +126,18 @@ def _check_edge(
111126
except RuntimeError:
112127
ars = ""
113128
for i, arg in enumerate(edge[1:]):
129+
am = _safe_mtype(arg)
114130
if (
115131
i < len(ars)
116132
and ars[i] == const.ArgRole.SPECIFICATION
117-
and arg.mtype() != EdgeType.SPECIFIER
133+
and am != EdgeType.SPECIFIER
118134
):
119135
errors.append(
120136
(
121137
"spec-arg-not-specifier",
122138
f"specification argument '{arg}' of '{edge}' must "
123139
f"be a specifier (type 'S'), but has type "
124-
f"{arg.mtype()}. Wrap it in a trigger (e.g. a "
140+
f"{am}. Wrap it in a trigger (e.g. a "
125141
f"special trigger atom like _/Tt/.).",
126142
)
127143
)

tests/test_correctness_errors.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,40 @@ def test_no_argroles_on_predicate(self):
234234
assert "no-argroles" in codes
235235

236236

237+
class TestUntypeableSubedgeIsReportedNotRaised:
238+
"""A malformed-but-parseable subedge whose type can't be determined (e.g. a
239+
concept used as a connector) must be REPORTED as a bad-type error, never
240+
crash the checker. Regression: typing such a subedge as an argument used to
241+
raise ``RuntimeError: Edge is malformed, type cannot be determined`` and take
242+
down the whole parse.
243+
"""
244+
245+
def test_concept_headed_edge_as_predicate_arg(self):
246+
# (media/C content/C) is a concept heading a concept: its type cannot be
247+
# determined, so typing it (as the predicate's argument) used to raise.
248+
edge = hedge("(is/P.s (media/C content/C))")
249+
errors = check_correctness(edge) # must not raise
250+
all_codes = {c for errs in errors.values() for c, _m, _s in errs}
251+
# both the bad inner connector and the bad predicate argument are flagged
252+
assert "conn-bad-type" in all_codes
253+
assert "pred-arg-bad-type" in all_codes
254+
255+
def test_untypeable_through_modifier_chain(self):
256+
# Mirrors the real failure: a predicate argument is a modifier chain whose
257+
# innermost element is concept-headed (and therefore untypeable).
258+
edge = hedge("(is/P.s (interesting/M (media/C content/C)))")
259+
errors = check_correctness(edge) # must not raise
260+
all_codes = {c for errs in errors.values() for c, _m, _s in errs}
261+
assert "conn-bad-type" in all_codes
262+
263+
def test_strict_mode_handles_untypeable_spec_arg(self):
264+
# strict mode types the x-role argument too; that path must also be safe.
265+
edge = hedge("(give/P.sx bob/C (media/C content/C))")
266+
errors = check_correctness(edge, strict=True) # must not raise
267+
all_codes = {c for errs in errors.values() for c, _m, _s in errs}
268+
assert "conn-bad-type" in all_codes
269+
270+
237271
class TestRecursiveErrorPropagation:
238272
"""Errors in deeply nested subedges should propagate to root."""
239273

0 commit comments

Comments
 (0)