# SPDX-License-Identifier: LGPL-2.1-only # Copyright 2022 Jookia <contact@jookia.org> from hypothesis import assume, given from hypothesis.strategies import ( booleans, integers, composite, lists, one_of, ) from src.parse import ( NoteSkipper, ParseContext, ParseError, ParseErrorException, ParseTask, ) from src.syntax import SyntaxStream, SyntaxType from tests.parse.test_parse import draw_parse_context 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, None) 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()) parent_context = draw(draw_parse_context()) if draw(booleans()): token = draw(draw_syntax_random()) assume(not (token.type == SyntaxType.TOKEN and token.value == "StartNote")) new_tokens = [token] + tokens[1:0] context = ParseContext(ParseTask.PARSE_NOTE, new_tokens[0], parent_context) if token.type == SyntaxType.TOKEN: error = ParseErrorException( ParseError.WRONG_TOKEN, token, "StartNote", context ) else: error = ParseErrorException(ParseError.NOT_TOKEN, token, None, context) return (new_tokens, error, parent_context) else: context = ParseContext(ParseTask.PARSE_NOTE, None, parent_context) error = ParseErrorException(ParseError.NO_TOKEN, None, None, context) return ([], error, parent_context) # 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) parent_context = draw(draw_parse_context()) context = ParseContext(ParseTask.PARSE_NOTE, new_tokens[0], parent_context) error = ParseErrorException(ParseError.FOUND_STARTNOTE, start, None, context) return (new_tokens, error, parent_context) # 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()) parent_context = draw(draw_parse_context()) context = ParseContext(ParseTask.PARSE_NOTE, tokens[0], parent_context) error = ParseErrorException(ParseError.NO_TOKEN, None, None, context) return (tokens[0:-1], error, parent_context) # 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, context) = test_data stream = SyntaxStream(tokens) try: parsed = NoteSkipper().skip_note(stream, context) raise AssertionError("Parsed invalid data: %s" % (parsed)) except ParseErrorException as e: assert e == error