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

from hypothesis import assume, given
from hypothesis.strategies import (
    booleans,
    composite,
    integers,
    lists,
    one_of,
)

from src.parse import (
    ParseContext,
    ParseError,
    ParseErrorException,
    ParseTask,
    Parser,
)
from src.syntax import Syntax, SyntaxStream, SyntaxType
from tests.parse.templates import template_parse_valid
from tests.parse.test_parse import draw_parse_context
from tests.test_syntax import (
    draw_token_by_value,
    draw_syntax_random,
    draw_syntax_token,
)


# Inserts an element at a random place in a list
def insert_random(draw, list, data):
    pos = draw(integers(min_value=1, max_value=(len(list) - 1)))
    new_data = list[0:pos] + [data] + list[pos:]
    return new_data


# Draws a random token suitable for text building
@composite
def draw_text_value_token(draw):
    token = draw(draw_syntax_token())
    assume(token.value not in ["StartText", "EndText"])
    return token


# Draws tokens to make a valid text string and its value
@composite
def draw_syntax_text_valid(draw):
    tokens = draw(lists(draw_text_value_token()))
    buffer = ""
    for token in tokens:
        buffer += token.value + " "
    value = buffer[:-1]  # Drop trailing space
    start = draw(draw_token_by_value("StartText"))
    end = draw(draw_token_by_value("EndText"))
    all_tokens = [start] + tokens + [end]
    result = Syntax(value, start.location, SyntaxType.TEXT)
    return (all_tokens, result)


# Tests parse_text works correctly
# We expect the following behaviour:
# - The resulting text is the value of tokens between StartText and EndText
# - The value of the tokens is joined by U+0020 SPACE code points
# - The Syntax's value is the resulting text
# - The Syntax's type is SyntaxType.TEXT
# template_parse_valid provides general parsing properties
@template_parse_valid(Parser().parse_text, draw_syntax_text_valid())
def test_parse_text_valid():
    pass


# Generate text without StartText
# We expect the following behaviour:
# - Error if there is no StartText node at all
# - Error if StartText is not a SyntaxType.TOKEN
# - Error if StartText's token value is not "StartText"
@composite
def draw_syntax_text_invalid_nostarttext(draw):
    (tokens, _) = draw(draw_syntax_text_valid())
    parent_context = draw(draw_parse_context())
    if draw(booleans()):
        token = draw(draw_syntax_random())
        assume(not (token.type == SyntaxType.TOKEN and token.value == "StartText"))
        new_tokens = [token] + tokens[1:0]
        context = ParseContext(ParseTask.PARSE_TEXT, new_tokens[0], parent_context)
        if token.type == SyntaxType.TOKEN:
            error = ParseErrorException(
                ParseError.WRONG_TOKEN, token, "StartText", context
            )
        else:
            error = ParseErrorException(ParseError.NOT_TOKEN, token, None, context)
        return (new_tokens, error, parent_context)
    else:
        context = ParseContext(ParseTask.PARSE_TEXT, None, parent_context)
        error = ParseErrorException(ParseError.NO_TOKEN, None, None, context)
        return ([], error, parent_context)


# Generate text with invalid content tokens
# We expect the following behaviour:
# - Error if a content token is not a SyntaxType.TOKEN
@composite
def draw_syntax_text_invalid_invalidcontent(draw):
    (tokens, _) = draw(draw_syntax_text_valid())
    token = draw(draw_syntax_random())
    assume(token.type != SyntaxType.TOKEN)
    new_tokens = insert_random(draw, tokens, token)
    parent_context = draw(draw_parse_context())
    context = ParseContext(ParseTask.PARSE_TEXT, new_tokens[0], parent_context)
    error = ParseErrorException(ParseError.NOT_TOKEN, token, None, context)
    return (new_tokens, error, parent_context)


# Generate text with a StartText token in it
# We expect the following behaviour:
# - Error if a StartText token is in the text content
@composite
def draw_syntax_text_invalid_extrastarttext(draw):
    (tokens, _) = draw(draw_syntax_text_valid())
    start = draw(draw_token_by_value("StartText"))
    new_tokens = insert_random(draw, tokens, start)
    parent_context = draw(draw_parse_context())
    context = ParseContext(ParseTask.PARSE_TEXT, new_tokens[0], parent_context)
    error = ParseErrorException(ParseError.FOUND_STARTTEXT, start, None, context)
    return (new_tokens, error, parent_context)


# Generate text without EndText
# We expect the following behaviour:
# - Error if there is no EndText node at all
@composite
def draw_syntax_text_invalid_noendtext(draw):
    (tokens, _) = draw(draw_syntax_text_valid())
    parent_context = draw(draw_parse_context())
    context = ParseContext(ParseTask.PARSE_TEXT, tokens[0], parent_context)
    error = ParseErrorException(ParseError.NO_TOKEN, None, None, context)
    return (tokens[0:-1], error, parent_context)


# Generate an invalid text case
@composite
def draw_syntax_text_invalid(draw):
    strategies = [
        draw_syntax_text_invalid_nostarttext(),
        draw_syntax_text_invalid_invalidcontent(),
        draw_syntax_text_invalid_extrastarttext(),
        draw_syntax_text_invalid_noendtext(),
    ]
    return draw(one_of(strategies))


# Test that parse_text errors in invalid cases
@given(draw_syntax_text_invalid())
def test_parse_text_invalid(test_data):
    (tokens, error, context) = test_data
    stream = SyntaxStream(tokens)
    try:
        parsed = Parser().parse_text(stream, context)
        raise AssertionError("Parsed invalid data: %s" % (parsed))
    except ParseErrorException as e:
        assert e == error