aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-10 16:55:33 +0530
committernatsuoto <[email protected]>2026-07-10 16:55:33 +0530
commit6f27b88358e5760cdd034a77a61de7a32e77c425 (patch)
treea251217ea65ccd08c29db92906a3331ad1efe9ad
parent433a7af9e012f88f59da1ace228dd79245cfa4bd (diff)
downloadedify-6f27b88358e5760cdd034a77a61de7a32e77c425.tar.xz
edify-6f27b88358e5760cdd034a77a61de7a32e77c425.zip
test: cover diagnose fallback branches and caller-context position fallbacks
-rw-r--r--tests/builder/diagnose.test.py105
-rw-r--r--tests/errors/context.test.py112
2 files changed, 217 insertions, 0 deletions
diff --git a/tests/builder/diagnose.test.py b/tests/builder/diagnose.test.py
new file mode 100644
index 0000000..c61b3a9
--- /dev/null
+++ b/tests/builder/diagnose.test.py
@@ -0,0 +1,105 @@
+"""Tests for the diagnostic helpers in :mod:`edify.builder.diagnose`."""
+
+from types import SimpleNamespace
+
+import pytest
+
+from edify import Pattern, RegexBuilder
+from edify.builder.diagnose import _fallback, _frame_display_name, diagnose_unfinished
+from edify.errors.comparison import CannotCompareUnfinishedBuilderError
+
+
+def _first_pointer_hint(text: str) -> str | None:
+ for line in text.splitlines():
+ if "->" in line and ".py:" in line:
+ return line
+ return None
+
+
+def test_diagnose_returns_none_when_state_is_fully_specified():
+ finished = RegexBuilder().digit()
+ problem = diagnose_unfinished(finished._state, "left operand")
+ assert problem is None
+
+
+def test_open_group_frame_names_group_in_the_problem_description():
+ unfinished = RegexBuilder().group().digit()
+ with pytest.raises(CannotCompareUnfinishedBuilderError) as excinfo:
+ _ = unfinished == RegexBuilder()
+ text = str(excinfo.value)
+ assert "open `group()`" in text
+
+
+def test_open_capture_frame_names_capture_in_the_problem_description():
+ unfinished = RegexBuilder().capture().digit()
+ with pytest.raises(CannotCompareUnfinishedBuilderError) as excinfo:
+ _ = unfinished == RegexBuilder()
+ text = str(excinfo.value)
+ assert "open `capture()`" in text
+
+
+def test_open_named_capture_frame_names_the_name_in_the_problem_description():
+ unfinished = RegexBuilder().named_capture("year").digit()
+ with pytest.raises(CannotCompareUnfinishedBuilderError) as excinfo:
+ _ = unfinished == RegexBuilder()
+ text = str(excinfo.value)
+ assert 'named_capture("year")' in text
+
+
+def test_open_assert_ahead_frame_names_lookahead():
+ unfinished = RegexBuilder().assert_ahead().digit()
+ with pytest.raises(CannotCompareUnfinishedBuilderError) as excinfo:
+ _ = unfinished == RegexBuilder()
+ text = str(excinfo.value)
+ assert "assert_ahead()" in text
+
+
+def test_open_assert_not_ahead_frame_names_negative_lookahead():
+ unfinished = RegexBuilder().assert_not_ahead().digit()
+ with pytest.raises(CannotCompareUnfinishedBuilderError) as excinfo:
+ _ = unfinished == RegexBuilder()
+ text = str(excinfo.value)
+ assert "assert_not_ahead()" in text
+
+
+def test_open_assert_behind_frame_names_lookbehind():
+ unfinished = RegexBuilder().assert_behind().digit()
+ with pytest.raises(CannotCompareUnfinishedBuilderError) as excinfo:
+ _ = unfinished == RegexBuilder()
+ text = str(excinfo.value)
+ assert "assert_behind()" in text
+
+
+def test_open_assert_not_behind_frame_names_negative_lookbehind():
+ unfinished = RegexBuilder().assert_not_behind().digit()
+ with pytest.raises(CannotCompareUnfinishedBuilderError) as excinfo:
+ _ = unfinished == RegexBuilder()
+ text = str(excinfo.value)
+ assert "assert_not_behind()" in text
+
+
+def test_frame_display_name_falls_back_to_class_name_for_unknown_container():
+ mystery_class = type("MysteryContainer", (), {})
+ mystery_element = mystery_class()
+ fake_frame = SimpleNamespace(type_node=mystery_element)
+ assert _frame_display_name(fake_frame) == "MysteryContainer"
+
+
+def test_fallback_returns_context_when_provided():
+ dummy_context = "sentinel"
+ assert _fallback(dummy_context) == "sentinel"
+
+
+def test_fallback_returns_placeholder_when_context_is_none():
+ placeholder = _fallback(None)
+ assert placeholder.filename == "<unknown>"
+ assert placeholder.lineno == 0
+ assert placeholder.source_line == ""
+
+
+def test_dangling_quantifier_on_pattern_reports_quantifier_name():
+ unfinished_pattern = Pattern().one_or_more()
+ with pytest.raises(CannotCompareUnfinishedBuilderError) as excinfo:
+ _ = unfinished_pattern == Pattern()
+ text = str(excinfo.value)
+ assert "pending `.one_or_more()`" in text
diff --git a/tests/errors/context.test.py b/tests/errors/context.test.py
new file mode 100644
index 0000000..3b54605
--- /dev/null
+++ b/tests/errors/context.test.py
@@ -0,0 +1,112 @@
+"""Tests for the caller-source-location capture helpers in :mod:`edify.errors.context`."""
+
+from types import SimpleNamespace
+from unittest import mock
+
+from edify.errors.context import (
+ CallerContext,
+ _context_for_frame,
+ capture_caller_context,
+)
+
+
+class _FakeCode:
+ """A stand-in for ``types.CodeType`` used to force specific ``co_positions`` behaviour."""
+
+ def __init__(self, filename: str, positions: list[tuple]) -> None:
+ self.co_filename = filename
+ self._positions = list(positions)
+
+ def co_positions(self):
+ return iter(self._positions)
+
+
+class _FakeFrame:
+ """A stand-in for ``types.FrameType`` shaped for :func:`_context_for_frame`."""
+
+ def __init__(
+ self,
+ filename: str,
+ f_lineno: int,
+ f_lasti: int,
+ positions: list[tuple],
+ ) -> None:
+ self.f_code = _FakeCode(filename, positions)
+ self.f_lineno = f_lineno
+ self.f_lasti = f_lasti
+
+
+def test_capture_caller_context_returns_none_when_every_frame_is_inside_edify():
+ with mock.patch(
+ "edify.errors.context.sys._getframe", return_value=None
+ ):
+ assert capture_caller_context() is None
+
+
+def test_context_for_frame_uses_lineno_fallback_when_instruction_index_out_of_range():
+ frame = _FakeFrame(
+ filename="/tmp/fake.py",
+ f_lineno=42,
+ f_lasti=999,
+ positions=[],
+ )
+ context = _context_for_frame(frame)
+ assert context.lineno == 42
+ assert context.colno == 1
+ assert context.end_colno == 1
+
+
+def test_context_for_frame_defaults_start_line_when_position_start_line_is_none():
+ frame = _FakeFrame(
+ filename="/tmp/fake.py",
+ f_lineno=99,
+ f_lasti=0,
+ positions=[(None, None, None, None)],
+ )
+ context = _context_for_frame(frame)
+ assert context.lineno == 99
+
+
+def test_context_for_frame_defaults_end_line_when_position_end_line_is_none():
+ frame = _FakeFrame(
+ filename="/tmp/fake.py",
+ f_lineno=7,
+ f_lasti=0,
+ positions=[(5, None, 2, 4)],
+ )
+ context = _context_for_frame(frame)
+ assert context.lineno == 5
+ assert context.colno == 3
+ assert context.end_colno == 5
+
+
+def test_context_for_frame_defaults_start_col_when_position_start_col_is_none():
+ frame = _FakeFrame(
+ filename="/tmp/fake.py",
+ f_lineno=1,
+ f_lasti=0,
+ positions=[(1, 1, None, 5)],
+ )
+ context = _context_for_frame(frame)
+ assert context.colno == 1
+ assert context.end_colno == 6
+
+
+def test_context_for_frame_defaults_end_col_when_position_end_col_is_none():
+ frame = _FakeFrame(
+ filename="/tmp/fake.py",
+ f_lineno=1,
+ f_lasti=0,
+ positions=[(1, 1, 3, None)],
+ )
+ context = _context_for_frame(frame)
+ assert context.colno == 4
+ assert context.end_colno == 4
+
+
+def test_caller_context_dataclass_is_frozen_and_holds_all_five_fields():
+ context = CallerContext(
+ filename="a.py", lineno=1, colno=1, end_colno=5, source_line="x = 1"
+ )
+ assert context.filename == "a.py"
+ assert context.source_line == "x = 1"