diff options
| author | natsuoto <[email protected]> | 2026-07-10 16:55:33 +0530 |
|---|---|---|
| committer | natsuoto <[email protected]> | 2026-07-10 16:55:33 +0530 |
| commit | 6f27b88358e5760cdd034a77a61de7a32e77c425 (patch) | |
| tree | a251217ea65ccd08c29db92906a3331ad1efe9ad | |
| parent | 433a7af9e012f88f59da1ace228dd79245cfa4bd (diff) | |
| download | edify-6f27b88358e5760cdd034a77a61de7a32e77c425.tar.xz edify-6f27b88358e5760cdd034a77a61de7a32e77c425.zip | |
test: cover diagnose fallback branches and caller-context position fallbacks
| -rw-r--r-- | tests/builder/diagnose.test.py | 105 | ||||
| -rw-r--r-- | tests/errors/context.test.py | 112 |
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" |
