Newer
Older
NewLang / tests / parse / test_conditional.py
# SPDX-License-Identifier: LGPL-2.1-only
# Copyright 2022 Jookia <contact@jookia.org>

# Conditional syntax consists of the following tokens:
# - "If"
# - A test statement, terminated by "Then"
# - A success statement, terminated by "Else"
# - A failure statement, terminated by "EndIf"
#
# Parsing gives a Conditional data structure containing:
# test - The parsed test statement
# success - The parsed success statement
# failure - The parsed failure statement
#
# The following cases are errors:
# - If not being the literal "To"
# - The test not parsing correctly
# - The success not parsing correctly
# - The failure not parsing correctly
#
# The following error contexts are used:
# PARSE_CONDITIONAL - Used when parsing the general syntax
# PARSE_TEST - Used when parsing the test statement
# PARSE_SUCCESS - Used when parsing the success statement
# PARSE_FAILURE - Used when parsing the failure statement
#
# The following parse errors are generated:
# NO_TOKEN - When there's not enough tokens
# WRONG_TOKEN - When If isn't the correct values
#
# The following parsers are used and have their errors
# and data structures propagated:
# parse_statement - Used with "Then" terminator for the test statement
# parse_statement - Used with "Else" terminator for the success statement
# parse_statement - Used with "EndIf" terminator for the failure statement

import enum

from hypothesis import assume, given
from hypothesis.strategies import composite, data, integers, just, one_of

from src.ast_types import Conditional
from src.parse import (
    ParseContext,
    ParseError,
    ParseErrorException,
    ParseTask,
    Parser,
    read_token,
)
from tests.parse.templates import (
    template_test_valid,
    template_test_invalid,
)
from tests.test_token import (
    draw_token_random,
    static_token_by_value,
)
from tests.parse.test_parse import static_parse_context

#
# Helper functions
#


# Values used by the mocked parser
class MockStatement(enum.Enum):
    MockTest = enum.auto()
    MockSuccess = enum.auto()
    MockFailure = enum.auto()


# Mocks and tests the parse_statement parser
# Instead of parsing a complex statement it just parses
# a the following tokens: MockTest, MockSuccess, MockFailure
# The terminator is required to be:
# - "Then" for MockTest
# - "Else" for MockSuccess
# - "EndIf" for MockFailure
class MockParser(Parser):
    def parse_statement(self, stream, parent_context, terminator):
        token = read_token(stream, None, parent_context)
        if token.value == "MockTest" and terminator == "Then":
            return MockStatement.MockTest
        elif token.value == "MockSuccess" and terminator == "Else":
            return MockStatement.MockSuccess
        elif token.value == "MockFailure" and terminator == "EndIf":
            return MockStatement.MockFailure
        else:
            raise ParseErrorException(
                ParseError.WRONG_TOKEN, token, None, parent_context
            )


# A valid conditional expression and tokens
def static_conditional_valid_tokens():
    tokens = [
        static_token_by_value("If"),
        static_token_by_value("MockTest"),
        static_token_by_value("MockSuccess"),
        static_token_by_value("MockFailure"),
    ]
    expected = Conditional(
        MockStatement.MockTest, MockStatement.MockSuccess, MockStatement.MockFailure
    )
    return (tokens, expected)


# Calculates the parse context for a specific token in a conditional expression
def context_at(parent_context, tokens, index):
    max = len(tokens) - 1
    if max == -1:
        start = None
        token = None
    elif max < index:
        start = tokens[0]
        token = None
    else:
        start = tokens[0]
        token = tokens[index]
    context = ParseContext(ParseTask.PARSE_CONDITIONAL, start, parent_context)
    if index == 0:
        return context
    elif index == 1:
        subcontext = ParseContext(ParseTask.PARSE_TEST, token, context)
        return subcontext
    elif index == 2:
        subcontext = ParseContext(ParseTask.PARSE_SUCCESS, token, context)
        return subcontext
    elif index == 3:
        subcontext = ParseContext(ParseTask.PARSE_FAILURE, token, context)
        return subcontext
    else:
        assert "Should never be called"


