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

# Directive syntax consists of one of the following:
# - A set
# - A conditional
# - A statement, terminated by "Done"
#
# Parsing gives one of the following:
# Set - The parsed set node
# Conditional - The parsed conditional node
# Statement - The parsed statement node
#
# The following error contexts are used:
# PARSE_DIRECTIVE - Used when parsing the directive
#
# The following parse errors are generated:
# NO_TOKEN - When there's not enough tokens
#
# The following parsers are used and have their errors
# and data structures propagated:
# parse_statement - Used with "Done" terminator

import enum

from hypothesis import given
from hypothesis.strategies import composite, just, one_of

from src.parse import (
    ParseContext,
    ParseError,
    ParseErrorException,
    ParseTask,
    Parser,
)
from tests.parse.templates import (
    template_test_valid,
    template_test_invalid,
)
from tests.test_token import (
    draw_token_unknown,
    static_token_by_value,
)
from tests.parse.test_error import static_parse_context
from src.ast_types import Bool, Statement

#
# Helper functions
#


# Values used by the mocked parser
class MockDirective(enum.Enum):
    MockSet = enum.auto()
    MockConditional = enum.auto()
    MockStatement = enum.auto()


# Mocks and tests the parse_directive parser
# Instead of parsing sets, conditionals and statements
# it instead returns a mock value
class MockParserValid(Parser):
    def parse_set(self, stream, parent_context):
        stream.pop()
        return MockDirective.MockSet

    def parse_conditional(self, stream, parent_context):
        stream.pop()
        return MockDirective.MockConditional

    def parse_statement(self, stream, parent_context, terminator):
        assert terminator == "Done"
        stream.pop()
        return MockDirective.MockStatement


# Mocks and tests the parse_directive parser error handling
# Instead of parsing, just return an error
# Re-use the enum elements to give a unique error for each node
class MockParserInvalid(Parser):
    def _raise_error(self, error, parent_context):
        raise ParseErrorException(error, None, None, parent_context)

    def parse_set(self, stream, parent_context):
        self._raise_error(MockDirective.MockSet, parent_context)

    def parse_conditional(self, stream, parent_context):
        self._raise_error(MockDirective.MockConditional, parent_context)

    def parse_statement(self, stream, parent_context, terminator):
        assert terminator == "Done"
        self._raise_error(MockDirective.MockStatement, parent_context)


# A valid directive containing a set
def static_directive_set():
    return ([static_token_by_value("Set")], MockDirective.MockSet)


# A valid directive containing a conditional
def static_directive_conditional():
    return ([static_token_by_value("If")], MockDirective.MockConditional)


# Draws a valid directive containing a statement
@composite
def draw_directive_statement(draw):
    return ([draw(draw_token_unknown())], MockDirective.MockStatement)


# Draws a valid directive
@composite
def draw_directive_valid(draw):
    return draw(
        one_of(
            [
                just(static_directive_set()),
                just(static_directive_conditional()),
                draw_directive_statement(),
            ]
        )
    )


# A simple directive tokens and result
def static_directive_valid():
    return (
        [static_token_by_value("True"), static_token_by_value("Done")],
        Statement(Bool(True), None, []),
    )


# An invalid directive token
def static_directive_invalid():
    return [static_token_by_value("Done")]


# An invalid directive token error
def static_directive_invalid_error(parent_context):
    token = static_directive_invalid()[0]
    directive_context = ParseContext(ParseTask.PARSE_DIRECTIVE, token, parent_context)
    statement_context = ParseContext(
        ParseTask.PARSE_STATEMENT, token, directive_context
    )
    context = ParseContext(ParseTask.PARSE_SUBJECT, token, statement_context)
    return ParseErrorException(ParseError.FOUND_TERMINATOR, token, None, context)


#
# Test functions
#

# Tests parsing a valid directive
# We expect the following behaviour:
# - Sets are detected and parsed
# - Conditionals are detected and parsed
# - Statements are detected and parsed
@given(draw_directive_valid())
def test_parse_directive_valid(test_data):
    (tokens, expected) = test_data
    parser = MockParserValid().parse_directive
    return template_test_valid(parser, tokens, expected)


# Tests parsing an empty directive
# We expect the following behaviour:
# - A NO_TOKEN parse error is raised
# - The error context is PARSE_DIRECTIVE
def test_parse_directive_empty():
    tokens = []
    parent_context = static_parse_context()
    context = ParseContext(ParseTask.PARSE_DIRECTIVE, None, parent_context)
    error = ParseErrorException(ParseError.NO_TOKEN, None, None, context)
    parser = MockParserValid().parse_directive
    template_test_invalid(parser, parent_context, tokens, error)


# Tests error propagation
# We expect the following behaviour:
# - A mock error is raised for each case
# - Our error context is used for the error
@given(draw_directive_valid())
def test_parse_directive_error(test_data):
    (tokens, expected) = test_data
    parent_context = static_parse_context()
    context = ParseContext(ParseTask.PARSE_DIRECTIVE, tokens[0], parent_context)
    error = ParseErrorException(expected, None, None, context)
    parser = MockParserInvalid().parse_directive
    template_test_invalid(parser, parent_context, tokens, error)