diff --git a/tests/parse/test_parse.py b/tests/parse/test_parse.py new file mode 100644 index 0000000..14500b8 --- /dev/null +++ b/tests/parse/test_parse.py @@ -0,0 +1,482 @@ +# SPDX-License-Identifier: LGPL-2.1-only +# Copyright 2022 Jookia + +from hypothesis import assume, given +from hypothesis.strategies import ( + booleans, + composite, + integers, + lists, + one_of, + sampled_from, + text, +) + +from src.parse import ( + NoteSkipper, + ParseError, + ParseErrorException, + Parser, + parse, +) +from src.syntax import Syntax, SyntaxStream, SyntaxType +from tests.test_syntax import ( + draw_token_bool, + draw_token_by_value, + draw_token_classified, + draw_syntax_random, + draw_syntax_token, +) + + +# Inserts an element at a random place in a list +def insert_random(draw, list, data): + pos = draw(integers(min_value=1, max_value=(len(list) - 1))) + new_data = list[0:pos] + [data] + list[pos:] + 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): + token = draw(draw_syntax_token()) + assume(token.value not in ["StartText", "EndText"]) + return token + + +# Draws tokens to make a valid text string and its value +@composite +def draw_syntax_text_valid(draw): + tokens = draw(lists(draw_text_value_token())) + buffer = "" + for token in tokens: + buffer += token.value + " " + value = buffer[:-1] # Drop trailing space + start = draw(draw_token_by_value("StartText")) + end = draw(draw_token_by_value("EndText")) + all_tokens = [start] + tokens + [end] + result = Syntax(value, start.location, SyntaxType.TEXT) + return (all_tokens, result) + + +# Tests parse_text works correctly +# We expect the following behaviour: +# - Only the text expression is parsed +# - The resulting text is the value of tokens between StartText and EndText +# - The value of the tokens is joined by U+0020 SPACE code points +# - The Syntax's value is the resulting text +# - The Syntax's type is SyntaxType.TEXT +# - The Syntax's location is the StartText location +@given(draw_syntax_random(), draw_syntax_text_valid()) +def test_parse_text_valid(canary, test_data): + (tokens, result) = test_data + stream = SyntaxStream(tokens + [canary]) + parsed = Parser().parse_text(stream) + assert parsed is not None + assert parsed == result + assert stream.pop() == canary + assert stream.pop() is None + + +# Generate text without StartText +# We expect the following behaviour: +# - Error if there is no StartText node at all +# - Error if StartText is not a SyntaxType.TOKEN +# - Error if StartText's token value is not "StartText" +@composite +def draw_syntax_text_invalid_nostarttext(draw): + (tokens, _) = draw(draw_syntax_text_valid()) + if draw(booleans()): + token = draw(draw_syntax_random()) + assume(not (token.type == SyntaxType.TOKEN and token.value == "StartText")) + new_tokens = [token] + tokens[1:0] + 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: + error = ParseErrorException(ParseError.NO_TOKEN, None, None) + return ([], error) + + +# Generate text with invalid content tokens +# We expect the following behaviour: +# - Error if a content token is not a SyntaxType.TOKEN +@composite +def draw_syntax_text_invalid_invalidcontent(draw): + (tokens, _) = draw(draw_syntax_text_valid()) + token = draw(draw_syntax_random()) + assume(token.type != SyntaxType.TOKEN) + new_tokens = insert_random(draw, tokens, token) + error = ParseErrorException(ParseError.NOT_TOKEN, token, None) + return (new_tokens, error) + + +# Generate text with a StartText token in it +# We expect the following behaviour: +# - Error if a StartText token is in the text content +@composite +def draw_syntax_text_invalid_extrastarttext(draw): + (tokens, _) = draw(draw_syntax_text_valid()) + start = draw(draw_token_by_value("StartText")) + new_tokens = insert_random(draw, tokens, start) + error = ParseErrorException(ParseError.FOUND_STARTTEXT, start, None) + return (new_tokens, error) + + +# Generate text without EndText +# We expect the following behaviour: +# - Error if there is no EndText node at all +@composite +def draw_syntax_text_invalid_noendtext(draw): + (tokens, _) = draw(draw_syntax_text_valid()) + error = ParseErrorException(ParseError.NO_TOKEN, None, None) + return (tokens[0:-1], error) + + +# Generate an invalid text case +@composite +def draw_syntax_text_invalid(draw): + strategies = [ + draw_syntax_text_invalid_nostarttext(), + draw_syntax_text_invalid_invalidcontent(), + draw_syntax_text_invalid_extrastarttext(), + draw_syntax_text_invalid_noendtext(), + ] + return draw(one_of(strategies)) + + +# Test that parse_text errors in invalid cases +@given(draw_syntax_text_invalid()) +def test_parse_text_invalid(test_data): + (tokens, error) = test_data + stream = SyntaxStream(tokens) + try: + parsed = Parser().parse_text(stream) + raise AssertionError("Parsed invalid data: %s" % (parsed)) + except ParseErrorException as e: + 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())) + 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 = NoteSkipper().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 = NoteSkipper().skip_note(stream) + raise AssertionError("Parsed invalid data: %s" % (parsed)) + except ParseErrorException as e: + assert e == error + + +# Draws tokens to make a valid boolean +@composite +def draw_syntax_bool_valid(draw): + token = draw(draw_token_bool()) + value = token.value == "True" + result = Syntax(value, token.location, SyntaxType.BOOL) + return (token, result) + + +# Tests parse_bool works correctly +# We expect the following behaviour: +# - Only the first token is parsed +# - The resulting boolean is True if the first token is True +# - The resulting boolean is False if the first token is False +# - The Syntax's value is the resulting token +# - The Syntax's type is SyntaxType.BOOL +# - The Syntax's location is the first token's location +@given(draw_syntax_random(), draw_syntax_bool_valid()) +def test_parse_bool_valid(canary, test_data): + (token, result) = test_data + stream = SyntaxStream([token] + [canary]) + parsed = Parser().parse_bool(stream) + assert parsed is not None + assert parsed == result + assert stream.pop() == canary + assert stream.pop() is None + + +# Generate an invalid boolean +# We expect the following behaviour: +# - Error if there isn't a token +# - Error if the token is not a SyntaxType.TOKEN +# - Error if the token is not True or False +@composite +def draw_syntax_bool_invalid(draw): + if draw(booleans()): + token = draw(draw_syntax_random()) + assume( + not (token.type == SyntaxType.TOKEN and token.value in ["True", "False"]) + ) + if token.type == SyntaxType.TOKEN: + error = ParseErrorException(ParseError.NOT_BOOL, token, None) + else: + error = ParseErrorException(ParseError.NOT_TOKEN, token, None) + return ([token], error) + else: + error = ParseErrorException(ParseError.NO_TOKEN, None, None) + return ([], error) + + +# Test that parse_bool errors in invalid cases +@given(draw_syntax_bool_invalid()) +def test_parse_bool_invalid(test_data): + (tokens, error) = test_data + stream = SyntaxStream(tokens) + try: + parsed = Parser().parse_bool(stream) + raise AssertionError("Parsed invalid data: %s" % (parsed)) + except ParseErrorException as e: + 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: +# - Notes to be removed from the tokens +@given(lists(draw_token_classified())) +def test_parse_fuzz(tokens): + result = None + try: + stream = SyntaxStream(tokens.copy()) + result = NoteSkipper().clear_notes(stream) + except ParseErrorException as e: + result = e + try: + parsed = parse(tokens) + assert parsed == result + except ParseErrorException as e: + assert e == result diff --git a/tests/test_parse.py b/tests/test_parse.py deleted file mode 100644 index 14500b8..0000000 --- a/tests/test_parse.py +++ /dev/null @@ -1,482 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-only -# Copyright 2022 Jookia - -from hypothesis import assume, given -from hypothesis.strategies import ( - booleans, - composite, - integers, - lists, - one_of, - sampled_from, - text, -) - -from src.parse import ( - NoteSkipper, - ParseError, - ParseErrorException, - Parser, - parse, -) -from src.syntax import Syntax, SyntaxStream, SyntaxType -from tests.test_syntax import ( - draw_token_bool, - draw_token_by_value, - draw_token_classified, - draw_syntax_random, - draw_syntax_token, -) - - -# Inserts an element at a random place in a list -def insert_random(draw, list, data): - pos = draw(integers(min_value=1, max_value=(len(list) - 1))) - new_data = list[0:pos] + [data] + list[pos:] - 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): - token = draw(draw_syntax_token()) - assume(token.value not in ["StartText", "EndText"]) - return token - - -# Draws tokens to make a valid text string and its value -@composite -def draw_syntax_text_valid(draw): - tokens = draw(lists(draw_text_value_token())) - buffer = "" - for token in tokens: - buffer += token.value + " " - value = buffer[:-1] # Drop trailing space - start = draw(draw_token_by_value("StartText")) - end = draw(draw_token_by_value("EndText")) - all_tokens = [start] + tokens + [end] - result = Syntax(value, start.location, SyntaxType.TEXT) - return (all_tokens, result) - - -# Tests parse_text works correctly -# We expect the following behaviour: -# - Only the text expression is parsed -# - The resulting text is the value of tokens between StartText and EndText -# - The value of the tokens is joined by U+0020 SPACE code points -# - The Syntax's value is the resulting text -# - The Syntax's type is SyntaxType.TEXT -# - The Syntax's location is the StartText location -@given(draw_syntax_random(), draw_syntax_text_valid()) -def test_parse_text_valid(canary, test_data): - (tokens, result) = test_data - stream = SyntaxStream(tokens + [canary]) - parsed = Parser().parse_text(stream) - assert parsed is not None - assert parsed == result - assert stream.pop() == canary - assert stream.pop() is None - - -# Generate text without StartText -# We expect the following behaviour: -# - Error if there is no StartText node at all -# - Error if StartText is not a SyntaxType.TOKEN -# - Error if StartText's token value is not "StartText" -@composite -def draw_syntax_text_invalid_nostarttext(draw): - (tokens, _) = draw(draw_syntax_text_valid()) - if draw(booleans()): - token = draw(draw_syntax_random()) - assume(not (token.type == SyntaxType.TOKEN and token.value == "StartText")) - new_tokens = [token] + tokens[1:0] - 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: - error = ParseErrorException(ParseError.NO_TOKEN, None, None) - return ([], error) - - -# Generate text with invalid content tokens -# We expect the following behaviour: -# - Error if a content token is not a SyntaxType.TOKEN -@composite -def draw_syntax_text_invalid_invalidcontent(draw): - (tokens, _) = draw(draw_syntax_text_valid()) - token = draw(draw_syntax_random()) - assume(token.type != SyntaxType.TOKEN) - new_tokens = insert_random(draw, tokens, token) - error = ParseErrorException(ParseError.NOT_TOKEN, token, None) - return (new_tokens, error) - - -# Generate text with a StartText token in it -# We expect the following behaviour: -# - Error if a StartText token is in the text content -@composite -def draw_syntax_text_invalid_extrastarttext(draw): - (tokens, _) = draw(draw_syntax_text_valid()) - start = draw(draw_token_by_value("StartText")) - new_tokens = insert_random(draw, tokens, start) - error = ParseErrorException(ParseError.FOUND_STARTTEXT, start, None) - return (new_tokens, error) - - -# Generate text without EndText -# We expect the following behaviour: -# - Error if there is no EndText node at all -@composite -def draw_syntax_text_invalid_noendtext(draw): - (tokens, _) = draw(draw_syntax_text_valid()) - error = ParseErrorException(ParseError.NO_TOKEN, None, None) - return (tokens[0:-1], error) - - -# Generate an invalid text case -@composite -def draw_syntax_text_invalid(draw): - strategies = [ - draw_syntax_text_invalid_nostarttext(), - draw_syntax_text_invalid_invalidcontent(), - draw_syntax_text_invalid_extrastarttext(), - draw_syntax_text_invalid_noendtext(), - ] - return draw(one_of(strategies)) - - -# Test that parse_text errors in invalid cases -@given(draw_syntax_text_invalid()) -def test_parse_text_invalid(test_data): - (tokens, error) = test_data - stream = SyntaxStream(tokens) - try: - parsed = Parser().parse_text(stream) - raise AssertionError("Parsed invalid data: %s" % (parsed)) - except ParseErrorException as e: - 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())) - 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 = NoteSkipper().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 = NoteSkipper().skip_note(stream) - raise AssertionError("Parsed invalid data: %s" % (parsed)) - except ParseErrorException as e: - assert e == error - - -# Draws tokens to make a valid boolean -@composite -def draw_syntax_bool_valid(draw): - token = draw(draw_token_bool()) - value = token.value == "True" - result = Syntax(value, token.location, SyntaxType.BOOL) - return (token, result) - - -# Tests parse_bool works correctly -# We expect the following behaviour: -# - Only the first token is parsed -# - The resulting boolean is True if the first token is True -# - The resulting boolean is False if the first token is False -# - The Syntax's value is the resulting token -# - The Syntax's type is SyntaxType.BOOL -# - The Syntax's location is the first token's location -@given(draw_syntax_random(), draw_syntax_bool_valid()) -def test_parse_bool_valid(canary, test_data): - (token, result) = test_data - stream = SyntaxStream([token] + [canary]) - parsed = Parser().parse_bool(stream) - assert parsed is not None - assert parsed == result - assert stream.pop() == canary - assert stream.pop() is None - - -# Generate an invalid boolean -# We expect the following behaviour: -# - Error if there isn't a token -# - Error if the token is not a SyntaxType.TOKEN -# - Error if the token is not True or False -@composite -def draw_syntax_bool_invalid(draw): - if draw(booleans()): - token = draw(draw_syntax_random()) - assume( - not (token.type == SyntaxType.TOKEN and token.value in ["True", "False"]) - ) - if token.type == SyntaxType.TOKEN: - error = ParseErrorException(ParseError.NOT_BOOL, token, None) - else: - error = ParseErrorException(ParseError.NOT_TOKEN, token, None) - return ([token], error) - else: - error = ParseErrorException(ParseError.NO_TOKEN, None, None) - return ([], error) - - -# Test that parse_bool errors in invalid cases -@given(draw_syntax_bool_invalid()) -def test_parse_bool_invalid(test_data): - (tokens, error) = test_data - stream = SyntaxStream(tokens) - try: - parsed = Parser().parse_bool(stream) - raise AssertionError("Parsed invalid data: %s" % (parsed)) - except ParseErrorException as e: - 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: -# - Notes to be removed from the tokens -@given(lists(draw_token_classified())) -def test_parse_fuzz(tokens): - result = None - try: - stream = SyntaxStream(tokens.copy()) - result = NoteSkipper().clear_notes(stream) - except ParseErrorException as e: - result = e - try: - parsed = parse(tokens) - assert parsed == result - except ParseErrorException as e: - assert e == result