# 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 newlang.ast_types import Conditional from newlang.parse2.parse import ( ParseContext, ParseError, ParseErrorException, ParseTask, Parser, read_token, ) from tests.parse2.templates import ( template_test_valid, template_test_invalid, ) from tests.parse2.test_token import ( draw_token_random, static_token_by_value, ) from tests.parse2.test_error 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_parse2_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_parse2_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_parse2_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_parse2_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)