diff --git a/src/parse.py b/src/parse.py index 0c662c6..8c1292d 100644 --- a/src/parse.py +++ b/src/parse.py @@ -11,6 +11,7 @@ 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 # Exception thrown when a parse error is encountered @@ -71,6 +72,20 @@ return Syntax(value, location, type) +# Skip a note +def skip_note(stream): + read_token(stream, "StartNote") + while True: + s = read_token(stream, None) + # Don't allow StartNote in notes + if s.value in ["StartNote"]: + raise ParseErrorException(ParseError.FOUND_STARTNOTE, s, None) + # EndNote found, end things + elif s.value == "EndNote": + break + return None + + # Parses tokens def parse(tokens): return tokens diff --git a/tests/test_parse.py b/tests/test_parse.py index 8fab876..0fb8d1b 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -12,7 +12,7 @@ text, ) -from src.parse import ParseError, ParseErrorException, parse, parse_text +from src.parse import ParseError, ParseErrorException, parse, parse_text, skip_note from src.syntax import Syntax, SyntaxStream, SyntaxType from tests.test_syntax import ( draw_token_classified, @@ -195,6 +195,109 @@ assert e == error +# Draws a random token suitable for note building +@composite +def draw_note_value_token(draw): + token = draw(draw_syntax_token()) + assume(token.value not in ["StartNote", "EndNote"]) + return token + + +# Draws tokens to make a valid note +@composite +def draw_syntax_note_valid(draw): + tokens = draw(lists(draw_note_value_token())) + buffer = "" + for token in tokens: + buffer += token.value + " " + start = draw(draw_token_by_value("StartNote")) + end = draw(draw_token_by_value("EndNote")) + all_tokens = [start] + tokens + [end] + return all_tokens + + +# Tests skip_note works correctly +# We expect the following behaviour: +# - Only the note expression is parsed +# - No value is returned +# - All tokens are consumed up to and including EndNote +@given(draw_syntax_random(), draw_syntax_note_valid()) +def test_parse_note_valid(canary, test_data): + tokens = test_data + stream = SyntaxStream(tokens + [canary]) + skipped = skip_note(stream) + assert skipped is None + assert stream.pop() == canary + assert stream.pop() is None + + +# Generate note without StartNote +# We expect the following behaviour: +# - Error if there is no StartNote node at all +# - Error if StartNote is not a SyntaxType.TOKEN +# - Error if StartNote's token value is not "StartNote" +@composite +def draw_syntax_note_invalid_nostartnote(draw): + tokens = draw(draw_syntax_note_valid()) + if draw(booleans()): + token = draw(draw_syntax_random()) + assume(not (token.type == SyntaxType.TOKEN and token.value == "StartNote")) + new_tokens = [token] + tokens[1:0] + if token.type == SyntaxType.TOKEN: + error = ParseErrorException(ParseError.WRONG_TOKEN, token, "StartNote") + else: + error = ParseErrorException(ParseError.NOT_TOKEN, token, None) + return (new_tokens, error) + else: + error = ParseErrorException(ParseError.NO_TOKEN, None, None) + return ([], error) + + +# Generate note with a StartNote token in it +# We expect the following behaviour: +# - Error if a StartNote token is in the note content +@composite +def draw_syntax_note_invalid_extrastartnote(draw): + tokens = draw(draw_syntax_note_valid()) + start = draw(draw_token_by_value("StartNote")) + new_tokens = insert_random(draw, tokens, start) + error = ParseErrorException(ParseError.FOUND_STARTNOTE, start, None) + return (new_tokens, error) + + +# Generate note without EndNote +# We expect the following behaviour: +# - Error if there is no EndNote node at all +@composite +def draw_syntax_note_invalid_noendnote(draw): + tokens = draw(draw_syntax_note_valid()) + error = ParseErrorException(ParseError.NO_TOKEN, None, None) + return (tokens[0:-1], error) + + +# Generate an invalid note case +@composite +def draw_syntax_note_invalid(draw): + strategies = [ + draw_syntax_note_invalid_nostartnote(), + draw_syntax_note_invalid_extrastartnote(), + draw_syntax_note_invalid_noendnote(), + ] + return draw(one_of(strategies)) + + +# Test that parse_note errors in invalid cases +@given(draw_syntax_note_invalid()) +def test_parse_note_invalid(test_data): + (tokens, error) = test_data + stream = SyntaxStream(tokens) + try: + parsed = skip_note(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