# Draws something that isn't a mock statement
@composite
def draw_not_statement(draw):
    token = draw(draw_token_random())
    assume(token.value not in ["MockTest", "MockSuccess", "MockFailure"])
    return token


# Draws the wrong statement for a given conditional position
def draw_wrong_statement_at(index):
    random = draw_not_statement()
    test = just(static_token_by_value("MockTest"))
    success = just(static_token_by_value("MockSuccess"))
    failure = just(static_token_by_value("MockFailure"))
    if index == 1:
        return one_of([success, failure, random])
    elif index == 2:
        return one_of([test, failure, random])
    elif index == 3:
        return one_of([test, success, random])
    else:
        assert "Should never be called"


#
# Test functions
#

# Tests parsing a valid statement
# We expect the following behaviour:
# - The test statement is read and assigned
# - The success statement is read and assigned
# - The failure statement is read and assigned
def test_parse_conditional_valid():
    (tokens, expected) = static_conditional_valid_tokens()
    parser = MockParser().parse_conditional
    return template_test_valid(parser, tokens, expected)


# Tests parsing a truncated statement
# We expect the following behaviour:
# - A NO_TOKEN parse error is raised
# - The error context is PARSE_CONDITIONAL
# - The test statement has its own subcontext, PARSE_TEST
# - The success statement has its own subcontext, PARSE_SUCCESS
# - The failure statement has its own subcontext, PARSE_FAILURE
@given(data())
def test_parse_conditional_short(data):
    (tokens, _) = static_conditional_valid_tokens()
    new_len = data.draw(
        integers(min_value=0, max_value=(len(tokens) - 1)), label="shorten point"
    )
    short_tokens = tokens[0:new_len]
    parent_context = static_parse_context()
    context = context_at(parent_context, short_tokens, new_len)
    error = ParseErrorException(ParseError.NO_TOKEN, None, None, context)
    parser = MockParser().parse_conditional
    template_test_invalid(parser, parent_context, short_tokens, error)


# Tests parsing an invalid "If"
# We expect the following behaviour:
# - A WRONG_TOKEN parse error is raised
# - The error context is PARSE_CONDITIONAL
# - The token "If" is expected
@given(data())
def test_parse_conditional_wrong_if(data):
    (tokens, _) = static_conditional_valid_tokens()
    new_if = data.draw(draw_token_random(), label="new if")
    assume(new_if.value != "If")
    new_tokens = [new_if] + tokens[1:]
    parent_context = static_parse_context()
    context = context_at(parent_context, new_tokens, 0)
    error = ParseErrorException(ParseError.WRONG_TOKEN, new_if, "If", context)
    parser = MockParser().parse_conditional
    template_test_invalid(parser, parent_context, new_tokens, error)


# Tests parsing an invalid statement
# We expect the following behaviour:
# - A WRONG_TOKEN parse error is raised by the mock parser
# - The error context is PARSE_CONDITIONAL
# - Our error context is retained by parse_statement
@given(data())
def test_parse_conditional_wrong_statement(data):
    (tokens, _) = static_conditional_valid_tokens()
    statement_pos = data.draw(
        integers(min_value=1, max_value=(len(tokens) - 1)), label="statement position"
    )
    new_statement = data.draw(draw_wrong_statement_at(statement_pos))
    new_tokens = tokens[0:statement_pos] + [new_statement] + tokens[statement_pos + 1 :]
    parent_context = static_parse_context()
    context = context_at(parent_context, new_tokens, statement_pos)
    error = ParseErrorException(ParseError.WRONG_TOKEN, new_statement, None, context)
    parser = MockParser().parse_conditional
    template_test_invalid(parser, parent_context, new_tokens, error)