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

# Set syntax consists of the following tokens:
# - "Set"
# - Name, a value that isn't a keyword
# - "To"
# - A statement, terminated by "EndSet"
#
# Parsing gives a Set data structure containing:
# subject - The value of Name
# statement - The parsed statement
#
# The following cases are errors:
# - Not having enough tokens to parse
# - Set not being the literal "Set"
# - Name being a keyword
# - To not being the literal "To"
# - The statement not parsing correctly
#
# The following error contexts are used:
# PARSE_SET - Used when parsing the general syntax
# PARSE_SUBJECT - Used when parsing the subject
#
# The following parse errors are generated:
# NO_TOKEN - When there's not enough tokens
# WRONG_TOKEN - When Set or To aren't the correct values
# RESERVED_NAME - When Name is not a keyword
#
# The following parsers are used and have their errors
# and data structures propagated:
# parse_statement - Used with "EndSet" terminator for the statement

import enum

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

from src.ast_types import Set
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_known,
    draw_token_random,
    draw_token_unknown,
    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):
    MockValue = enum.auto()


# Mocks and tests the parse_statement parser
# Instead of parsing a complex statement it just parses
# a single token: MockStatement
# The terminator is required to be "EndSet"
class MockParser(Parser):
    def parse_statement(self, stream, parent_context, terminator):
        assert terminator == "EndSet"
        read_token(stream, "MockStatement", parent_context)
        return MockStatement.MockValue


# Draws a valid set expression and tokens
@composite
def draw_set_valid_tokens(draw):
    subject = draw(draw_token_unknown())
    tokens = [
        static_token_by_value("Set"),
        subject,
        static_token_by_value("To"),
        static_token_by_value("MockStatement"),
    ]
    expected = Set(subject.value, MockStatement.MockValue)
    return (tokens, expected)


# Calculates the parse context for a specific token in a set 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_SET, start, parent_context)
    if index == 1:
        subcontext = ParseContext(ParseTask.PARSE_SUBJECT, token, context)
        return subcontext
    else:
        return context


#
# Test functions
#

# Tests parsing a valid statement
# We expect the following behaviour:
# - The name is read as the subject
# - The statement is read as the statement
@given(draw_set_valid_tokens())
def test_parse_set_valid(test_data):
    (tokens, expected) = test_data
    parser = MockParser().parse_set
    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_SET
# - The subject has its own subcontext, PARSE_SUBJECT
@given(data())
def test_parse_set_short(data):
    (tokens, _) = data.draw(draw_set_valid_tokens(), label="valid data")
    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_set
    template_test_invalid(parser, parent_context, short_tokens, error)


# Tests parsing an invalid "Set"
# We expect the following behaviour:
# - A WRONG_TOKEN parse error is raised
# - The error context is PARSE_SET
# - The token "Set" is expected
@given(data())
def test_parse_set_wrong_set(data):
    (tokens, _) = data.draw(draw_set_valid_tokens(), label="valid data")
    new_set = data.draw(draw_token_random(), label="new set")
    assume(new_set.value != "Set")
    new_tokens = [new_set] + tokens[1:]
    parent_context = static_parse_context()
    context = context_at(parent_context, new_tokens, 0)
    error = ParseErrorException(ParseError.WRONG_TOKEN, new_set, "Set", context)
    parser = MockParser().parse_set
    template_test_invalid(parser, parent_context, new_tokens, error)


# Tests parsing an invalid "To"
# We expect the following behaviour:
# - A WRONG_TOKEN parse error is raised
# - The error context is PARSE_SET
# - The token "To" is expected
@given(data())
def test_parse_set_wrong_to(data):
    (tokens, _) = data.draw(draw_set_valid_tokens(), label="valid data")
    new_to = data.draw(draw_token_random(), label="new to")
    assume(new_to.value != "To")
    new_tokens = tokens[0:2] + [new_to] + tokens[3:]
    parent_context = static_parse_context()
    context = context_at(parent_context, new_tokens, 2)
    error = ParseErrorException(ParseError.WRONG_TOKEN, new_to, "To", context)
    parser = MockParser().parse_set
    template_test_invalid(parser, parent_context, new_tokens, error)


# Tests parsing an invalid name
# We expect the following behaviour:
# - A WRONG_TOKEN parse error is raised
# - The error context is PARSE_SET
# - The token "To" is expected
@given(data())
def test_parse_set_wrong_name(data):
    (tokens, _) = data.draw(draw_set_valid_tokens(), label="valid data")
    new_name = data.draw(draw_token_known(), label="new name")
    new_tokens = tokens[0:1] + [new_name] + tokens[2:]
    parent_context = static_parse_context()
    context = context_at(parent_context, new_tokens, 1)
    error = ParseErrorException(ParseError.RESERVED_NAME, new_name, None, context)
    parser = MockParser().parse_set
    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_SET
# - Our error context is retained by parse_statement
@given(data())
def test_parse_set_wrong_statement(data):
    (tokens, _) = data.draw(draw_set_valid_tokens(), label="valid data")
    new_statement = static_token_by_value("NotStatement")
    new_tokens = tokens[0:3] + [new_statement]
    parent_context = static_parse_context()
    context = context_at(parent_context, new_tokens, 3)
    error = ParseErrorException(
        ParseError.WRONG_TOKEN, new_statement, "MockStatement", context
    )
    parser = MockParser().parse_set
    template_test_invalid(parser, parent_context, new_tokens, error)