# 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 @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)