diff --git a/tests/parse/test_clear_notes.py b/tests/parse/test_clear_notes.py new file mode 100644 index 0000000..7d2e0ec --- /dev/null +++ b/tests/parse/test_clear_notes.py @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: LGPL-2.1-only +# Copyright 2022 Jookia + +from hypothesis import assume, given +from hypothesis.strategies import ( + composite, + lists, +) + +from src.parse import ( + NoteSkipper, + ParseError, + ParseErrorException, +) +from src.syntax import SyntaxStream +from tests.test_syntax import ( + draw_token_by_value, + draw_syntax_token, +) + + +# 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 diff --git a/tests/parse/test_note.py b/tests/parse/test_note.py new file mode 100644 index 0000000..37222f4 --- /dev/null +++ b/tests/parse/test_note.py @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: LGPL-2.1-only +# Copyright 2022 Jookia + +from hypothesis import assume, given +from hypothesis.strategies import ( + booleans, + integers, + composite, + lists, + one_of, +) + +from src.parse import ( + NoteSkipper, + ParseError, + ParseErrorException, +) +from src.syntax import SyntaxStream, SyntaxType +from tests.test_syntax import ( + draw_token_by_value, + 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 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 diff --git a/tests/parse/test_parse.py b/tests/parse/test_parse.py index b4d88c7..59edc6d 100644 --- a/tests/parse/test_parse.py +++ b/tests/parse/test_parse.py @@ -1,13 +1,11 @@ # SPDX-License-Identifier: LGPL-2.1-only # Copyright 2022 Jookia -from hypothesis import assume, given +from hypothesis import given from hypothesis.strategies import ( - booleans, composite, integers, lists, - one_of, sampled_from, text, ) @@ -18,12 +16,10 @@ ParseErrorException, parse, ) -from src.syntax import SyntaxStream, SyntaxType +from src.syntax import SyntaxStream from tests.test_syntax import ( - draw_token_by_value, draw_token_classified, draw_syntax_random, - draw_syntax_token, ) @@ -69,214 +65,6 @@ assert (except1 == except2) == equals -# 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 - - -# 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