aboutsummaryrefslogtreecommitdiff
path: root/tests/errors
diff options
context:
space:
mode:
Diffstat (limited to 'tests/errors')
-rw-r--r--tests/errors/errors.test.py147
-rw-r--r--tests/errors/formatting.test.py129
-rw-r--r--tests/errors/quantifier.test.py12
3 files changed, 249 insertions, 39 deletions
diff --git a/tests/errors/errors.test.py b/tests/errors/errors.test.py
index 6472469..5b48165 100644
--- a/tests/errors/errors.test.py
+++ b/tests/errors/errors.test.py
@@ -35,131 +35,210 @@ from edify.errors.structure import (
def test_start_input_already_defined_outside_subexpression():
error = StartInputAlreadyDefinedError()
- assert "already has a start" in str(error)
- assert "ignore_start_and_end" not in str(error)
+ text = str(error)
+ assert "start_of_input has already been added" in text
+ assert "help: remove the duplicate .start_of_input()" in text
+ assert "ignore_start_and_end" not in text
def test_start_input_already_defined_in_subexpression():
error = StartInputAlreadyDefinedError(in_subexpression=True)
- assert "ignore_start_and_end" in str(error)
+ text = str(error)
+ assert "start_of_input has already been added" in text
+ assert "ignore_start_and_end=True" in text
def test_end_input_already_defined_outside_subexpression():
error = EndInputAlreadyDefinedError()
- assert "already has an end" in str(error)
- assert "ignore_start_and_end" not in str(error)
+ text = str(error)
+ assert "end_of_input has already been added" in text
+ assert "help: remove the duplicate .end_of_input()" in text
+ assert "ignore_start_and_end" not in text
def test_end_input_already_defined_in_subexpression():
error = EndInputAlreadyDefinedError(in_subexpression=True)
- assert "ignore_start_and_end" in str(error)
+ text = str(error)
+ assert "end_of_input has already been added" in text
+ assert "ignore_start_and_end=True" in text
def test_cannot_define_start_after_end():
error = CannotDefineStartAfterEndError()
- assert "start of input after defining an end" in str(error)
+ text = str(error)
+ assert "start_of_input cannot follow end_of_input" in text
+ assert "move .start_of_input() to before" in text
-def test_invalid_total_capture_groups_index():
+def test_invalid_total_capture_groups_index_reports_out_of_range():
error = InvalidTotalCaptureGroupsIndexError(5, 3)
- assert "Invalid index #5" in str(error)
- assert "only 3 capture groups" in str(error)
+ text = str(error)
+ assert "back_reference index #5 is out of range" in text
+ assert "3 capture groups" in text
+ assert "valid indices are 1 to 3" in text
+
+
+def test_invalid_total_capture_groups_index_with_no_capture_groups_prompts_add_one():
+ error = InvalidTotalCaptureGroupsIndexError(1, 0)
+ text = str(error)
+ assert "no capture groups yet" in text
+ assert "add a .capture()" in text
def test_must_be_a_string():
error = MustBeAStringError("Name", "int")
- assert "Name must be a string" in str(error)
- assert "int" in str(error)
+ text = str(error)
+ assert "Name must be a string" in text
+ assert "int" in text
+ assert "convert the value with str(...)" in text
def test_must_be_one_character():
error = MustBeOneCharacterError("Value")
- assert "Value must be one character long" in str(error)
+ text = str(error)
+ assert "Value must be one character long" in text
def test_must_be_single_character():
error = MustBeSingleCharacterError("Value", "str")
- assert "Value must be a single character" in str(error)
- assert "str" in str(error)
+ text = str(error)
+ assert "Value must be a single character" in text
+ assert "str" in text
def test_must_be_positive_integer():
error = MustBePositiveIntegerError("count")
- assert "count must be a positive integer" in str(error)
+ text = str(error)
+ assert "count must be a positive integer" in text
def test_must_be_integer_greater_than_zero():
error = MustBeIntegerGreaterThanZeroError("x")
- assert "x must be an integer greater than zero" in str(error)
+ text = str(error)
+ assert "x must be an integer greater than zero" in text
def test_must_be_instance():
error = MustBeInstanceError("Expression", "str", "RegexBuilder")
- assert "Expression must be an instance of RegexBuilder" in str(error)
- assert "str" in str(error)
+ text = str(error)
+ assert "Expression must be an instance of RegexBuilder" in text
+ assert "str" in text
-def test_must_have_a_smaller_value():
+def test_must_have_a_smaller_value_reports_the_codepoints():
error = MustHaveASmallerValueError("z", "a")
- assert "z must have a smaller character value than a" in str(error)
+ text = str(error)
+ assert "range bounds are inverted" in text
+ assert "'z'" in text and "'a'" in text
+ assert "= 122" in text and "= 97" in text
def test_must_be_less_than():
error = MustBeLessThanError("X", "Y")
- assert "X must be less than Y" in str(error)
+ text = str(error)
+ assert "X must be less than Y" in text
def test_must_be_at_least_two_operands():
error = MustBeAtLeastTwoOperandsError("any_of")
- assert "any_of requires at least two operands" in str(error)
+ text = str(error)
+ assert "any_of requires at least two operands" in text
def test_must_be_at_least_one_literal():
error = MustBeAtLeastOneLiteralError("one_of")
- assert "one_of requires at least one literal" in str(error)
+ text = str(error)
+ assert "one_of requires at least one literal" in text
def test_name_not_valid():
error = NameNotValidError("bad name")
- assert "Name bad name is not valid" in str(error)
+ text = str(error)
+ assert "'bad name' is not a valid identifier" in text
+ assert "letters, digits, and underscores" in text
def test_cannot_create_duplicate_named_group():
error = CannotCreateDuplicateNamedGroupError("dup")
- assert 'Can not create duplicate named group "dup"' in str(error)
+ text = str(error)
+ assert "named group 'dup' already exists" in text
+ assert ".named_back_reference('dup')" in text
def test_named_group_does_not_exist():
error = NamedGroupDoesNotExistError("missing")
- assert 'Named group "missing" does not exist' in str(error)
+ text = str(error)
+ assert "named group 'missing' does not exist" in text
+ assert ".named_capture('missing')" in text
def test_cannot_end_while_building_root_expression():
error = CannotEndWhileBuildingRootExpressionError()
- assert "Can not end while building the root expression" in str(error)
+ text = str(error)
+ assert "cannot .end() while building the root expression" in text
+ assert "no matching opener" in text or "no frame to close" in text
def test_cannot_call_subexpression():
error = CannotCallSubexpressionError("capture")
- assert "Can not call subexpression" in str(error)
- assert "capture" in str(error)
+ text = str(error)
+ assert "cannot merge a subexpression that has an unclosed frame" in text
+ assert "capture" in text
def test_unknown_element_type():
error = UnknownElementTypeError("WeirdElement")
- assert "WeirdElement" in str(error)
+ text = str(error)
+ assert "unknown element type 'WeirdElement'" in text
def test_non_fusable_element():
error = NonFusableElementError("DigitElement")
- assert "Cannot fuse element of type DigitElement" in str(error)
+ text = str(error)
+ assert "cannot fuse element 'DigitElement' into a character class" in text
def test_unexpected_frame_type():
error = UnexpectedFrameTypeError("DigitElement")
- assert "Stack frame anchored at unexpected element type DigitElement" in str(error)
+ text = str(error)
+ assert "stack frame anchored at unexpected element 'DigitElement'" in text
def test_failed_to_compile_regex():
error = FailedToCompileRegexError("missing )")
- assert "Cannot compile regex: missing )" in str(error)
+ text = str(error)
+ assert "rejected by the re engine" in text
+ assert "missing )" in text
+
+
+def test_every_annotated_error_message_starts_with_error_prefix():
+ errors = [
+ StartInputAlreadyDefinedError(),
+ CannotDefineStartAfterEndError(),
+ EndInputAlreadyDefinedError(),
+ InvalidTotalCaptureGroupsIndexError(1, 0),
+ MustBeAStringError("X", "int"),
+ MustBeOneCharacterError("X"),
+ MustBeSingleCharacterError("X", "int"),
+ MustBePositiveIntegerError("X"),
+ MustBeIntegerGreaterThanZeroError("X"),
+ MustBeInstanceError("X", "int", "Y"),
+ MustHaveASmallerValueError("z", "a"),
+ MustBeLessThanError("A", "B"),
+ MustBeAtLeastTwoOperandsError("f"),
+ MustBeAtLeastOneLiteralError("f"),
+ NameNotValidError("x"),
+ CannotCreateDuplicateNamedGroupError("x"),
+ NamedGroupDoesNotExistError("x"),
+ CannotEndWhileBuildingRootExpressionError(),
+ CannotCallSubexpressionError("capture"),
+ UnknownElementTypeError("X"),
+ NonFusableElementError("X"),
+ UnexpectedFrameTypeError("X"),
+ FailedToCompileRegexError("boom"),
+ ]
+ for error in errors:
+ text = str(error)
+ assert text.startswith("error:")
+ assert "help:" in text
+ assert "= note:" in text
diff --git a/tests/errors/formatting.test.py b/tests/errors/formatting.test.py
new file mode 100644
index 0000000..8cdf0ab
--- /dev/null
+++ b/tests/errors/formatting.test.py
@@ -0,0 +1,129 @@
+"""Tests for the message formatter helpers in :mod:`edify.errors.formatting`."""
+
+from unittest import mock
+
+from edify.errors.context import CallerContext
+from edify.errors.formatting import (
+ FixInsertion,
+ Problem,
+ compose_annotated_message,
+ format_error,
+ format_fix_block,
+ format_help_header,
+ format_note_line,
+ format_pointer_block,
+ format_problem,
+ format_problem_header,
+)
+
+
+def _context(source_line: str = " x = do_the_thing(1, 2, 3)", colno: int = 9) -> CallerContext:
+ return CallerContext(
+ filename="/tmp/user_code.py",
+ lineno=42,
+ colno=colno,
+ end_colno=colno + 15,
+ source_line=source_line,
+ )
+
+
+def test_format_error_with_only_summary_returns_bare_header():
+ assert format_error("boom") == "error: boom"
+
+
+def test_format_error_with_summary_and_blocks_joins_them_with_blank_line():
+ output = format_error("boom", "first block", "second block")
+ assert output == "error: boom\n\nfirst block\n\nsecond block"
+
+
+def test_format_error_skips_empty_blocks():
+ output = format_error("boom", "", "only real block", "")
+ assert output == "error: boom\n\nonly real block"
+
+
+def test_format_pointer_block_draws_caret_at_caller_column():
+ context = _context()
+ block = format_pointer_block(context, "here")
+ lines = block.splitlines()
+ assert lines[0] == " --> /tmp/user_code.py:42:9"
+ assert "^^^^^^^^^^^^^^^ here" in block
+
+
+def test_format_pointer_block_has_at_least_one_caret_when_span_is_empty():
+ context = CallerContext(
+ filename="/tmp/x.py",
+ lineno=1,
+ colno=5,
+ end_colno=5,
+ source_line="abcde",
+ )
+ block = format_pointer_block(context, "point")
+ assert "^ point" in block
+
+
+def test_format_note_line_prepends_the_note_prefix():
+ assert format_note_line("something is off") == " = note: something is off"
+
+
+def test_format_problem_header_prepends_problem_prefix():
+ assert format_problem_header("stuff") == "problem: stuff"
+
+
+def test_format_help_header_prepends_help_prefix():
+ assert format_help_header("do X") == "help: do X"
+
+
+def test_format_fix_block_inserts_text_at_the_target_column():
+ context = _context(source_line="value = compute()", colno=1)
+ insertion = FixInsertion(column=9, text="new_")
+ block = format_fix_block(context, insertion)
+ assert "value = new_compute()" in block
+ assert "++++" in block
+
+
+def test_format_problem_composes_header_pointer_help_and_fix():
+ problem_context = _context(source_line="pattern = build()", colno=11)
+ fix_context = _context(source_line="pattern = build()", colno=11)
+ fix_insertion = FixInsertion(column=11, text=".digit()")
+ problem = Problem(
+ description="missing operand",
+ problem_context=problem_context,
+ problem_hint="no operand added",
+ help_summary="add .digit() before .build()",
+ fix_context=fix_context,
+ fix_insertion=fix_insertion,
+ )
+ output = format_problem(problem)
+ assert "problem: missing operand" in output
+ assert "no operand added" in output
+ assert "help: add .digit() before .build()" in output
+ assert "++++++++" in output
+
+
+def test_compose_annotated_message_when_caller_context_is_available():
+ output = compose_annotated_message(
+ summary="bad thing happened",
+ trigger_hint="right here",
+ note="because reasons",
+ help_line="help: do X",
+ )
+ assert output.startswith("error: bad thing happened")
+ assert "-->" in output
+ assert "= note: because reasons" in output
+ assert output.rstrip().endswith("help: do X")
+
+
+def test_compose_annotated_message_falls_back_when_caller_context_is_none():
+ with mock.patch(
+ "edify.errors.formatting.capture_caller_context", return_value=None
+ ):
+ output = compose_annotated_message(
+ summary="bad thing happened",
+ trigger_hint="here",
+ note="because reasons",
+ help_line="help: do X",
+ )
+ assert output.startswith("error: bad thing happened")
+ assert "-->" not in output
+ assert "= note: because reasons" in output
+ assert output.rstrip().endswith("help: do X")
diff --git a/tests/errors/quantifier.test.py b/tests/errors/quantifier.test.py
index 27b5ddf..1256a68 100644
--- a/tests/errors/quantifier.test.py
+++ b/tests/errors/quantifier.test.py
@@ -42,14 +42,15 @@ def test_to_regex_raises_when_a_bare_quantifier_has_no_operand():
def test_dangling_message_hints_at_appending_an_operand():
- with pytest.raises(DanglingQuantifierError, match="Append an element"):
+ with pytest.raises(DanglingQuantifierError, match="append the element"):
RegexBuilder().exactly(3).to_regex_string()
def test_dangling_quantifier_error_message_contains_expected_text():
error = DanglingQuantifierError()
- assert "Dangling quantifier" in str(error)
- assert "no operand" in str(error)
+ text = str(error)
+ assert "dangling quantifier" in text
+ assert "no operand" in text
def test_stacking_one_or_more_over_exactly_raises():
@@ -84,5 +85,6 @@ def test_a_valid_quantifier_element_quantifier_element_chain_works():
def test_stacked_quantifier_error_message_contains_expected_text():
error = StackedQuantifierError()
- assert "stack" in str(error)
- assert "pending" in str(error)
+ text = str(error)
+ assert "stack a quantifier" in text
+ assert "pending quantifier" in text