From 855884b74fcad68ba2b8e5c7b828222fcf581010 Mon Sep 17 00:00:00 2001 From: mrom1 Date: Thu, 18 Jul 2024 01:18:02 +0200 Subject: [PATCH] Fixed: - Fixed leading zero issue in decimal number for lex token interpreter, e.g. +012345 is now interpreted correctly. - Added support for UTF-8, UTF-16, UTF-32, ISO encoding for A2L input files. - Fixed handling of empty blocks inside IF_DATA, e.g. "/begin TEST_X /end TEST_X" (allowing and saving to AST now). Refactor: - Moved a2lparser/a2lparser.py to a2lparser/main.py - Moved a2lparser/a2l/parser.py to a2lparser/a2lparser.py - Moved a2lparser/a2l/parsing_exception.py to a2lparser/a2lparser_exception.py - Moved Loguru format initialization to A2LParser constructor. - Changed PyPI and build workflow names --- .github/workflows/build.yml | 2 +- .github/workflows/publish-to-pypi.yml | 2 +- README.md | 78 ++--- a2lparser/__init__.py | 5 +- a2lparser/a2l/a2l_yacc.py | 14 +- a2lparser/a2l/lex/lexer_regex.py | 2 +- a2lparser/a2l/parser.py | 156 ---------- a2lparser/a2l/rules/rules_sections.py | 9 +- a2lparser/a2lparser.py | 269 +++++++++-------- ...ng_exception.py => a2lparser_exception.py} | 2 +- a2lparser/converter/json_converter.py | 9 +- a2lparser/converter/xml_converter.py | 12 +- a2lparser/converter/yaml_converter.py | 4 + a2lparser/main.py | 149 ++++++++++ pyproject.toml | 2 +- setup.py | 2 +- .../test_integration_asap2_demo_v161.py | 4 +- .../test_integration_asap2_demo_v171.py | 4 +- .../test_integration_nested_includes.py | 4 +- tests/integration/test_integration_version.py | 2 +- tests/lex/test_lex_datatypes.py | 2 +- tests/parser/test_parser_find_includes.py | 4 +- tests/parser/test_parser_load_file.py | 6 +- tests/rules/test_rules_if_data.py | 275 ++++++++++++++++-- 24 files changed, 651 insertions(+), 367 deletions(-) delete mode 100644 a2lparser/a2l/parser.py rename a2lparser/{a2l/parsing_exception.py => a2lparser_exception.py} (97%) create mode 100644 a2lparser/main.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b624a6..3ec3644 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: pypi +name: build # Controls when the action will run. on: diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 7bbf4a1..78fb986 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -1,4 +1,4 @@ -name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI +name: pypi publish on: push diff --git a/README.md b/README.md index d479f97..bd59ff9 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,39 @@ # Python A2L Parser -![main_workflow](https://github.com/mrom1/a2lparser/actions/workflows/main.yml/badge.svg) -![build_workflow](https://github.com/mrom1/a2lparser/actions/workflows/build.yml/badge.svg) -![flake8_workflow](https://github.com/mrom1/a2lparser/actions/workflows/flake8.yml/badge.svg) +![Main Workflow](https://github.com/mrom1/a2lparser/actions/workflows/main.yml/badge.svg) +![PyPI Workflow](https://github.com/mrom1/a2lparser/actions/workflows/publish-to-pypi.yml/badge.svg) +![Build Workflow](https://github.com/mrom1/a2lparser/actions/workflows/build.yml/badge.svg) +![Flake8 Workflow](https://github.com/mrom1/a2lparser/actions/workflows/flake8.yml/badge.svg) [![codecov](https://codecov.io/gh/mrom1/a2lparser/branch/main/graph/badge.svg?token=CZ74J83NO2)](https://codecov.io/gh/mrom1/a2lparser) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -This A2L Parser, implemented in Python using [PLY](https://ply.readthedocs.io/en/latest/index.html), serves the purpose of reading A2L files according to the [ASAM MCD-2 MC](https://www.asam.net/standards/detail/mcd-2-mc/) Data Model for ECU Measurement and Calibration Standard. All resources utilized in developing this project are derived from publicly available information, such as the [ASAM Wiki](https://www.asam.net/standards/detail/mcd-2-mc/wiki/). +## Overview -This Python module enables the parsing of A2L files into an Abstract Syntax Tree with dictionary access in Python. Moreover, it provides functionality to convert the parsed A2L file into simpler formats, currently supporting XML, JSON, or YAML conversions. It's important to note that this project focuses solely on parsing the A2L grammar and does not provide mapping capabilities. It has been entirely rewritten from the original codebase and now fully supports ASAM MCD-2 MC Version 1.7.1. +The Python A2L Parser is a tool designed for reading A2L files compliant with the [ASAM MCD-2 MC](https://www.asam.net/standards/detail/mcd-2-mc/) Data Model for ECU Measurement and Calibration. This parser, implemented in Python using [PLY](https://ply.readthedocs.io/en/latest/index.html), constructs an Abstract Syntax Tree (AST) from A2L files, allowing for structured data access and utility functions like searching. -Released under the GPL license with no warranty, it is recommended primarily for educational purposes. For professional solutions, consider exploring specialized companies in this domain, such as the [MATLAB Vehicle Network Toolbox](https://www.mathworks.com/help/vnt/index.html) or the [Vector ASAP2 Toolset](https://www.vector.com/int/en/products/products-a-z/software/asap2-tool-set/). +This project supports ASAM MCD-2 MC Version 1.7.1 and focuses on parsing A2L grammar, not providing mapping capabilities. The module also includes functionality for converting parsed A2L files into simpler formats like XML, JSON, and YAML. -## Installation - -```console -pip install a2lparser -``` +You can use this repository to interpret A2L files, build upon this functionality, or for educational purposes. -## Usage from CLI +**Note:** This project is released under the GPL license with no warranty and is recommended for educational purposes. For professional solutions, consider exploring specialized tools such as the [MATLAB Vehicle Network Toolbox](https://www.mathworks.com/help/vnt/index.html) or the [Vector ASAP2 Toolset](https://www.vector.com/int/en/products/products-a-z/software/asap2-tool-set/). -```console -❯ a2lparser --help -usage: a2lparser [-h] [-x] [-j] [-y] [--no-prompt] [--no-optimize] [--no-validation] [--output-dir [OUTPUT_DIR]] - [--gen-ast [GEN_AST]] [--version] - [filename] +## Installation -positional arguments: - filename A2L file(s) to parse +To install the A2L Parser, run: -options: - -h, --help show this help message and exit - -x, --xml Converts an A2L file to a XML output file - -j, --json Converts an A2L file to a JSON output file - -y, --yaml Converts an A2L file to a YAML output file - --no-prompt Disables CLI prompt after parsing - --no-validation Disables possible A2L validation warnings - --output-dir [OUTPUT_DIR] - Output directory for converted files - --gen-ast [GEN_AST] Generates python file containing AST node classes - --version show program's version number and exit - ``` +```console +pip install -i https://test.pypi.org/simple/ a2lparser --extra-index-url https://pypi.org/simple/ +``` ## Usage as Module ```python -from a2lparser.a2l.parser import Parser +from a2lparser.a2lparser import A2LParser +from a2lparser.a2lparser_exception import A2LParserException try: # Create Parser and parse files - ast = Parser().parse_files(files="./data/test.a2l") + ast = A2LParser(quiet=True).parse_file(files="./data/test.a2l") # Dictionary access on abstract syntax tree module = ast["test.a2l"]["PROJECT"]["MODULE"] @@ -58,6 +42,32 @@ try: measurements = ast.find_sections("MEASUREMENT") print(measurements) -except Exception as ex: +except A2LParserException as ex: print(ex) ``` + +## Usage from CLI + +```console +❯ a2lparser --help +usage: a2lparser [-h] [-x] [-j] [-y] [--output-dir [PATH]] [--prompt] [--quiet] [--no-optimize] [--no-validation] + [--gen-ast [CONFIG]] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--version] + [file] + +positional arguments: + file A2L files to parse + +options: + -h, --help show this help message and exit + -x, --xml Converts an A2L file to a XML output file + -j, --json Converts an A2L file to a JSON output file + -y, --yaml Converts an A2L file to a YAML output file + --output-dir [PATH] Output directory for converted files + --prompt Enables CLI prompt after parsing + --quiet Disables console output + --no-optimize Disables optimization mode + --no-validation Disables possible A2L validation warnings + --gen-ast [CONFIG] Generates python file containing AST node classes + --log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL} + --version show program's version number and exit ``` +``` diff --git a/a2lparser/__init__.py b/a2lparser/__init__.py index 0d94711..27df7a8 100644 --- a/a2lparser/__init__.py +++ b/a2lparser/__init__.py @@ -58,7 +58,7 @@ Node() except ImportError: - print("First time initialization...") + print("No AST node classes found. Generating AST nodes...") from a2lparser.a2l.ast.ast_generator import ASTGenerator # Generate the AST nodes from the standard config in configs/A2L_ASAM.cfg @@ -66,7 +66,6 @@ ast_nodes_file = A2L_GENERATED_FILES_DIR / "a2l_ast.py" # Generate the AST node containers - print("Generating python file containing the AST nodes...") generator = ASTGenerator(asam_config.as_posix(), ast_nodes_file.as_posix()) generator.generate() - print(f"Generated file at: {ast_nodes_file.as_posix()}") + print(f"Generated AST nodes file at: {ast_nodes_file.as_posix()}") diff --git a/a2lparser/a2l/a2l_yacc.py b/a2lparser/a2l/a2l_yacc.py index 71b5827..6f43cf7 100644 --- a/a2lparser/a2l/a2l_yacc.py +++ b/a2lparser/a2l/a2l_yacc.py @@ -29,7 +29,7 @@ from a2lparser.a2l.rules.rules_sections import RulesSections from a2lparser.a2l.rules.rules_datatypes import RulesDatatypes from a2lparser.a2l.rules.rules_sections_errorhandlers import RulesSectionsErrorhandlers -from a2lparser.a2l.parsing_exception import ParsingException +from a2lparser.a2lparser_exception import A2LParserException from a2lparser.a2l.ast.abstract_syntax_tree import AbstractSyntaxTree import a2lparser.gen.a2l_ast as ASTNodes @@ -82,19 +82,23 @@ def __init__( self.debug = debug self.a2l_sections_list = [] - def generate_ast(self, content: str) -> AbstractSyntaxTree: + def generate_ast(self, content: str, show_progressbar: bool = True) -> AbstractSyntaxTree: """ Generates an AbstractSyntaxTree from an input string. """ content_lines = content.count("\n") - with alive_bar(content_lines) as progressbar: - self.a2l_lex.progressbar = progressbar + + if show_progressbar: + with alive_bar(content_lines) as progressbar: + self.a2l_lex.progressbar = progressbar + ast = self.a2l_yacc.parse(input=content, lexer=self.a2l_lex, debug=self.debug) + else: ast = self.a2l_yacc.parse(input=content, lexer=self.a2l_lex, debug=self.debug) if hasattr(ast, "node") and ast.node is not None: return AbstractSyntaxTree(ast.node) - raise ParsingException("Unable to parse given input. Generated AST is empty!") + raise A2LParserException("Unable to parse given input. Generated AST is empty!") ################################################## # General Parsing rules and starting point. # diff --git a/a2lparser/a2l/lex/lexer_regex.py b/a2lparser/a2l/lex/lexer_regex.py index 55eaa95..3d90bd9 100644 --- a/a2lparser/a2l/lex/lexer_regex.py +++ b/a2lparser/a2l/lex/lexer_regex.py @@ -46,7 +46,7 @@ class LexerRegex: hex_digits = "[0-9a-fA-F]+" bin_prefix = "[+-]?0[bB]" integer_suffix_opt = r"(([uU]ll)|([uU]LL)|(ll[uU]?)|(LL[uU]?)|([uU][lL])|([lL][uU]?)|[uU])?" - decimal_constant = f"([+-]?0{integer_suffix_opt})|([+-]?[1-9][0-9]*{integer_suffix_opt})" + decimal_constant = f"[+-]?([0-9]+){integer_suffix_opt}" hex_prefix = "[+-]?0[xX]" hex_constant = hex_prefix + hex_digits + integer_suffix_opt exponent_part = r"""([eE][-+]?[0-9]+)""" diff --git a/a2lparser/a2l/parser.py b/a2lparser/a2l/parser.py deleted file mode 100644 index 636e419..0000000 --- a/a2lparser/a2l/parser.py +++ /dev/null @@ -1,156 +0,0 @@ -####################################################################################### -# a2lparser: https://github.com/mrom1/a2lparser # -# author: https://github.com/mrom1 # -# # -# This file is part of the a2lparser package. # -# # -# a2lparser is free software: you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the # -# Free Software Foundation, either version 3 of the License, or (at your option) # -# any later version. # -# # -# a2lparser is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # -# or FITNESS FOR A PARTICULAR PURPOSE. # -# See the GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with a2lparser. If not, see . # -####################################################################################### - - -import os -import re -import glob -from pathlib import Path -from loguru import logger -from a2lparser.a2l.a2l_yacc import A2LYacc -from a2lparser.a2l.a2l_validator import A2LValidator -from a2lparser.a2l.parsing_exception import ParsingException -from a2lparser.a2l.ast.abstract_syntax_tree import AbstractSyntaxTree - - -class Parser: - """ - Parser class for parsing A2L content. - - Usage: - >>> try: - >>> parser = Parser() - >>> ast = parser.parse_files(files="./data/*.a2l") - >>> except ParsingException as ex: - >>> print(ex) - """ - - def __init__(self, optimize: bool = True, validation: bool = True) -> None: - """ - Parser Constructor. - - Args: - optimize: Will optimize the lex and yacc parsing process. - validation: Will validate the A2L content before parsing. - """ - self.validation = validation - self.parser = A2LYacc(optimize=optimize) - self._include_pattern = re.compile(r""" - /include # matches literal string "/include" - \s+ # matches one or more whitespaces - ( # start of the capturing group for the filename - [^\s"']+ # matches any character that is not whitespace or a quotation mark - | # OR - "[^"]*" # matches a quoted string (double quotes) capturing the content inside the quotes - | # OR - '[^']*' # matches a quoted string (single quotes) capturing the content inside the quotes - ) # end of the capturing group - """, re.IGNORECASE | re.VERBOSE) - - def parse_files(self, files: str) -> dict: - """ - Parses the given files. - Returns a dictionary of AbstractSyntaxTree objects with the file name as a key pair. - """ - ast_objects = {} - - # Glob A2L input files - a2l_files = glob.glob(files) - if not a2l_files: - raise ParsingException(f"Unable to find any A2L files matching: \"{files}\"") - - for a2l_file in a2l_files: - try: - # Load content from file into memory - a2l_content = self._load_file(filename=a2l_file) - - # Validate the content read - if self.validation: - try: - A2LValidator().validate(a2l_content) - except A2LValidator.A2LValidationError as e: - logger.warning(f"WARNING: Validation of file \"{a2l_file}\" failed!\n{e}") - - # Parse the content - filename = os.path.basename(a2l_file) - logger.info("Parsing file: {}", filename) - ast_objects[filename] = self._parse_content(content=a2l_content) - - except ParsingException as e: - logger.error(f"Unable to parse file \"{a2l_file}\": {e}") - - return ast_objects - - def _parse_content(self, content: str) -> AbstractSyntaxTree: - """ - Parses the given content string and returns an AbstractSyntaxTree object. - """ - return self.parser.generate_ast(content) - - def _load_file(self, filename: str, current_dir: str = None) -> str: - """ - Reads the content of the given filename and returns it with includes replaced recursively. - - Args: - filename (str): The filename of the A2L file to be read. - - Returns: - str: The complete A2L file with the included content. - """ - a2l_file = Path(filename) - if current_dir is None: - current_dir = a2l_file.parent - file_path = current_dir / a2l_file.name - else: - file_path = current_dir / a2l_file - file_path = file_path.resolve() - current_dir = file_path.parent - - with open(file_path, "r", encoding="utf-8") as file: - content = file.read() - - if includes := self._find_includes(content): - if isinstance(includes, list): - for include in includes: - included_content = self._load_file(include, current_dir) - content = self._include_pattern.sub(included_content, content, count=1) - else: - included_content = self._load_file(includes, current_dir) - content = self._include_pattern.sub(included_content, content) - return content - - def _find_includes(self, content: str) -> str | list | None: - """ - Looks for /include {file.a2l} tags inside given content and returns the full filename. - - Args: - content (str): The content of the A2L file. - - Returns: - str | list | None: The filenames to be included if found. - """ - try: - matches = self._include_pattern.findall(content) - if len(matches) == 1: - return matches[0].strip("\"'") - if len(matches) > 1: - return [match.strip("\"'") for match in matches] - except Exception as e: - raise ParsingException(e) from e diff --git a/a2lparser/a2l/rules/rules_sections.py b/a2lparser/a2l/rules/rules_sections.py index 2c70014..809c562 100644 --- a/a2lparser/a2l/rules/rules_sections.py +++ b/a2lparser/a2l/rules/rules_sections.py @@ -1342,7 +1342,8 @@ def p_if_data_opt_block(self, p): """ if_data_opt : if_data_block """ - p[0] = p[1] + if p[1]: + p[0] = p[1] def p_if_data_opt_list(self, p): """ @@ -1363,6 +1364,12 @@ def p_if_data_block(self, p): data_params = [x for x in p[2] if not isinstance(x, ASTNodes.If_Data_Block)] p[0] = ASTNodes.If_Data_Block(Name=p[1], DataParams=data_params, If_Data_Block=if_data_block) + def p_if_data_block_empty(self, p): + """ + if_data_block : if_data_block_begin if_data_block_end + """ + p[0] = ASTNodes.If_Data_Block(Name=p[1]) + def p_if_data_block_begin(self, p): """ if_data_block_begin : BEGIN ident diff --git a/a2lparser/a2lparser.py b/a2lparser/a2lparser.py index 3614fae..a715079 100644 --- a/a2lparser/a2lparser.py +++ b/a2lparser/a2lparser.py @@ -20,128 +20,165 @@ import os +import re import sys -import argparse +import glob +from pathlib import Path from loguru import logger -from a2lparser import __version__ -from a2lparser import A2L_CONFIGS_DIR -from a2lparser import A2L_PARSER_HEADLINE -from a2lparser import A2L_DEFAULT_CONFIG_NAME -from a2lparser import A2L_GENERATED_FILES_DIR -from a2lparser.a2l.parser import Parser -from a2lparser.cli.command_prompt import CommandPrompt -from a2lparser.a2l.ast.ast_generator import ASTGenerator -from a2lparser.converter.xml_converter import XMLConverter -from a2lparser.converter.json_converter import JSONConverter -from a2lparser.converter.yaml_converter import YAMLConverter - - -@logger.catch -def main() -> None: - """ - Main function of the a2lparser. - - Usage through installation with pip: - $ a2lparser --help +from a2lparser.a2l.a2l_yacc import A2LYacc +from a2lparser.a2l.a2l_validator import A2LValidator +from a2lparser.a2lparser_exception import A2LParserException +from a2lparser.a2l.ast.abstract_syntax_tree import AbstractSyntaxTree - Usage from root project dir: - $ python -m a2lparser.a2lparser --help - Documentation at: https://github.com/mrom1/a2lparser +class A2LParser: + """ + Parser class for parsing A2L content. + + Usage: + >>> try: + >>> parser = Parser() + >>> ast = parser.parse_file(files="./data/*.a2l") + >>> except ParsingException as ex: + >>> print(ex) """ - try: - args = parse_arguments(sys.argv[1:]) - # Set the logger - logger.remove() - logger.add( - sink=sys.stdout, - format="[{time:HH:mm:ss}] {message}", - level="INFO", + def __init__(self, validation: bool = True, optimize: bool = True, log_level: str = "INFO", quiet: bool = False) -> None: + """ + Parser Constructor. + + Args: + validation: Will validate the A2L content before parsing. + optimize: Will optimize the lex and yacc parsing process. + log_level: The log level of the console output. + quiet: Will not log anything to the console. + """ + self.validation = validation + self.quiet_mode = quiet + self.show_progressbar = log_level in {"DEBUG", "INFO"} and not quiet + self.parser = A2LYacc(optimize=optimize) + self._include_pattern = re.compile( + r""" + /include # matches literal string "/include" + \s+ # matches one or more whitespaces + ( # start of the capturing group for the filename + [^\s"']+ # matches any character that is not whitespace or a quotation mark + | # OR + "[^"]*" # matches a quoted string (double quotes) capturing the content inside the quotes + | # OR + '[^']*' # matches a quoted string (single quotes) capturing the content inside the quotes + ) # end of the capturing group + """, + re.IGNORECASE | re.VERBOSE, ) - - # Print header - print(A2L_PARSER_HEADLINE) - - # Generates the AST node classes for the A2L objects using the ASTGenerator - if args.gen_ast: - print("Generating python file containing the AST nodes...") - if args.gen_ast == A2L_DEFAULT_CONFIG_NAME: - config_file = A2L_CONFIGS_DIR / A2L_DEFAULT_CONFIG_NAME - elif os.path.isfile(args.gen_ast): - config_file = args.gen_ast - else: - print(f"Given config file {args.gen_ast} not found. Aborting AST generation.") - sys.exit(1) - print("Generating AST nodes from config at: ", config_file.as_posix()) - generated_file = A2L_GENERATED_FILES_DIR / "a2l_ast.py" - generator = ASTGenerator(config_file.as_posix(), generated_file.as_posix()) - generator.generate() - print(f"Generated {generated_file.as_posix()}") - sys.exit(0) - - # Provide a file or a collection of A2L-files to parse. - if args.file is None: - print() - print("\nPlease specify a A2L file.") - print("For more information use the -h or --help flag.") - sys.exit(1) - - # Initializing the A2L Parser - parser = Parser(optimize=not args.no_optimize, validation=not args.no_validation) - - # Parse input files into abstract syntax tree - ast = parser.parse_files(args.file) - if not ast: - logger.error("Unable to parse any of the given files! Aborting now...") - sys.exit(1) - - if args.xml: - try: - XMLConverter().convert(ast, output_dir=args.output_dir) - except XMLConverter.XMLConverterException as ex: - logger.error(f"XML Conversion error: {ex}") - if args.json: + # Set loguru logger format and level + logger.remove() + if not quiet: + logger.add( + sink=sys.stdout, + format="[{time:HH:mm:ss}] [{level}] {message}", + level=log_level, + ) + + def parse_file(self, files: str) -> dict: + """ + Parses the given files. + Returns a dictionary of AbstractSyntaxTree objects with the file name as a key pair. + """ + ast_objects = {} + + # Glob A2L input files + a2l_files = glob.glob(files) + if not a2l_files: + raise A2LParserException(f'Unable to find any A2L files matching: "{files}"') + + for a2l_file in a2l_files: try: - JSONConverter().convert(ast, output_dir=args.output_dir) - except JSONConverter.JSONConverterException as ex: - logger.error(f"JSON Conversion error: {ex}") - if args.yaml: + # Load content from file into memory + logger.info("Parsing file: {}", a2l_file) + a2l_content = self._load_file(filename=a2l_file) + + # Validate the content read + if self.validation: + try: + A2LValidator().validate(a2l_content) + except A2LValidator.A2LValidationError as e: + logger.warning(f'Validation of file "{a2l_file}" failed!\n{e}') + + # Parse the content + filename = os.path.basename(a2l_file) + ast_objects[filename] = self._parse_content(content=a2l_content, show_progressbar=self.show_progressbar) + logger.success("Created Abstract Syntax Tree from file: {}", filename) + + except A2LParserException as e: + logger.error(f'Unable to parse file "{a2l_file}": {e}') + + return ast_objects + + def _parse_content(self, content: str, show_progressbar: bool = True) -> AbstractSyntaxTree: + """ + Parses the given content string and returns an AbstractSyntaxTree object. + """ + logger.debug("Starting AST generation...") + return self.parser.generate_ast(content, show_progressbar) + + def _load_file(self, filename: str, current_dir: str = None) -> str: + """ + Reads the content of the given filename and returns it with includes replaced recursively. + + Args: + filename (str): The filename of the A2L file to be read. + + Returns: + str: The complete A2L file with the included content. + """ + a2l_file = Path(filename) + if current_dir is None: + current_dir = a2l_file.parent + file_path = current_dir / a2l_file.name + else: + file_path = current_dir / a2l_file + file_path = file_path.resolve() + current_dir = file_path.parent + + encodings = ["utf-8", "utf-16", "utf-32"] + for encoding in encodings: try: - YAMLConverter().convert(ast, output_dir=args.output_dir) - except YAMLConverter.YAMLConverterException as ex: - logger.error(f"YAML Conversion error: {ex}") - - if not args.no_prompt: - CommandPrompt.prompt(ast) - - except Exception as ex: - logger.error(ex) - - -def parse_arguments(args: list) -> argparse.Namespace: - """ - Parse the command line arguments. - """ - parser = argparse.ArgumentParser(prog="a2lparser") - parser.add_argument("file", nargs="?", help="A2L files to parse") - parser.add_argument("-x", "--xml", action="store_true", help="Converts an A2L file to a XML output file") - parser.add_argument("-j", "--json", action="store_true", help="Converts an A2L file to a JSON output file") - parser.add_argument("-y", "--yaml", action="store_true", help="Converts an A2L file to a YAML output file") - parser.add_argument("--no-prompt", action="store_true", default=False, help="Disables CLI prompt after parsing") - parser.add_argument("--no-optimize", action="store_true", default=False, help="Disables optimization mode") - parser.add_argument("--no-validation", action="store_true", default=False, help="Disables possible A2L validation warnings") - parser.add_argument("--output-dir", nargs="?", default=None, metavar="PATH", help="Output directory for converted files") - parser.add_argument( - "--gen-ast", - nargs="?", - metavar="CONFIG", - const=A2L_DEFAULT_CONFIG_NAME, - help="Generates python file containing AST node classes", - ) - parser.add_argument("--version", action="version", version=f"a2lparser version: {__version__}") - return parser.parse_args(args) - - -if __name__ == "__main__": - main() + with open(file_path, "r", encoding=encoding) as file: + content = file.read() + break + except UnicodeError: + content = None + + if content is None: + with open(file_path, "r", encoding="latin-1") as file: + content = file.read() + + if includes := self._find_includes(content): + if isinstance(includes, list): + for include in includes: + included_content = self._load_file(include, current_dir) + content = self._include_pattern.sub(included_content, content, count=1) + else: + included_content = self._load_file(includes, current_dir) + content = self._include_pattern.sub(included_content, content) + return content + + def _find_includes(self, content: str) -> str | list | None: + """ + Looks for /include {file.a2l} tags inside given content and returns the full filename. + + Args: + content (str): The content of the A2L file. + + Returns: + str | list | None: The filenames to be included if found. + """ + try: + matches = self._include_pattern.findall(content) + if len(matches) == 1: + return matches[0].strip("\"'") + if len(matches) > 1: + return [match.strip("\"'") for match in matches] + except Exception as e: + raise A2LParserException(e) from e diff --git a/a2lparser/a2l/parsing_exception.py b/a2lparser/a2lparser_exception.py similarity index 97% rename from a2lparser/a2l/parsing_exception.py rename to a2lparser/a2lparser_exception.py index ba51a8b..ce30f41 100644 --- a/a2lparser/a2l/parsing_exception.py +++ b/a2lparser/a2lparser_exception.py @@ -19,7 +19,7 @@ ####################################################################################### -class ParsingException(Exception): +class A2LParserException(Exception): """ Exception thrown when encountering a fatal error during parsing. """ diff --git a/a2lparser/converter/json_converter.py b/a2lparser/converter/json_converter.py index c91bd85..1e70bd4 100644 --- a/a2lparser/converter/json_converter.py +++ b/a2lparser/converter/json_converter.py @@ -20,6 +20,7 @@ import json +from loguru import logger from a2lparser.converter.a2l_converter import A2LConverter @@ -36,10 +37,7 @@ class JSONConverterException(Exception): Exception raised when an error occurs while converting an AST to a JSON file. """ - def convert(self, ast: dict, - output_dir: str = ".", - output_filename: str = None, - pretty: bool = True) -> None: + def convert(self, ast: dict, output_dir: str = ".", output_filename: str = None, pretty: bool = True) -> None: """ Convert the given AST dictionary to JSON and write it to a file. @@ -50,10 +48,13 @@ def convert(self, ast: dict, pretty (bool, optional): Whether to format the JSON file with indentation and newlines. """ try: + logger.info("Converting AST to JSON and writing to file...") converted_tuples = self.convert_to_string(ast, output_filename, pretty) for tup in converted_tuples: filename, json_string = tup self.write_to_file(content=json_string, filename=filename, output_dir=output_dir) + logger.success(f"Created JSON file: {filename}") + except Exception as e: raise self.JSONConverterException(e) from e diff --git a/a2lparser/converter/xml_converter.py b/a2lparser/converter/xml_converter.py index f287c1c..180857b 100644 --- a/a2lparser/converter/xml_converter.py +++ b/a2lparser/converter/xml_converter.py @@ -20,6 +20,7 @@ import xmltodict +from loguru import logger from a2lparser.converter.a2l_converter import A2LConverter @@ -36,11 +37,9 @@ class XMLConverterException(Exception): Exception raised when an error occurs while converting an AST to a XML file. """ - def convert(self, ast: dict, - output_dir: str = ".", - output_filename: str = None, - encoding: str = "utf-8", - pretty: bool = True) -> None: + def convert( + self, ast: dict, output_dir: str = ".", output_filename: str = None, encoding: str = "utf-8", pretty: bool = True + ) -> None: """ Convert the given AST dictionary to XML and write it to a file. @@ -52,10 +51,13 @@ def convert(self, ast: dict, pretty (bool, optional): Whether to format the XML file with indentation and newlines. """ try: + logger.info("Converting AST to XML and writing to file...") converted_tuples = self.convert_to_string(ast, output_filename, encoding, pretty) for tup in converted_tuples: filename, xml_string = tup self.write_to_file(content=xml_string, filename=filename, output_dir=output_dir) + logger.success(f"Created XML file: {filename}") + except Exception as e: raise self.XMLConverterException(e) from e diff --git a/a2lparser/converter/yaml_converter.py b/a2lparser/converter/yaml_converter.py index 68c3e61..62aaa7d 100644 --- a/a2lparser/converter/yaml_converter.py +++ b/a2lparser/converter/yaml_converter.py @@ -20,6 +20,7 @@ import yaml +from loguru import logger from a2lparser.converter.a2l_converter import A2LConverter @@ -48,10 +49,13 @@ def convert(self, ast: dict, output_filename (str, optional): The filename of the YAML file. """ try: + logger.info("Converting AST to YAML and writing to file...") converted_tuples = self.convert_to_string(ast, output_filename) for tup in converted_tuples: filename, yaml_string = tup self.write_to_file(content=yaml_string, filename=filename, output_dir=output_dir) + logger.success(f"Created YAML file: {filename}") + except Exception as e: raise self.YAMLConverterException(e) from e diff --git a/a2lparser/main.py b/a2lparser/main.py new file mode 100644 index 0000000..5ffebcc --- /dev/null +++ b/a2lparser/main.py @@ -0,0 +1,149 @@ +####################################################################################### +# a2lparser: https://github.com/mrom1/a2lparser # +# author: https://github.com/mrom1 # +# # +# This file is part of the a2lparser package. # +# # +# a2lparser is free software: you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the # +# Free Software Foundation, either version 3 of the License, or (at your option) # +# any later version. # +# # +# a2lparser is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # +# or FITNESS FOR A PARTICULAR PURPOSE. # +# See the GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with a2lparser. If not, see . # +####################################################################################### + + +import os +import sys +import argparse +from loguru import logger +from a2lparser import __version__ +from a2lparser import A2L_CONFIGS_DIR +from a2lparser import A2L_PARSER_HEADLINE +from a2lparser import A2L_DEFAULT_CONFIG_NAME +from a2lparser import A2L_GENERATED_FILES_DIR +from a2lparser.a2lparser import A2LParser +from a2lparser.a2lparser_exception import A2LParserException +from a2lparser.cli.command_prompt import CommandPrompt +from a2lparser.a2l.ast.ast_generator import ASTGenerator +from a2lparser.converter.xml_converter import XMLConverter +from a2lparser.converter.json_converter import JSONConverter +from a2lparser.converter.yaml_converter import YAMLConverter + + +@logger.catch +def main() -> None: + """ + Main function of the a2lparser. + + Usage through installation with pip: + $ a2lparser --help + + Usage from root project dir: + $ python -m a2lparser.main --help + + Documentation at: https://github.com/mrom1/a2lparser + """ + try: + args = parse_arguments(sys.argv[1:]) + + # Print header + if not args.quiet and args.log_level in ["DEBUG", "INFO"]: + print(A2L_PARSER_HEADLINE) + + # Generates the AST node classes for the A2L objects using the ASTGenerator + if args.gen_ast: + print("Generating python file containing the AST nodes...") + if args.gen_ast == A2L_DEFAULT_CONFIG_NAME: + config_file = A2L_CONFIGS_DIR / A2L_DEFAULT_CONFIG_NAME + elif os.path.isfile(args.gen_ast): + config_file = args.gen_ast + else: + print(f"Given config file {args.gen_ast} not found. Aborting AST generation.") + sys.exit(1) + print("Generating AST nodes from config at: ", config_file.as_posix()) + generated_file = A2L_GENERATED_FILES_DIR / "a2l_ast.py" + generator = ASTGenerator(config_file.as_posix(), generated_file.as_posix()) + generator.generate() + print(f"Generated {generated_file.as_posix()}") + sys.exit(0) + + # Provide a file or a collection of A2L-files to parse. + if args.file is None: + print() + print("\nPlease specify a A2L file.") + print("For more information use the -h or --help flag.") + sys.exit(1) + + # Initializing the A2L Parser + parser = A2LParser( + validation=not args.no_validation, optimize=not args.no_optimize, log_level=args.log_level, quiet=args.quiet + ) + + # Parse input files into abstract syntax tree + ast = None + try: + ast = parser.parse_file(args.file) + except A2LParserException as ex: + if not ast: + logger.error(ex) + logger.error("Unable to parse any of the given files! Aborting now...") + sys.exit(1) + + if args.xml: + try: + XMLConverter().convert(ast, output_dir=args.output_dir) + except XMLConverter.XMLConverterException as ex: + logger.error(f"XML Conversion error: {ex}") + if args.json: + try: + JSONConverter().convert(ast, output_dir=args.output_dir) + except JSONConverter.JSONConverterException as ex: + logger.error(f"JSON Conversion error: {ex}") + if args.yaml: + try: + YAMLConverter().convert(ast, output_dir=args.output_dir) + except YAMLConverter.YAMLConverterException as ex: + logger.error(f"YAML Conversion error: {ex}") + + if args.prompt: + CommandPrompt.prompt(ast) + + except Exception as ex: + logger.error(ex) + + +def parse_arguments(args: list) -> argparse.Namespace: + """ + Parse the command line arguments. + """ + parser = argparse.ArgumentParser(prog="a2lparser") + parser.add_argument("file", nargs="?", help="A2L files to parse") + parser.add_argument("-x", "--xml", action="store_true", help="Converts an A2L file to a XML output file") + parser.add_argument("-j", "--json", action="store_true", help="Converts an A2L file to a JSON output file") + parser.add_argument("-y", "--yaml", action="store_true", help="Converts an A2L file to a YAML output file") + parser.add_argument("--output-dir", nargs="?", default=None, metavar="PATH", help="Output directory for converted files") + parser.add_argument("--prompt", action="store_true", default=False, help="Enables CLI prompt after parsing") + parser.add_argument("--quiet", action="store_true", default=False, help="Disables console output") + parser.add_argument("--no-optimize", action="store_true", default=False, help="Disables optimization mode") + parser.add_argument("--no-validation", action="store_true", default=False, help="Disables possible A2L validation warnings") + parser.add_argument( + "--gen-ast", + nargs="?", + metavar="CONFIG", + const=A2L_DEFAULT_CONFIG_NAME, + help="Generates python file containing AST node classes", + ) + parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]) + parser.add_argument("--version", action="version", version=f"a2lparser version: {__version__}") + return parser.parse_args(args) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 75994d9..5c702fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ exclude = ["tests*"] package-data = {"a2lparser" = ["*.cfg", "*.config"]} [project.scripts] -a2lparser = "a2lparser.a2lparser:main" +a2lparser = "a2lparser.main:main" [tool.black] line-length = 128 diff --git a/setup.py b/setup.py index b1ffbce..ef86d31 100644 --- a/setup.py +++ b/setup.py @@ -46,5 +46,5 @@ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", ], - entry_points={"console_scripts": ["a2lparser = a2lparser.a2lparser:main"]}, + entry_points={"console_scripts": ["a2lparser = a2lparser.main:main"]}, ) diff --git a/tests/integration/test_integration_asap2_demo_v161.py b/tests/integration/test_integration_asap2_demo_v161.py index 0b3a742..b399a54 100644 --- a/tests/integration/test_integration_asap2_demo_v161.py +++ b/tests/integration/test_integration_asap2_demo_v161.py @@ -22,7 +22,7 @@ import tempfile from pathlib import Path from a2lparser import A2L_PACKAGE_DIR -from a2lparser.a2lparser import main +from a2lparser.main import main from tests.fixture_utils import compare_files, check_files_exist @@ -37,7 +37,7 @@ def test_integration_asap2_demo_v161(monkeypatch, compare_files, check_files_exi with tempfile.TemporaryDirectory(dir=temp_test_output_path, prefix=temp_test_dir_prefix) as tempdir: monkeypatch.setattr("sys.argv", [ "a2lparser", f"testfiles/A2L/{a2l_filename}", - "--json", "--xml", "--yaml", "--no-prompt", + "--json", "--xml", "--yaml", "--output-dir", f"\"{Path(tempdir).resolve().as_posix()}\"", ]) main() diff --git a/tests/integration/test_integration_asap2_demo_v171.py b/tests/integration/test_integration_asap2_demo_v171.py index 6d06251..31d0565 100644 --- a/tests/integration/test_integration_asap2_demo_v171.py +++ b/tests/integration/test_integration_asap2_demo_v171.py @@ -22,7 +22,7 @@ import tempfile from pathlib import Path from a2lparser import A2L_PACKAGE_DIR -from a2lparser.a2lparser import main +from a2lparser.main import main from tests.fixture_utils import compare_files, check_files_exist @@ -37,7 +37,7 @@ def test_integration_asap2_demo_v171(monkeypatch, compare_files, check_files_exi with tempfile.TemporaryDirectory(dir=temp_test_output_path, prefix=temp_test_dir_prefix) as tempdir: monkeypatch.setattr("sys.argv", [ "a2lparser", f"testfiles/A2L/{a2l_filename}", - "--json", "--xml", "--yaml", "--no-prompt", + "--json", "--xml", "--yaml", "--output-dir", f"\"{Path(tempdir).resolve().as_posix()}\"", ]) main() diff --git a/tests/integration/test_integration_nested_includes.py b/tests/integration/test_integration_nested_includes.py index 402da08..eb7aac2 100644 --- a/tests/integration/test_integration_nested_includes.py +++ b/tests/integration/test_integration_nested_includes.py @@ -22,7 +22,7 @@ import tempfile from pathlib import Path from a2lparser import A2L_PACKAGE_DIR -from a2lparser.a2lparser import main +from a2lparser.main import main from tests.fixture_utils import compare_files, check_files_exist @@ -37,7 +37,7 @@ def test_integration_nested_includes(monkeypatch, compare_files, check_files_exi with tempfile.TemporaryDirectory(dir=temp_test_output_path, prefix=temp_test_dir_prefix) as tempdir: monkeypatch.setattr("sys.argv", [ "a2lparser", f"testfiles/A2L/{a2l_filename}", - "--json", "--xml", "--yaml", "--no-prompt", + "--json", "--xml", "--yaml", "--output-dir", f"\"{Path(tempdir).resolve().as_posix()}\"", ]) main() diff --git a/tests/integration/test_integration_version.py b/tests/integration/test_integration_version.py index 9092f30..f8f88bd 100644 --- a/tests/integration/test_integration_version.py +++ b/tests/integration/test_integration_version.py @@ -21,7 +21,7 @@ import pytest from a2lparser import __version__ -from a2lparser.a2lparser import main +from a2lparser.main import main def test_integration_version_argument(monkeypatch, capsys): diff --git a/tests/lex/test_lex_datatypes.py b/tests/lex/test_lex_datatypes.py index 58389be..69e81ae 100644 --- a/tests/lex/test_lex_datatypes.py +++ b/tests/lex/test_lex_datatypes.py @@ -23,7 +23,7 @@ from a2lparser.a2l.a2l_lex import A2LLex -@pytest.mark.parametrize("decimal_constant", ["123", "-456", "0"]) +@pytest.mark.parametrize("decimal_constant", ["123", "-456", "0", "08001", "-01234", "+22"]) def test_lex_datatypes_decimal_constants(decimal_constant): """ Testing the A2L Lexer for detection of decimal numbers. diff --git a/tests/parser/test_parser_find_includes.py b/tests/parser/test_parser_find_includes.py index 4e293db..0646bfd 100644 --- a/tests/parser/test_parser_find_includes.py +++ b/tests/parser/test_parser_find_includes.py @@ -20,7 +20,7 @@ import pytest -from a2lparser.a2l.parser import Parser +from a2lparser.a2lparser import A2LParser @pytest.mark.parametrize('matching_includes, expected_filename', [ @@ -42,6 +42,6 @@ def test_parser_load_file_include_mechanism(matching_includes, expected_filename """ Tests the include mechanism of the A2L parser. """ - parser = Parser() + parser = A2LParser() filename = parser._find_includes(matching_includes) assert filename == expected_filename diff --git a/tests/parser/test_parser_load_file.py b/tests/parser/test_parser_load_file.py index 4abf86b..6de0873 100644 --- a/tests/parser/test_parser_load_file.py +++ b/tests/parser/test_parser_load_file.py @@ -23,7 +23,7 @@ import tempfile import pytest from a2lparser import A2L_PACKAGE_DIR -from a2lparser.a2l.parser import Parser +from a2lparser.a2lparser import A2LParser @pytest.fixture @@ -92,7 +92,7 @@ def test_parser_load_file_simple(create_file, a2l_content_sections_tuple): temp_test_dir_prefix = "temp_dir_output_" # Parser object - parser = Parser() + parser = A2LParser() # Create temporary directory and files with tempfile.TemporaryDirectory(dir=temp_test_output_path, prefix=temp_test_dir_prefix) as tempdir: @@ -145,7 +145,7 @@ def test_parser_load_file_complex(create_file, a2l_content_sections_tuple): temp_test_dir_prefix = "temp_dir_output_" # Parser object - parser = Parser() + parser = A2LParser() # Create temporary directory and files with tempfile.TemporaryDirectory(dir=temp_test_output_path, prefix=temp_test_dir_prefix) as tempdir: diff --git a/tests/rules/test_rules_if_data.py b/tests/rules/test_rules_if_data.py index a251712..13f946a 100644 --- a/tests/rules/test_rules_if_data.py +++ b/tests/rules/test_rules_if_data.py @@ -95,14 +95,14 @@ def test_rules_if_data(): assert daq_event_test["FOO"]["BAR"]["DataParams"] == ["BAR"] # Second nested If_Data_Block within EVENT - daq_event_X = daq_event["X"] - assert daq_event_X["Name"] == "X" - assert daq_event_X["DataParams"] == ["X"] + daq_event_x = daq_event["X"] + assert daq_event_x["Name"] == "X" + assert daq_event_x["DataParams"] == ["X"] # Third nested If_Data_Block within EVENT - daq_event_Y = daq_event["Y"] - assert daq_event_Y["Name"] == "Y" - assert daq_event_Y["DataParams"] == ["Y"] + daq_event_y = daq_event["Y"] + assert daq_event_y["Name"] == "Y" + assert daq_event_y["DataParams"] == ["Y"] # Second If_Data_Block protocol_layer = if_data["PROTOCOL_LAYER"] @@ -168,7 +168,6 @@ def test_rules_if_data_including_keywords(): assert ast if_data = ast["IF_DATA"] - assert if_data assert if_data["Name"] == "ETK" tp_blob = if_data["TP_BLOB"] source_1 = if_data["SOURCE"][0] @@ -184,30 +183,258 @@ def test_rules_if_data_including_keywords(): assert source_5 assert source_1["Name"] == "SOURCE" - assert source_1["DataParams"] == ['"Engine_1"', '103', '1', 'QP_BLOB', '0x100', '1', 'DIRECT', '23', 'MEASUREMENT', - '1952251460', '1020', '2952232964', '2952243268', '0', '0', '0', '15000', '256', '0'] + assert source_1["DataParams"] == [ + '"Engine_1"', + "103", + "1", + "QP_BLOB", + "0x100", + "1", + "DIRECT", + "23", + "MEASUREMENT", + "1952251460", + "1020", + "2952232964", + "2952243268", + "0", + "0", + "0", + "15000", + "256", + "0", + ] assert source_2["Name"] == "SOURCE" - assert source_2["DataParams"] == ['"Engine_2"', '103', '1', 'QP_BLOB', '0x200', '2', 'DIRECT', '21', 'MEASUREMENT', - '2952251460', '1020', '2952233996', '2952244288', '0', '0', '0', '15000', '256', '0'] + assert source_2["DataParams"] == [ + '"Engine_2"', + "103", + "1", + "QP_BLOB", + "0x200", + "2", + "DIRECT", + "21", + "MEASUREMENT", + "2952251460", + "1020", + "2952233996", + "2952244288", + "0", + "0", + "0", + "15000", + "256", + "0", + ] assert source_3["Name"] == "SOURCE" - assert source_3["DataParams"] == ['"Engine_3"', '103', '1', 'QP_BLOB', '0x300', '3', 'DIRECT', '19', 'MEASUREMENT', - '3952251460', '1020', '2952235028', '2952245308', '0', '0', '0', '15000', '256', '0'] + assert source_3["DataParams"] == [ + '"Engine_3"', + "103", + "1", + "QP_BLOB", + "0x300", + "3", + "DIRECT", + "19", + "MEASUREMENT", + "3952251460", + "1020", + "2952235028", + "2952245308", + "0", + "0", + "0", + "15000", + "256", + "0", + ] assert source_4["Name"] == "SOURCE" - assert source_4["DataParams"] == ['"10ms_sync"', '4', '1', 'QP_BLOB', '0x400', '7', 'DIRECT', '11', 'MEASUREMENT', - '4952251460', '1020', '2952239156', '2952249388', '0', '0', '0', '10000', '512', '0'] + assert source_4["DataParams"] == [ + '"10ms_sync"', + "4", + "1", + "QP_BLOB", + "0x400", + "7", + "DIRECT", + "11", + "MEASUREMENT", + "4952251460", + "1020", + "2952239156", + "2952249388", + "0", + "0", + "0", + "10000", + "512", + "0", + ] assert source_5["Name"] == "SOURCE" - assert source_5["DataParams"] == ['"100ms_sync"', '5', '1', 'QP_BLOB', '0x500', '8', 'DIRECT', '9', 'MEASUREMENT', - '5952251460', '1020', '2952241212', '2952250408', '0', '0', '0', '100000', '512', '0'] + assert source_5["DataParams"] == [ + '"100ms_sync"', + "5", + "1", + "QP_BLOB", + "0x500", + "8", + "DIRECT", + "9", + "MEASUREMENT", + "5952251460", + "1020", + "2952241212", + "2952250408", + "0", + "0", + "0", + "100000", + "512", + "0", + ] assert tp_blob["Name"] == "TP_BLOB" - assert tp_blob["DataParams"] == ['0x1000103', 'INTERFACE_SPEED_100MBIT', '0x0', 'ETK_CFG', '0x10', '0x1D', '0x61', - '0x1', '0x1', '0xFF', '0xFF', '0x63', '0xCF', '0x7F', '0x81', '0x84', '0x79', '0x64', - '0xB', '0x65', '0x8C', '0x66', '0xA0', '0x67', '0x91', 'ETK_MAILBOX', '0x11223344', - 'EXRAM', '0xAFF7FF00', '0xFF', 'EXRAM', '0xAFF7FF00', '0xFF', 'PAGE_SWITCH_METHOD', '0x1', - 'MAILBOX', '0x1', '0x1F4', '0xAFF7C928', 'AUTOSTART_BEHAVIOR', 'ALWAYS_RP', - 'OCT_WORKINGPAGE', '0x400', '0xAFF7C84C', '0xDC'] + assert tp_blob["DataParams"] == [ + "0x1000103", + "INTERFACE_SPEED_100MBIT", + "0x0", + "ETK_CFG", + "0x10", + "0x1D", + "0x61", + "0x1", + "0x1", + "0xFF", + "0xFF", + "0x63", + "0xCF", + "0x7F", + "0x81", + "0x84", + "0x79", + "0x64", + "0xB", + "0x65", + "0x8C", + "0x66", + "0xA0", + "0x67", + "0x91", + "ETK_MAILBOX", + "0x11223344", + "EXRAM", + "0xAFF7FF00", + "0xFF", + "EXRAM", + "0xAFF7FF00", + "0xFF", + "PAGE_SWITCH_METHOD", + "0x1", + "MAILBOX", + "0x1", + "0x1F4", + "0xAFF7C928", + "AUTOSTART_BEHAVIOR", + "ALWAYS_RP", + "OCT_WORKINGPAGE", + "0x400", + "0xAFF7C84C", + "0xDC", + ] tp_blob_distab_cfg = tp_blob["DISTAB_CFG"] assert tp_blob_distab_cfg assert tp_blob_distab_cfg["Name"] == "DISTAB_CFG" - assert tp_blob_distab_cfg["DataParams"] == ['0xD', '0x122', '0x2', '0x0', '0x0', 'TRG_MOD', '0x0'] + assert tp_blob_distab_cfg["DataParams"] == ["0xD", "0x122", "0x2", "0x0", "0x0", "TRG_MOD", "0x0"] + + +def test_rules_if_data_empty_ident_block(): + """ + Tests that an empty /begin /end section does not raise an error. + """ + if_data_input = """ + /begin IF_DATA XCP + /begin DAQ + STATIC + 0x0004 + GRANULARITY_ENTRY_SIZE_DAQ_BYTE + 0x04 + NO_OVERLOAD_INDICATION + /begin DAQ_LIST + DAQ_LIST_TYPE DAQ + MAX_ODT 0x01 + EVENT_FIXED 0x1001 + /begin PREDEFINED + /begin ODT + 0x00 + ODT_ENTRY + 0x04 + /end ODT + /end PREDEFINED + /end DAQ_LIST + /begin DAQ_LIST + 0x01 + DAQ_LIST_TYPE DAQ + MAX_ODT 0x10 + EVENT_FIXED 0x2001 + /begin PREDEFINED + /end PREDEFINED + /end DAQ_LIST + /begin DAQ_LIST + 0x02 + DAQ_LIST_TYPE DAQ + MAX_ODT 0x00 + EVENT_FIXED 0x4001 + /begin PREDEFINED + /end PREDEFINED + /end DAQ_LIST + /begin EVENT + "1_ms_task" + DAQ + 1 + 0x00 + /end EVENT + /end DAQ + /begin XCP_ON_UDP_IP + 0x0001 + 08007 + ADDRESS "192.168.1.101" + /end XCP_ON_UDP_IP + /end IF_DATA + """ + parser = A2LYacc() + ast = parser.generate_ast(if_data_input) + assert ast + + if_data = ast["IF_DATA"] + assert if_data["Name"] == "XCP" + assert if_data["DAQ"] + assert if_data["XCP_ON_UDP_IP"] + + daq = if_data["DAQ"] + assert daq["Name"] == "DAQ" + assert daq["DataParams"] == ["STATIC", "0x0004", "GRANULARITY_ENTRY_SIZE_DAQ_BYTE", "0x04", "NO_OVERLOAD_INDICATION"] + assert daq["DAQ_LIST"] + + daq_list = daq["DAQ_LIST"] + assert len(daq_list) == 3 + assert daq_list[0]["Name"] == "DAQ_LIST" + assert daq_list[0]["DataParams"] == ["DAQ_LIST_TYPE", "DAQ", "MAX_ODT", "0x01", "EVENT_FIXED", "0x1001"] + assert daq_list[0]["PREDEFINED"] + assert daq_list[0]["PREDEFINED"]["ODT"]["DataParams"] == ["0x00", "ODT_ENTRY", "0x04"] + assert daq_list[1]["Name"] == "DAQ_LIST" + assert daq_list[1]["DataParams"] == ["0x01", "DAQ_LIST_TYPE", "DAQ", "MAX_ODT", "0x10", "EVENT_FIXED", "0x2001"] + assert daq_list[1]["PREDEFINED"] + assert daq_list[2]["Name"] == "DAQ_LIST" + assert daq_list[2]["DataParams"] == ["0x02", "DAQ_LIST_TYPE", "DAQ", "MAX_ODT", "0x00", "EVENT_FIXED", "0x4001"] + assert daq_list[2]["PREDEFINED"] + + daq_event = daq["EVENT"] + assert daq_event + assert daq_event["Name"] == "EVENT" + assert daq_event["DataParams"] == ['"1_ms_task"', "DAQ", "1", "0x00"] + + xcp_on_udp_ip = if_data["XCP_ON_UDP_IP"] + assert xcp_on_udp_ip + assert xcp_on_udp_ip["Name"] == "XCP_ON_UDP_IP" + assert xcp_on_udp_ip["DataParams"] == ["0x0001", "08007", "ADDRESS", '"192.168.1.101"']