Newer
Older
NewLang / tests / parse / test_statement.py
# 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)