# SPDX-License-Identifier: LGPL-2.1-only # Copyright 2022 Jookia <contact@jookia.org> import enum from hypothesis import assume from hypothesis.strategies import composite, integers, lists from src.ast_types import Statement from src.parse import ( ParseContext, ParseError, ParseErrorException, ParseTask, Parser, read_token, ) from tests.parse.templates import ( template_parse_valid_composite, template_parse_invalid_composite, ) from tests.test_token import ( draw_token_known, draw_token_random, draw_token_unknown, static_token_by_value, ) # Values indicating what a parser did class ParserMockAction(enum.Enum): PARSE_VALUE = enum.auto() WRONG_VALUE = enum.auto() # Dummy Parser for testing statement parsing # Return a static value of ParserMockACTION.PARSE_VALUE if a token starts with "TestValue" # Otherwise throw an error of ParseMockAction.WRONG_VALUE class ParserStatementMock(Parser): def parse_value(self, stream, context): token = read_token(stream, None, context) if token.value.startswith("TestValue"): return ParserMockAction.PARSE_VALUE else: raise ParseErrorException( ParserMockAction.WRONG_VALUE, token, None, context ) # Creates a dummy parse function with a terminator specified by the last token def make_test_parser(tokens): def parser(stream, context): if tokens == []: terminator = "" else: terminator = tokens[-1].value return ParserStatementMock().parse_statement(stream, context, terminator) return parser # Draws a statement value with a somewhat random name @composite def draw_token_statement_value(draw): number = draw(integers()) return static_token_by_value("TestValue" + str(number)) # Draws a statement name @composite def draw_token_statement_name(draw): return draw(draw_token_unknown()) # Draws a statement terminator @composite def draw_token_statement_terminator(draw): return draw(draw_token_random()) # Creates a context for a token in a statement def make_test_context(parent_context, index, statement_token, token): statement_context = ParseContext( ParseTask.PARSE_STATEMENT, statement_token, parent_context ) if index == 0: context = ParseTask.PARSE_SUBJECT elif index == 1: context = ParseTask.PARSE_VERB else: context = ParseTask.PARSE_ARGUMENT context = ParseContext(context, token, statement_context) return context # Creates a context using existing tokens def make_test_context_tokens(parent_context, index, tokens): return make_test_context(parent_context, index, tokens[0], tokens[index]) # Draws a valid statement's tokens @composite def draw_token_statement(draw): values = draw(lists(draw_token_statement_value(), min_size=1)) subject = values[0] verb = [] if len(values) > 1: verb = [draw(draw_token_statement_name())] arguments = values[2:] terminator = draw(draw_token_statement_terminator()) assume(terminator not in values) assume(terminator not in verb) tokens = [subject] + verb + arguments + [terminator] return tokens # Draws a valid statement @composite def draw_token_statement_valid(draw): tokens = draw(draw_token_statement()) subject = ParserMockAction.PARSE_VALUE verb = None # Account for the terminator if len(tokens) > 2: verb = tokens[1].value argument_count = len(tokens) - 3 arguments = [ParserMockAction.PARSE_VALUE] * argument_count statement = Statement(subject, verb, arguments) return (tokens, statement) # Tests parsing a valid statement # We expect the following behaviour: # - A value is read as the subject # - Optionally, a name is read as the verb # - Optionally, any number of arguments are read as values # - A terminator is found afterwards # template_parse_valid_composite provides general parsing properties @template_parse_valid_composite def test_parse_statement_valid(draw): (tokens, expected) = draw(draw_token_statement_valid()) parser = make_test_parser(tokens) return (parser, tokens, expected) # Tests parsing a statement without a terminator # This also covers cases of premature truncation for verbs and arguments # We expect the following behaviour: # - Error reading a verb or argument # - Have ParseTask.PARSE_VERB or ParseTask.PARSE_ARGUMENT as the context's parse task # - Have ParseTask.PARSE_STATEMENT as the context's parse task's parent # - Have ParseError.NO_TOKEN as the exception code # template_parse_invalid_composite provides general parsing properties @template_parse_invalid_composite def test_parse_statement_invalid_no_terminator(draw, parent_context): tokens = draw(draw_token_statement()) truncated = tokens[:-1] context = make_test_context(parent_context, len(truncated), tokens[0], None) error = ParseErrorException(ParseError.NO_TOKEN, None, None, context) parser = make_test_parser(tokens) return (parser, truncated, error) # Tests parsing a statement with an invalid value # We expect the following behaviour: # - Error reading a invalid value on subject or argument # - Have ParseTask.PARSE_SUBJECT or ParseTask.PARSE_ARGUMENT as the context's parse task # - Have ParseTask.PARSE_STATEMENT as the context's parse task's parent # - Have ParserMockAction.WRONG_VALUE as the exception code # template_parse_invalid_composite provides general parsing properties @template_parse_invalid_composite def test_parse_statement_invalid_value(draw, parent_context): tokens = draw(draw_token_statement()) new_token = draw(draw_token_random()) assume(not new_token.value.startswith("TestValue")) # Not a value assume(new_token.value != tokens[-1].value) # Not the terminator max_chosen = len(tokens) - 2 # Ignore Terminator and Verb chosen = draw(integers(min_value=0, max_value=max_chosen)) if chosen != 0: chosen += 1 # Skip Verb new_tokens = tokens[:chosen] + [new_token] + tokens[chosen + 1 :] context = make_test_context_tokens(parent_context, chosen, new_tokens) error = ParseErrorException(ParserMockAction.WRONG_VALUE, new_token, None, context) parser = make_test_parser(tokens) return (parser, new_tokens, error) # Tests parsing a statement with an invalid verb # We expect the following behaviour: # - Error reading a known token as a verb # - Have ParseTask.PARSE_VERB as the context's parse task # - Have ParseTask.PARSE_STATEMENT as the context's parse task's parent # - Have ParseError.RESERVED_NAME as the exception code # template_parse_invalid_composite provides general parsing properties @template_parse_invalid_composite def test_parse_statement_invalid_verb(draw, parent_context): tokens = draw(draw_token_statement()) new_token = draw(draw_token_known()) assume(new_token.value != tokens[-1].value) new_tokens = tokens[:1] + [new_token] + tokens[1:] context = make_test_context_tokens(parent_context, 1, new_tokens) error = ParseErrorException(ParseError.RESERVED_NAME, new_token, None, context) parser = make_test_parser(new_tokens) return (parser, new_tokens, error) # Tests parsing an empty statement # We expect the following behaviour: # - Error reading an empty statement # - Have ParseTask.PARSE_SUBJECT as the context's parse task # - Have ParseTask.PARSE_STATEMENT as the context's parse task's parent # - Have ParserError.NO_TOKEN as the exception code # template_parse_invalid_composite provides general parsing properties @template_parse_invalid_composite def test_parse_statement_invalid_empty(draw, parent_context): tokens = [] context = make_test_context(parent_context, 0, None, None) error = ParseErrorException(ParseError.NO_TOKEN, None, None, context) parser = make_test_parser(tokens) return (parser, tokens, error)