diff --git a/src/parse.py b/src/parse.py index d2e6551..7e1fafa 100644 --- a/src/parse.py +++ b/src/parse.py @@ -7,12 +7,14 @@ # Errors that can happen when parsing class ParseError(enum.Enum): + TEST_ERROR = enum.auto() # pragma: no mutate 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 FOUND_STARTNOTE = enum.auto() # pragma: no mutate NOT_BOOL = enum.auto() # pragma: no mutate + FOUND_ENDNOTE = enum.auto() # pragma: no mutate # Exception thrown when a parse error is encountered @@ -67,6 +69,23 @@ break return None + # Clear notes + def clear_notes(self, stream): + tokens = [] + token = stream.peek() + while token is not None: + # Found a note, skip it + if token.value == "StartNote": + self.skip_note(stream) + # EndNote found outside note + elif token.value == "EndNote": + raise ParseErrorException(ParseError.FOUND_ENDNOTE, token, None) + # Add the token if it's not note related + else: + tokens.append(stream.pop()) + token = stream.peek() + return tokens + # The recursive descent parser in a wrapper class for easy testing class Parser: diff --git a/tests/test_parse.py b/tests/test_parse.py index 2fdf302..58f328e 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -364,6 +364,114 @@ assert e == error +# Dummy parse_note implementation for testing note clearing +# This redefines skip_note to skip the StartNote and not do anything else +def clear_notes_skip_note_valid(stream): + stream.pop() + return None + + +# Dummy parse_note implementation for testing error propgation +# This redefines skip_note to always throw an error +def clear_notes_skip_note_error(stream): + s = stream.peek() + raise ParseErrorException(ParseError.TEST_ERROR, s, None) + + +# Draws a random token suitable for note clearing testing +@composite +def draw_clear_notes_value_token(draw): + token = draw(draw_syntax_token()) + assume(token.value not in ["EndNote"]) + return token + + +# Draws tokens to make a valid soup to clear notes +@composite +def draw_syntax_clear_notes_valid(draw): + tokens = draw(lists(draw_clear_notes_value_token())) + output = [] + for token in tokens: + if token.value != "StartNote": + output.append(token) + return (tokens, output) + + +# Tests clear_notes works correctly +# We expect the following behaviour: +# - When StartNote is encountered skip_note is called to skip the note +# - Other tokens are passed through +@given(draw_syntax_clear_notes_valid()) +def test_parse_clear_notes_valid(test_data): + (tokens, result) = test_data + stream = SyntaxStream(tokens) + skipper = NoteSkipper() + skipper.skip_note = clear_notes_skip_note_valid + cleared = skipper.clear_notes(stream) + assert cleared == result + + +# Draws tokens to test clear_notes error propagation +@composite +def draw_syntax_clear_notes_startnote_propagation(draw): + tokens = draw(lists(draw_clear_notes_value_token())) + # Ensure we have a StartNote somewhere + start = draw(draw_token_by_value("StartNote")) + new_tokens = tokens + [start] + for token in new_tokens: + if token.value == "StartNote": + error = ParseErrorException(ParseError.TEST_ERROR, token, None) + return (new_tokens, error) + raise AssertionError("Unable to find StartNote?") + + +# Tests clear_notes passes through skip_note errors +# We expect the following behaviour: +# - When StartNote is encountered skip_note is called to skip the note +# - Any error skip_note gives is propagated through clear_notes +@given(draw_syntax_clear_notes_startnote_propagation()) +def test_parse_clear_notes(test_data): + (tokens, error) = test_data + stream = SyntaxStream(tokens) + skipper = NoteSkipper() + skipper.skip_note = clear_notes_skip_note_error + try: + parsed = skipper.clear_notes(stream) + raise AssertionError("Parsed invalid data: %s" % (parsed)) + except ParseErrorException as e: + assert e == error + + +# Draws tokens to test clear_notes EndNote invalid error +@composite +def draw_syntax_clear_notes_invalid_endnote(draw): + tokens = draw(lists(draw_clear_notes_value_token())) + # Ensure we have an EndNote somewhere + start = draw(draw_token_by_value("EndNote")) + new_tokens = tokens + [start] + for token in new_tokens: + if token.value == "EndNote": + error = ParseErrorException(ParseError.FOUND_ENDNOTE, token, None) + return (new_tokens, error) + raise AssertionError("Unable to find EndNote?") + + +# Tests clear_notes errors when finding an EndNote +# We expect the following behaviour: +# - When EndNote is encountered a FOUND_ENDNOTE error is raised +@given(draw_syntax_clear_notes_invalid_endnote()) +def test_parse_clear_notes_invalid_endnote(test_data): + (tokens, error) = test_data + stream = SyntaxStream(tokens) + skipper = NoteSkipper() + skipper.skip_note = clear_notes_skip_note_valid + try: + parsed = skipper.clear_notes(stream) + raise AssertionError("Parsed invalid data: %s" % (parsed)) + except ParseErrorException as e: + assert e == error + + # Tests the parser wrapper works correctly # We expect the following behaviour: # - Nothing happens for now