diff --git a/src/parse.py b/src/parse.py index 8494d4a..0c662c6 100644 --- a/src/parse.py +++ b/src/parse.py @@ -1,18 +1,52 @@ # SPDX-License-Identifier: LGPL-2.1-only # Copyright 2022 Jookia +import enum from src.syntax import Syntax, SyntaxType +# Errors that can happen when parsing +class ParseError(enum.Enum): + NO_TOKEN = enum.auto() # pragma: no mutate + NOT_TOKEN = enum.auto() # pragma: no mutate + WRONG_TOKEN = enum.auto() # pragma: no mutate + FOUND_STARTTEXT = enum.auto() # pragma: no mutate + + +# Exception thrown when a parse error is encountered +class ParseErrorException(BaseException): + def __init__(self, error, syntax, expected): + self.error = error + self.syntax = syntax + self.expected = expected + + def __str__(self): + return ( + "ParseErrorException(error %s, syntax %s, expected %s)" # pragma: no mutate + % ( # pragma: no mutate + self.error, + self.syntax, + self.expected, + ) + ) + + def __eq__(self, other): + return ( + self.error == other.error + and self.syntax == other.syntax + and self.expected == other.expected + ) + + # Reads a token, possibly of a certain value def read_token(stream, value): s = stream.pop() if s is None: - return None + raise ParseErrorException(ParseError.NO_TOKEN, None, None) elif s.type != SyntaxType.TOKEN: - return None + raise ParseErrorException(ParseError.NOT_TOKEN, s, None) elif value is not None and s.value != value: - return None + raise ParseErrorException(ParseError.WRONG_TOKEN, s, value) return s @@ -20,17 +54,13 @@ def parse_text(stream): buffer = "" s = read_token(stream, "StartText") - if s is None: - return None location = s.location # Parse following tokens while True: s = read_token(stream, None) - if s is None: - return None # Don't allow StartText in text - elif s.value in ["StartText"]: - return None + if s.value in ["StartText"]: + raise ParseErrorException(ParseError.FOUND_STARTTEXT, s, None) # EndText found, end things elif s.value == "EndText": break diff --git a/tests/test_parse.py b/tests/test_parse.py index fd08c4a..8fab876 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -8,9 +8,11 @@ integers, lists, one_of, + sampled_from, + text, ) -from src import parse +from src.parse import ParseError, ParseErrorException, parse, parse_text from src.syntax import Syntax, SyntaxStream, SyntaxType from tests.test_syntax import ( draw_token_classified, @@ -27,6 +29,41 @@ return new_data +# Draws a random parse error +@composite +def draw_parse_error(draw): + return draw(sampled_from(list(ParseError))) + + +# Draws a random parse error exception +@composite +def draw_parse_error_exception(draw): + error = draw(draw_parse_error()) + syntax = draw(draw_syntax_random()) + expected = draw(text()) + return ParseErrorException(error, syntax, expected) + + +# Test parse error exception getters +@given(draw_parse_error(), draw_syntax_random(), text()) +def test_syntax_syntax_getters(error, syntax, expected): + test = ParseErrorException(error, syntax, expected) + assert test.error == error + assert test.syntax == syntax + assert test.expected == expected + + +# Test parse error exception equals +@given(draw_parse_error_exception(), draw_parse_error_exception()) +def test_syntax_syntax_equality(except1, except2): + equals = ( + except1.error == except2.error + and except1.syntax == except2.syntax + and except1.expected == except2.expected + ) + assert (except1 == except2) == equals + + # Draws a random token suitable for text building @composite def draw_text_value_token(draw): @@ -70,7 +107,7 @@ def test_parse_text_valid(canary, test_data): (tokens, result) = test_data stream = SyntaxStream(tokens + [canary]) - parsed = parse.parse_text(stream) + parsed = parse_text(stream) assert parsed is not None assert parsed == result assert stream.pop() == canary @@ -89,9 +126,14 @@ token = draw(draw_syntax_random()) assume(not (token.type == SyntaxType.TOKEN and token.value == "StartText")) new_tokens = [token] + tokens[1:0] - return new_tokens + if token.type == SyntaxType.TOKEN: + error = ParseErrorException(ParseError.WRONG_TOKEN, token, "StartText") + else: + error = ParseErrorException(ParseError.NOT_TOKEN, token, None) + return (new_tokens, error) else: - return [] + error = ParseErrorException(ParseError.NO_TOKEN, None, None) + return ([], error) # Generate text with invalid content tokens @@ -103,7 +145,8 @@ token = draw(draw_syntax_random()) assume(token.type != SyntaxType.TOKEN) new_tokens = insert_random(draw, tokens, token) - return new_tokens + error = ParseErrorException(ParseError.NOT_TOKEN, token, None) + return (new_tokens, error) # Generate text with a StartText token in it @@ -114,7 +157,8 @@ (tokens, _) = draw(draw_syntax_text_valid()) start = draw(draw_token_by_value("StartText")) new_tokens = insert_random(draw, tokens, start) - return new_tokens + error = ParseErrorException(ParseError.FOUND_STARTTEXT, start, None) + return (new_tokens, error) # Generate text without EndText @@ -123,7 +167,8 @@ @composite def draw_syntax_text_invalid_noendtext(draw): (tokens, _) = draw(draw_syntax_text_valid()) - return tokens[0:-1] + error = ParseErrorException(ParseError.NO_TOKEN, None, None) + return (tokens[0:-1], error) # Generate an invalid text case @@ -141,10 +186,13 @@ # Test that parse_text errors in invalid cases @given(draw_syntax_text_invalid()) def test_parse_text_invalid(test_data): - tokens = test_data + (tokens, error) = test_data stream = SyntaxStream(tokens) - parsed = parse.parse_text(stream) - assert parsed is None + try: + parsed = parse_text(stream) + raise AssertionError("Parsed invalid data: %s" % (parsed)) + except ParseErrorException as e: + assert e == error # Tests the parser wrapper works correctly @@ -152,5 +200,5 @@ # - Nothing happens for now @given(lists(draw_token_classified())) def test_parse_fuzz(tokens): - parsed = parse.parse(tokens) + parsed = parse(tokens) assert tokens == parsed