From e3b26af7c0f2ab84ae7e32723e58d459df453d86 Mon Sep 17 00:00:00 2001 From: Azim Khan Date: Fri, 29 Jun 2018 02:36:57 +0100 Subject: [PATCH] Improve documentation in generate_test_code.py --- tests/Makefile | 4 +- tests/scripts/generate_test_code.py | 176 +++++++++++++++++----------- 2 files changed, 108 insertions(+), 72 deletions(-) diff --git a/tests/Makefile b/tests/Makefile index e344635f9..003d71c51 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -194,7 +194,7 @@ $(C_FILES): %.c: suites/$$(func.$$*).function suites/%.data scripts/generate_tes -t suites/main_test.function \ -p suites/host_test.function \ -s suites \ - --help-file suites/helpers.function \ + --helpers-file suites/helpers.function \ -o . @@ -229,7 +229,7 @@ $(EMBEDDED_TESTS): embedded_%: suites/$$(func.$$*).function suites/%.data script -t suites/main_test.function \ -p suites/target_test.function \ -s suites \ - --help-file suites/helpers.function \ + --helpers-file suites/helpers.function \ -o ./TESTS/mbedtls/$* generate-target-tests: $(EMBEDDED_TESTS) diff --git a/tests/scripts/generate_test_code.py b/tests/scripts/generate_test_code.py index b2d49129e..047b13001 100755 --- a/tests/scripts/generate_test_code.py +++ b/tests/scripts/generate_test_code.py @@ -19,22 +19,18 @@ # This file is part of mbed TLS (https://tls.mbed.org) """ -Test Suite code generator. +This script dynamically generates test suite code for Mbed TLS, by +taking following input files. -Generates a test source file using following input files: - -test_suite_xyz.function - Read test functions from test suite - functions file. -test_suite_xyz.data - Read test functions and their - dependencies to generate dispatch and - dependency check code. +test_suite_xyz.function - Test suite functions file contains test + functions. +test_suite_xyz.data - Contains test case vectors. main_test.function - Template to substitute generated test - function dispatch code, dependency - checking code. -platform .function - Read host or target platform - implementation for dispatching test - cases from .data file. -helpers.function - Read common reusable functions. + functions, dispatch code, dependency + checking code etc. +platform .function - Platform specific initialization and + platform code. +helpers.function - Common/reusable data and functions. """ @@ -43,7 +39,6 @@ import os import re import sys import argparse -import shutil BEGIN_HEADER_REGEX = '/\*\s*BEGIN_HEADER\s*\*/' @@ -59,39 +54,39 @@ BEGIN_CASE_REGEX = '/\*\s*BEGIN_CASE\s*(.*?)\s*\*/' END_CASE_REGEX = '/\*\s*END_CASE\s*\*/' -class InvalidFileFormat(Exception): - """ - Exception to indicate invalid file format. - """ - pass - - class GeneratorInputError(Exception): """ - Exception to indicate error in the input to the generator. + Exception to indicate error in the input files to this script. + This includes missing patterns, test function names and other + parsing errors. """ pass class FileWrapper(io.FileIO): """ - File wrapper class. Provides reading with line no. tracking. + This class extends built-in io.FileIO class with attribute line_no, + that indicates line number for the line that is read. """ def __init__(self, file_name): """ - Init file handle. + Instantiate the base class and initialize the line number to 0. :param file_name: File path to open. """ super(FileWrapper, self).__init__(file_name, 'r') self.line_no = 0 - # Override the generator function in a way that works in both - # Python 2 and Python 3. def __next__(self): """ - Iterator return impl. + Python 2 iterator method. This method overrides base class's + next method and extends the next method to count the line + numbers as each line is read. + + It works for both Python 2 and Python 3 by checking iterator + method name in the base iterator object. + :return: Line read from file. """ parent = super(FileWrapper, self) @@ -105,6 +100,8 @@ class FileWrapper(io.FileIO): # strip any whitespaces added in the decoding process. return line.decode(sys.getdefaultencoding()).strip() + "\n" return None + + # Python 3 iterator method next = __next__ @@ -113,15 +110,22 @@ def split_dep(dep): Split NOT character '!' from dependency. Used by gen_deps() :param dep: Dependency list - :return: list of tuples where index 0 has '!' if there was a '!' - before the dependency string + :return: string tuple. Ex: ('!', MACRO) for !MACRO and ('', MACRO) for + MACRO. """ return ('!', dep[1:]) if dep[0] == '!' else ('', dep) def gen_deps(deps): """ - Generates dependency i.e. if def and endif code + Test suite data and functions specifies compile time dependencies. + This function generates C preprocessor code from the input + dependency list. Caller uses the generated preprocessor code to + wrap dependent code. + A dependency in the input list can have a leading '!' character + to negate a condition. '!' is separated from the dependency using + function split_dep() and proper preprocessor check is generated + accordingly. :param deps: List of dependencies. :return: if defined and endif code with macro annotations for @@ -135,8 +139,8 @@ def gen_deps(deps): def gen_deps_one_line(deps): """ - Generates dependency checks in one line. Useful for writing code - in #else case. + Similar to gen_deps() but generates dependency checks in one line. + Useful for generating code with #else block. :param deps: List of dependencies. :return: ifdef code @@ -173,7 +177,12 @@ void {name}_wrapper( void ** params ) def gen_dispatch(name, deps): """ - Generates dispatch code for the test function table. + Test suite code template main_test.function defines a C function + array to contain test case functions. This function generates an + initializer entry for a function in that array. The entry is + composed of a compile time check for the test function + dependencies. At compile time the test function is assigned when + dependencies are met, else NULL is assigned. :param name: Test function name :param deps: List of dependencies @@ -198,11 +207,12 @@ def gen_dispatch(name, deps): def parse_until_pattern(funcs_f, end_regex): """ - Parses function headers or helper code until end pattern. + Matches pattern end_regex to the lines read from the file object. + Returns the lines read until end pattern is matched. :param funcs_f: file object for .functions file :param end_regex: Pattern to stop parsing - :return: Test suite headers code + :return: Lines read before the end pattern """ headers = '#line %d "%s"\n' % (funcs_f.line_no + 1, funcs_f.name) for line in funcs_f: @@ -210,7 +220,7 @@ def parse_until_pattern(funcs_f, end_regex): break headers += line else: - raise InvalidFileFormat("file: %s - end pattern [%s] not found!" % + raise GeneratorInputError("file: %s - end pattern [%s] not found!" % (funcs_f.name, end_regex)) return headers @@ -218,7 +228,10 @@ def parse_until_pattern(funcs_f, end_regex): def parse_suite_deps(funcs_f): """ - Parses test suite dependencies. + Parses test suite dependencies specified at the top of a + .function file, that starts with pattern BEGIN_DEPENDENCIES + and end with END_DEPENDENCIES. Dependencies are specified + after pattern 'depends_on:' and are delimited by ':'. :param funcs_f: file object for .functions file :return: List of test suite dependencies. @@ -231,7 +244,7 @@ def parse_suite_deps(funcs_f): if re.search(END_DEP_REGEX, line): break else: - raise InvalidFileFormat("file: %s - end dependency pattern [%s]" + raise GeneratorInputError("file: %s - end dependency pattern [%s]" " not found!" % (funcs_f.name, END_DEP_REGEX)) return deps @@ -239,7 +252,9 @@ def parse_suite_deps(funcs_f): def parse_function_deps(line): """ - Parses function dependencies. + Parses function dependencies, that are in the same line as + comment BEGIN_CASE. Dependencies are specified after pattern + 'depends_on:' and are delimited by ':'. :param line: Line from .functions file that has dependencies. :return: List of dependencies. @@ -256,7 +271,9 @@ def parse_function_deps(line): def parse_function_signature(line): """ - Parsing function signature + Parses test function signature for validation and generates + a dispatch wrapper function that translates input test vectors + read from the data file into test function arguments. :param line: Line from .functions file that has a function signature. @@ -266,6 +283,7 @@ def parse_function_signature(line): args = [] locals = '' args_dispatch = [] + # Check if the test function returns void. m = re.search('\s*void\s+(\w+)\s*\(', line, re.I) if not m: raise ValueError("Test function should return 'void'\n%s" % line) @@ -326,7 +344,7 @@ def parse_function_code(funcs_f, deps, suite_deps): name = 'test_' + name break else: - raise InvalidFileFormat("file: %s - Test functions not found!" % + raise GeneratorInputError("file: %s - Test functions not found!" % funcs_f.name) for line in funcs_f: @@ -334,7 +352,7 @@ def parse_function_code(funcs_f, deps, suite_deps): break code += line else: - raise InvalidFileFormat("file: %s - end case pattern [%s] not " + raise GeneratorInputError("file: %s - end case pattern [%s] not " "found!" % (funcs_f.name, END_CASE_REGEX)) # Add exit label if not present @@ -353,7 +371,8 @@ def parse_function_code(funcs_f, deps, suite_deps): def parse_functions(funcs_f): """ - Returns functions code pieces + Parses a test_suite_xxx.function file and returns information + for generating a C source file for the test suite. :param funcs_f: file object of the functions file. :return: List of test suite dependencies, test function dispatch @@ -427,7 +446,13 @@ def escaped_split(str, ch): def parse_test_data(data_f, debug=False): """ - Parses .data file + Parses .data file for each test case name, test function name, + test dependencies and test arguments. This information is + correlated with the test functions file for generating an + intermediate data file replacing the strings for test function + names, dependencies and integer constant expressions with + identifiers. Mainly for optimising space for on-target + execution. :param data_f: file object of the data file. :return: Generator that yields test name, function name, @@ -478,7 +503,8 @@ def parse_test_data(data_f, debug=False): def gen_dep_check(dep_id, dep): """ - Generate code for the dependency. + Generate code for checking dependency with the associated + identifier. :param dep_id: Dependency identifier :param dep: Dependency macro @@ -505,7 +531,8 @@ def gen_dep_check(dep_id, dep): def gen_expression_check(exp_id, exp): """ - Generates code for expression check + Generates code for evaluating an integer expression using + associated expression Id. :param exp_id: Expression Identifier :param exp: Expression/Macro @@ -527,8 +554,9 @@ def gen_expression_check(exp_id, exp): def write_deps(out_data_f, test_deps, unique_deps): """ - Write dependencies to intermediate test data file. - It also returns dependency check code. + Write dependencies to intermediate test data file, replacing + the string form with identifiers. Also, generates dependency + check code. :param out_data_f: Output intermediate data file :param test_deps: Dependencies @@ -553,8 +581,9 @@ def write_deps(out_data_f, test_deps, unique_deps): def write_parameters(out_data_f, test_args, func_args, unique_expressions): """ - Writes test parameters to the intermediate data file. - Also generates expression code. + Writes test parameters to the intermediate data file, replacing + the string form with identifiers. Also, generates expression + check code. :param out_data_f: Output intermediate data file :param test_args: Test parameters @@ -588,7 +617,7 @@ def write_parameters(out_data_f, test_args, func_args, unique_expressions): def gen_suite_deps_checks(suite_deps, dep_check_code, expression_code): """ - Adds preprocessor checks for test suite dependencies. + Generates preprocessor checks for test suite dependencies. :param suite_deps: Test suite dependencies read from the .functions file. @@ -614,8 +643,14 @@ def gen_suite_deps_checks(suite_deps, dep_check_code, expression_code): def gen_from_test_data(data_f, out_data_f, func_info, suite_deps): """ - Generates dependency checks, expression code and intermediate - data file from test data file. + This function reads test case name, dependencies and test vectors + from the .data file. This information is correlated with the test + functions file for generating an intermediate data file replacing + the strings for test function names, dependencies and integer + constant expressions with identifiers. Mainly for optimising + space for on-target execution. + It also generates test case dependency check code and expression + evaluation code. :param data_f: Data file object :param out_data_f:Output intermediate data file @@ -660,15 +695,16 @@ def gen_from_test_data(data_f, out_data_f, func_info, suite_deps): def generate_code(funcs_file, data_file, template_file, platform_file, - help_file, suites_dir, c_file, out_data_file): + helpers_file, suites_dir, c_file, out_data_file): """ - Generate mbed-os test code. + Generates C source code from test suite file, data file, common + helpers file and platform file. :param funcs_file: Functions file object :param data_file: Data file object :param template_file: Template file object :param platform_file: Platform file object - :param help_file: Helper functions file object + :param helpers_file: Helper functions file object :param suites_dir: Test suites dir :param c_file: Output C file object :param out_data_file: Output intermediate data file object @@ -678,7 +714,7 @@ def generate_code(funcs_file, data_file, template_file, platform_file, ('Data file', data_file), ('Template file', template_file), ('Platform file', platform_file), - ('Help code file', help_file), + ('Helpers code file', helpers_file), ('Suites dir', suites_dir)]: if not os.path.exists(path): raise IOError("ERROR: %s [%s] not found!" % (name, path)) @@ -686,9 +722,9 @@ def generate_code(funcs_file, data_file, template_file, platform_file, snippets = {'generator_script' : os.path.basename(__file__)} # Read helpers - with open(help_file, 'r') as help_f, open(platform_file, 'r') as \ + with open(helpers_file, 'r') as help_f, open(platform_file, 'r') as \ platform_f: - snippets['test_common_helper_file'] = help_file + snippets['test_common_helper_file'] = helpers_file snippets['test_common_helpers'] = help_f.read() snippets['test_platform_file'] = platform_file snippets['platform_code'] = platform_f.read().replace( @@ -730,36 +766,36 @@ def check_cmd(): :return: """ parser = argparse.ArgumentParser( - description='Generate code for mbed-os tests.') + description='Dynamically generate test suite code.') parser.add_argument("-f", "--functions-file", dest="funcs_file", help="Functions file", - metavar="FUNCTIONS", + metavar="FUNCTIONS_FILE", required=True) parser.add_argument("-d", "--data-file", dest="data_file", help="Data file", - metavar="DATA", + metavar="DATA_FILE", required=True) parser.add_argument("-t", "--template-file", dest="template_file", help="Template file", - metavar="TEMPLATE", + metavar="TEMPLATE_FILE", required=True) parser.add_argument("-s", "--suites-dir", dest="suites_dir", help="Suites dir", - metavar="SUITES", + metavar="SUITES_DIR", required=True) - parser.add_argument("--help-file", - dest="help_file", - help="Help file", - metavar="HELPER", + parser.add_argument("--helpers-file", + dest="helpers_file", + help="Helpers file", + metavar="HELPERS_FILE", required=True) parser.add_argument("-p", "--platform-file", @@ -789,7 +825,7 @@ def check_cmd(): os.makedirs(d) generate_code(args.funcs_file, args.data_file, args.template_file, - args.platform_file, args.help_file, args.suites_dir, + args.platform_file, args.helpers_file, args.suites_dir, out_c_file, out_data_file)