diff --git a/scripts/generate_bignum_tests.py b/scripts/generate_bignum_tests.py new file mode 100755 index 000000000..68ad42f48 --- /dev/null +++ b/scripts/generate_bignum_tests.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Generate test data for bignum functions. + +With no arguments, generate all test data. With non-option arguments, +generate only the specified files. + +Class structure: + +Child classes of test_data_generation.BaseTarget (file targets) represent an output +file. These indicate where test cases will be written to, for all subclasses of +this target. Multiple file targets should not reuse a `target_basename`. + +Each subclass derived from a file target can either be: + - A concrete class, representing a test function, which generates test cases. + - An abstract class containing shared methods and attributes, not associated + with a test function. An example is BignumOperation, which provides + common features used for bignum binary operations. + +Both concrete and abstract subclasses can be derived from, to implement +additional test cases (see BignumCmp and BignumCmpAbs for examples of deriving +from abstract and concrete classes). + + +Adding test case generation for a function: + +A subclass representing the test function should be added, deriving from a +file target such as BignumTarget. This test class must set/implement the +following: + - test_function: the function name from the associated .function file. + - test_name: a descriptive name or brief summary to refer to the test + function. + - arguments(): a method to generate the list of arguments required for the + test_function. + - generate_function_tests(): a method to generate TestCases for the function. + This should create instances of the class with required input data, and + call `.create_test_case()` to yield the TestCase. + +Additional details and other attributes/methods are given in the documentation +of BaseTarget in test_data_generation.py. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +import sys + +from abc import ABCMeta +from typing import List + +from mbedtls_framework import test_data_generation +from mbedtls_framework import bignum_common +# Import modules containing additional test classes +# Test function classes in these modules will be registered by +# the framework +from mbedtls_framework import bignum_core, bignum_mod_raw, bignum_mod # pylint: disable=unused-import + +class BignumTarget(test_data_generation.BaseTarget): + #pylint: disable=too-few-public-methods + """Target for bignum (legacy) test case generation.""" + target_basename = 'test_suite_bignum.generated' + + +class BignumOperation(bignum_common.OperationCommon, BignumTarget, + metaclass=ABCMeta): + #pylint: disable=abstract-method + """Common features for bignum operations in legacy tests.""" + unique_combinations_only = True + input_values = [ + "", "0", "-", "-0", + "7b", "-7b", + "0000000000000000123", "-0000000000000000123", + "1230000000000000000", "-1230000000000000000" + ] + + def description_suffix(self) -> str: + #pylint: disable=no-self-use # derived classes need self + """Text to add at the end of the test case description.""" + return "" + + def description(self) -> str: + """Generate a description for the test case. + + If not set, case_description uses the form A `symbol` B, where symbol + is used to represent the operation. Descriptions of each value are + generated to provide some context to the test case. + """ + if not self.case_description: + self.case_description = "{} {} {}".format( + self.value_description(self.arg_a), + self.symbol, + self.value_description(self.arg_b) + ) + description_suffix = self.description_suffix() + if description_suffix: + self.case_description += " " + description_suffix + return super().description() + + @staticmethod + def value_description(val) -> str: + """Generate a description of the argument val. + + This produces a simple description of the value, which is used in test + case naming to add context. + """ + if val == "": + return "0 (null)" + if val == "-": + return "negative 0 (null)" + if val == "0": + return "0 (1 limb)" + + if val[0] == "-": + tmp = "negative" + val = val[1:] + else: + tmp = "positive" + if val[0] == "0": + tmp += " with leading zero limb" + elif len(val) > 10: + tmp = "large " + tmp + return tmp + + +class BignumCmp(BignumOperation): + """Test cases for bignum value comparison.""" + count = 0 + test_function = "mpi_cmp_mpi" + test_name = "MPI compare" + input_cases = [ + ("-2", "-3"), + ("-2", "-2"), + ("2b4", "2b5"), + ("2b5", "2b6") + ] + + def __init__(self, val_a, val_b) -> None: + super().__init__(val_a, val_b) + self._result = int(self.int_a > self.int_b) - int(self.int_a < self.int_b) + self.symbol = ["<", "==", ">"][self._result + 1] + + def result(self) -> List[str]: + return [str(self._result)] + + +class BignumCmpAbs(BignumCmp): + """Test cases for absolute bignum value comparison.""" + count = 0 + test_function = "mpi_cmp_abs" + test_name = "MPI compare (abs)" + + def __init__(self, val_a, val_b) -> None: + super().__init__(val_a.strip("-"), val_b.strip("-")) + + +class BignumAdd(BignumOperation): + """Test cases for bignum value addition.""" + count = 0 + symbol = "+" + test_function = "mpi_add_mpi" + test_name = "MPI add" + input_cases = bignum_common.combination_pairs( + [ + "1c67967269c6", "9cde3", + "-1c67967269c6", "-9cde3", + ] + ) + + def __init__(self, val_a: str, val_b: str) -> None: + super().__init__(val_a, val_b) + self._result = self.int_a + self.int_b + + def description_suffix(self) -> str: + if (self.int_a >= 0 and self.int_b >= 0): + return "" # obviously positive result or 0 + if (self.int_a <= 0 and self.int_b <= 0): + return "" # obviously negative result or 0 + # The sign of the result is not obvious, so indicate it + return ", result{}0".format('>' if self._result > 0 else + '<' if self._result < 0 else '=') + + def result(self) -> List[str]: + return [bignum_common.quote_str("{:x}".format(self._result))] + +if __name__ == '__main__': + # Use the section of the docstring relevant to the CLI as description + test_data_generation.main(sys.argv[1:], "\n".join(__doc__.splitlines()[:4])) diff --git a/scripts/generate_ecp_tests.py b/scripts/generate_ecp_tests.py new file mode 100755 index 000000000..b506be8f5 --- /dev/null +++ b/scripts/generate_ecp_tests.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Generate test data for ecp functions. + +The command line usage, class structure and available methods are the same +as in generate_bignum_tests.py. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +import sys + +from mbedtls_framework import test_data_generation +# Import modules containing additional test classes +# Test function classes in these modules will be registered by +# the framework +from mbedtls_framework import ecp # pylint: disable=unused-import + +if __name__ == '__main__': + # Use the section of the docstring relevant to the CLI as description + test_data_generation.main(sys.argv[1:], "\n".join(__doc__.splitlines()[:4])) diff --git a/scripts/generate_pkcs7_tests.py b/scripts/generate_pkcs7_tests.py new file mode 100755 index 000000000..0e484b023 --- /dev/null +++ b/scripts/generate_pkcs7_tests.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later +# + +""" +Make fuzz like testing for pkcs7 tests +Given a valid DER pkcs7 file add tests to the test_suite_pkcs7.data file + - It is expected that the pkcs7_asn1_fail( data_t *pkcs7_buf ) + function is defined in test_suite_pkcs7.function + - This is not meant to be portable code, if anything it is meant to serve as + documentation for showing how those ugly tests in test_suite_pkcs7.data were created +""" + + +import sys +from os.path import exists + +PKCS7_TEST_FILE = "../suites/test_suite_pkcs7.data" + +class Test: # pylint: disable=too-few-public-methods + """ + A instance of a test in test_suite_pkcs7.data + """ + def __init__(self, name, depends, func_call): + self.name = name + self.depends = depends + self.func_call = func_call + + # pylint: disable=no-self-use + def to_string(self): + return "\n" + self.name + "\n" + self.depends + "\n" + self.func_call + "\n" + +class TestData: + """ + Take in test_suite_pkcs7.data file. + Allow for new tests to be added. + """ + mandatory_dep = "MBEDTLS_MD_CAN_SHA256" + test_name = "PKCS7 Parse Failure Invalid ASN1" + test_function = "pkcs7_asn1_fail:" + def __init__(self, file_name): + self.file_name = file_name + self.last_test_num, self.old_tests = self.read_test_file(file_name) + self.new_tests = [] + + # pylint: disable=no-self-use + def read_test_file(self, file): + """ + Parse the test_suite_pkcs7.data file. + """ + tests = [] + if not exists(file): + print(file + " Does not exist") + sys.exit() + with open(file, "r", encoding='UTF-8') as fp: + data = fp.read() + lines = [line.strip() for line in data.split('\n') if len(line.strip()) > 1] + i = 0 + while i < len(lines): + if "depends" in lines[i+1]: + tests.append(Test(lines[i], lines[i+1], lines[i+2])) + i += 3 + else: + tests.append(Test(lines[i], None, lines[i+1])) + i += 2 + latest_test_num = float(tests[-1].name.split('#')[1]) + return latest_test_num, tests + + def add(self, name, func_call): + self.last_test_num += 1 + self.new_tests.append(Test(self.test_name + ": " + name + " #" + \ + str(self.last_test_num), "depends_on:" + self.mandatory_dep, \ + self.test_function + '"' + func_call + '"')) + + def write_changes(self): + with open(self.file_name, 'a', encoding='UTF-8') as fw: + fw.write("\n") + for t in self.new_tests: + fw.write(t.to_string()) + + +def asn1_mutate(data): + """ + We have been given an asn1 structure representing a pkcs7. + We want to return an array of slightly modified versions of this data + they should be modified in a way which makes the structure invalid + + We know that asn1 structures are: + |---1 byte showing data type---|----byte(s) for length of data---|---data content--| + We know that some data types can contain other data types. + Return a dictionary of reasons and mutated data types. + """ + + # off the bat just add bytes to start and end of the buffer + mutations = [] + reasons = [] + mutations.append(["00"] + data) + reasons.append("Add null byte to start") + mutations.append(data + ["00"]) + reasons.append("Add null byte to end") + # for every asn1 entry we should attempt to: + # - change the data type tag + # - make the length longer than actual + # - make the length shorter than actual + i = 0 + while i < len(data): + tag_i = i + leng_i = tag_i + 1 + data_i = leng_i + 1 + (int(data[leng_i][1], 16) if data[leng_i][0] == '8' else 0) + if data[leng_i][0] == '8': + length = int(''.join(data[leng_i + 1: data_i]), 16) + else: + length = int(data[leng_i], 16) + + tag = data[tag_i] + print("Looking at ans1: offset " + str(i) + " tag = " + tag + \ + ", length = " + str(length)+ ":") + print(''.join(data[data_i:data_i+length])) + # change tag to something else + if tag == "02": + # turn integers into octet strings + new_tag = "04" + else: + # turn everything else into an integer + new_tag = "02" + mutations.append(data[:tag_i] + [new_tag] + data[leng_i:]) + reasons.append("Change tag " + tag + " to " + new_tag) + + # change lengths to too big + # skip any edge cases which would cause carry over + if int(data[data_i - 1], 16) < 255: + new_length = str(hex(int(data[data_i - 1], 16) + 1))[2:] + if len(new_length) == 1: + new_length = "0"+new_length + mutations.append(data[:data_i -1] + [new_length] + data[data_i:]) + reasons.append("Change length from " + str(length) + " to " \ + + str(length + 1)) + # we can add another test here for tags that contain other tags \ + # where they have more data than there containing tags account for + if tag in ["30", "a0", "31"]: + mutations.append(data[:data_i -1] + [new_length] + \ + data[data_i:data_i + length] + ["00"] + \ + data[data_i + length:]) + reasons.append("Change contents of tag " + tag + " to contain \ + one unaccounted extra byte") + # change lengths to too small + if int(data[data_i - 1], 16) > 0: + new_length = str(hex(int(data[data_i - 1], 16) - 1))[2:] + if len(new_length) == 1: + new_length = "0"+new_length + mutations.append(data[:data_i -1] + [new_length] + data[data_i:]) + reasons.append("Change length from " + str(length) + " to " + str(length - 1)) + + # some tag types contain other tag types so we should iterate into the data + if tag in ["30", "a0", "31"]: + i = data_i + else: + i = data_i + length + + return list(zip(reasons, mutations)) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("USAGE: " + sys.argv[0] + " ") + sys.exit() + + DATA_FILE = sys.argv[1] + TEST_DATA = TestData(PKCS7_TEST_FILE) + with open(DATA_FILE, 'rb') as f: + DATA_STR = f.read().hex() + # make data an array of byte strings eg ['de','ad','be','ef'] + HEX_DATA = list(map(''.join, [[DATA_STR[i], DATA_STR[i+1]] for i in range(0, len(DATA_STR), \ + 2)])) + # returns tuples of test_names and modified data buffers + MUT_ARR = asn1_mutate(HEX_DATA) + + print("made " + str(len(MUT_ARR)) + " new tests") + for new_test in MUT_ARR: + TEST_DATA.add(new_test[0], ''.join(new_test[1])) + + TEST_DATA.write_changes() diff --git a/scripts/generate_psa_tests.py b/scripts/generate_psa_tests.py new file mode 100755 index 000000000..1618e793d --- /dev/null +++ b/scripts/generate_psa_tests.py @@ -0,0 +1,849 @@ +#!/usr/bin/env python3 +"""Generate test data for PSA cryptographic mechanisms. + +With no arguments, generate all test data. With non-option arguments, +generate only the specified files. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +import enum +import re +import sys +from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional + +from mbedtls_framework import crypto_data_tests +from mbedtls_framework import crypto_knowledge +from mbedtls_framework import macro_collector #pylint: disable=unused-import +from mbedtls_framework import psa_information +from mbedtls_framework import psa_storage +from mbedtls_framework import test_case +from mbedtls_framework import test_data_generation + + + +def test_case_for_key_type_not_supported( + verb: str, key_type: str, bits: int, + dependencies: List[str], + *args: str, + param_descr: str = '' +) -> test_case.TestCase: + """Return one test case exercising a key creation method + for an unsupported key type or size. + """ + psa_information.hack_dependencies_not_implemented(dependencies) + tc = test_case.TestCase() + short_key_type = crypto_knowledge.short_expression(key_type) + adverb = 'not' if dependencies else 'never' + if param_descr: + adverb = param_descr + ' ' + adverb + tc.set_description('PSA {} {} {}-bit {} supported' + .format(verb, short_key_type, bits, adverb)) + tc.set_dependencies(dependencies) + tc.set_function(verb + '_not_supported') + tc.set_arguments([key_type] + list(args)) + return tc + +class KeyTypeNotSupported: + """Generate test cases for when a key type is not supported.""" + + def __init__(self, info: psa_information.Information) -> None: + self.constructors = info.constructors + + ALWAYS_SUPPORTED = frozenset([ + 'PSA_KEY_TYPE_DERIVE', + 'PSA_KEY_TYPE_PASSWORD', + 'PSA_KEY_TYPE_PASSWORD_HASH', + 'PSA_KEY_TYPE_RAW_DATA', + 'PSA_KEY_TYPE_HMAC' + ]) + def test_cases_for_key_type_not_supported( + self, + kt: crypto_knowledge.KeyType, + param: Optional[int] = None, + param_descr: str = '', + ) -> Iterator[test_case.TestCase]: + """Return test cases exercising key creation when the given type is unsupported. + + If param is present and not None, emit test cases conditioned on this + parameter not being supported. If it is absent or None, emit test cases + conditioned on the base type not being supported. + """ + if kt.name in self.ALWAYS_SUPPORTED: + # Don't generate test cases for key types that are always supported. + # They would be skipped in all configurations, which is noise. + return + import_dependencies = [('!' if param is None else '') + + psa_information.psa_want_symbol(kt.name)] + if kt.params is not None: + import_dependencies += [('!' if param == i else '') + + psa_information.psa_want_symbol(sym) + for i, sym in enumerate(kt.params)] + if kt.name.endswith('_PUBLIC_KEY'): + generate_dependencies = [] + else: + generate_dependencies = \ + psa_information.fix_key_pair_dependencies(import_dependencies, 'GENERATE') + import_dependencies = \ + psa_information.fix_key_pair_dependencies(import_dependencies, 'BASIC') + for bits in kt.sizes_to_test(): + yield test_case_for_key_type_not_supported( + 'import', kt.expression, bits, + psa_information.finish_family_dependencies(import_dependencies, bits), + test_case.hex_string(kt.key_material(bits)), + param_descr=param_descr, + ) + if not generate_dependencies and param is not None: + # If generation is impossible for this key type, rather than + # supported or not depending on implementation capabilities, + # only generate the test case once. + continue + # For public key we expect that key generation fails with + # INVALID_ARGUMENT. It is handled by KeyGenerate class. + if not kt.is_public(): + yield test_case_for_key_type_not_supported( + 'generate', kt.expression, bits, + psa_information.finish_family_dependencies(generate_dependencies, bits), + str(bits), + param_descr=param_descr, + ) + # To be added: derive + + ECC_KEY_TYPES = ('PSA_KEY_TYPE_ECC_KEY_PAIR', + 'PSA_KEY_TYPE_ECC_PUBLIC_KEY') + DH_KEY_TYPES = ('PSA_KEY_TYPE_DH_KEY_PAIR', + 'PSA_KEY_TYPE_DH_PUBLIC_KEY') + + def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]: + """Generate test cases that exercise the creation of keys of unsupported types.""" + for key_type in sorted(self.constructors.key_types): + if key_type in self.ECC_KEY_TYPES: + continue + if key_type in self.DH_KEY_TYPES: + continue + kt = crypto_knowledge.KeyType(key_type) + yield from self.test_cases_for_key_type_not_supported(kt) + for curve_family in sorted(self.constructors.ecc_curves): + for constr in self.ECC_KEY_TYPES: + kt = crypto_knowledge.KeyType(constr, [curve_family]) + yield from self.test_cases_for_key_type_not_supported( + kt, param_descr='type') + yield from self.test_cases_for_key_type_not_supported( + kt, 0, param_descr='curve') + for dh_family in sorted(self.constructors.dh_groups): + for constr in self.DH_KEY_TYPES: + kt = crypto_knowledge.KeyType(constr, [dh_family]) + yield from self.test_cases_for_key_type_not_supported( + kt, param_descr='type') + yield from self.test_cases_for_key_type_not_supported( + kt, 0, param_descr='group') + +def test_case_for_key_generation( + key_type: str, bits: int, + dependencies: List[str], + *args: str, + result: str = '' +) -> test_case.TestCase: + """Return one test case exercising a key generation. + """ + psa_information.hack_dependencies_not_implemented(dependencies) + tc = test_case.TestCase() + short_key_type = crypto_knowledge.short_expression(key_type) + tc.set_description('PSA {} {}-bit' + .format(short_key_type, bits)) + tc.set_dependencies(dependencies) + tc.set_function('generate_key') + tc.set_arguments([key_type] + list(args) + [result]) + + return tc + +class KeyGenerate: + """Generate positive and negative (invalid argument) test cases for key generation.""" + + def __init__(self, info: psa_information.Information) -> None: + self.constructors = info.constructors + + ECC_KEY_TYPES = ('PSA_KEY_TYPE_ECC_KEY_PAIR', + 'PSA_KEY_TYPE_ECC_PUBLIC_KEY') + DH_KEY_TYPES = ('PSA_KEY_TYPE_DH_KEY_PAIR', + 'PSA_KEY_TYPE_DH_PUBLIC_KEY') + + @staticmethod + def test_cases_for_key_type_key_generation( + kt: crypto_knowledge.KeyType + ) -> Iterator[test_case.TestCase]: + """Return test cases exercising key generation. + + All key types can be generated except for public keys. For public key + PSA_ERROR_INVALID_ARGUMENT status is expected. + """ + result = 'PSA_SUCCESS' + + import_dependencies = [psa_information.psa_want_symbol(kt.name)] + if kt.params is not None: + import_dependencies += [psa_information.psa_want_symbol(sym) + for i, sym in enumerate(kt.params)] + if kt.name.endswith('_PUBLIC_KEY'): + # The library checks whether the key type is a public key generically, + # before it reaches a point where it needs support for the specific key + # type, so it returns INVALID_ARGUMENT for unsupported public key types. + generate_dependencies = [] + result = 'PSA_ERROR_INVALID_ARGUMENT' + else: + generate_dependencies = \ + psa_information.fix_key_pair_dependencies(import_dependencies, 'GENERATE') + for bits in kt.sizes_to_test(): + if kt.name == 'PSA_KEY_TYPE_RSA_KEY_PAIR': + size_dependency = "PSA_VENDOR_RSA_GENERATE_MIN_KEY_BITS <= " + str(bits) + test_dependencies = generate_dependencies + [size_dependency] + else: + test_dependencies = generate_dependencies + yield test_case_for_key_generation( + kt.expression, bits, + psa_information.finish_family_dependencies(test_dependencies, bits), + str(bits), + result + ) + + def test_cases_for_key_generation(self) -> Iterator[test_case.TestCase]: + """Generate test cases that exercise the generation of keys.""" + for key_type in sorted(self.constructors.key_types): + if key_type in self.ECC_KEY_TYPES: + continue + if key_type in self.DH_KEY_TYPES: + continue + kt = crypto_knowledge.KeyType(key_type) + yield from self.test_cases_for_key_type_key_generation(kt) + for curve_family in sorted(self.constructors.ecc_curves): + for constr in self.ECC_KEY_TYPES: + kt = crypto_knowledge.KeyType(constr, [curve_family]) + yield from self.test_cases_for_key_type_key_generation(kt) + for dh_family in sorted(self.constructors.dh_groups): + for constr in self.DH_KEY_TYPES: + kt = crypto_knowledge.KeyType(constr, [dh_family]) + yield from self.test_cases_for_key_type_key_generation(kt) + +class OpFail: + """Generate test cases for operations that must fail.""" + #pylint: disable=too-few-public-methods + + class Reason(enum.Enum): + NOT_SUPPORTED = 0 + INVALID = 1 + INCOMPATIBLE = 2 + PUBLIC = 3 + + def __init__(self, info: psa_information.Information) -> None: + self.constructors = info.constructors + key_type_expressions = self.constructors.generate_expressions( + sorted(self.constructors.key_types) + ) + self.key_types = [crypto_knowledge.KeyType(kt_expr) + for kt_expr in key_type_expressions] + + def make_test_case( + self, + alg: crypto_knowledge.Algorithm, + category: crypto_knowledge.AlgorithmCategory, + reason: 'Reason', + kt: Optional[crypto_knowledge.KeyType] = None, + not_deps: FrozenSet[str] = frozenset(), + ) -> test_case.TestCase: + """Construct a failure test case for a one-key or keyless operation.""" + #pylint: disable=too-many-arguments,too-many-locals + tc = test_case.TestCase() + pretty_alg = alg.short_expression() + if reason == self.Reason.NOT_SUPPORTED: + short_deps = [re.sub(r'PSA_WANT_ALG_', r'', dep) + for dep in not_deps] + pretty_reason = '!' + '&'.join(sorted(short_deps)) + else: + pretty_reason = reason.name.lower() + if kt: + key_type = kt.expression + pretty_type = kt.short_expression() + else: + key_type = '' + pretty_type = '' + tc.set_description('PSA {} {}: {}{}' + .format(category.name.lower(), + pretty_alg, + pretty_reason, + ' with ' + pretty_type if pretty_type else '')) + dependencies = psa_information.automatic_dependencies(alg.base_expression, key_type) + dependencies = psa_information.fix_key_pair_dependencies(dependencies, 'BASIC') + for i, dep in enumerate(dependencies): + if dep in not_deps: + dependencies[i] = '!' + dep + tc.set_dependencies(dependencies) + tc.set_function(category.name.lower() + '_fail') + arguments = [] # type: List[str] + if kt: + key_material = kt.key_material(kt.sizes_to_test()[0]) + arguments += [key_type, test_case.hex_string(key_material)] + arguments.append(alg.expression) + if category.is_asymmetric(): + arguments.append('1' if reason == self.Reason.PUBLIC else '0') + error = ('NOT_SUPPORTED' if reason == self.Reason.NOT_SUPPORTED else + 'INVALID_ARGUMENT') + arguments.append('PSA_ERROR_' + error) + tc.set_arguments(arguments) + return tc + + def no_key_test_cases( + self, + alg: crypto_knowledge.Algorithm, + category: crypto_knowledge.AlgorithmCategory, + ) -> Iterator[test_case.TestCase]: + """Generate failure test cases for keyless operations with the specified algorithm.""" + if alg.can_do(category): + # Compatible operation, unsupported algorithm + for dep in psa_information.automatic_dependencies(alg.base_expression): + yield self.make_test_case(alg, category, + self.Reason.NOT_SUPPORTED, + not_deps=frozenset([dep])) + else: + # Incompatible operation, supported algorithm + yield self.make_test_case(alg, category, self.Reason.INVALID) + + def one_key_test_cases( + self, + alg: crypto_knowledge.Algorithm, + category: crypto_knowledge.AlgorithmCategory, + ) -> Iterator[test_case.TestCase]: + """Generate failure test cases for one-key operations with the specified algorithm.""" + for kt in self.key_types: + key_is_compatible = kt.can_do(alg) + if key_is_compatible and alg.can_do(category): + # Compatible key and operation, unsupported algorithm + for dep in psa_information.automatic_dependencies(alg.base_expression): + yield self.make_test_case(alg, category, + self.Reason.NOT_SUPPORTED, + kt=kt, not_deps=frozenset([dep])) + # Public key for a private-key operation + if category.is_asymmetric() and kt.is_public(): + yield self.make_test_case(alg, category, + self.Reason.PUBLIC, + kt=kt) + elif key_is_compatible: + # Compatible key, incompatible operation, supported algorithm + yield self.make_test_case(alg, category, + self.Reason.INVALID, + kt=kt) + elif alg.can_do(category): + # Incompatible key, compatible operation, supported algorithm + yield self.make_test_case(alg, category, + self.Reason.INCOMPATIBLE, + kt=kt) + else: + # Incompatible key and operation. Don't test cases where + # multiple things are wrong, to keep the number of test + # cases reasonable. + pass + + def test_cases_for_algorithm( + self, + alg: crypto_knowledge.Algorithm, + ) -> Iterator[test_case.TestCase]: + """Generate operation failure test cases for the specified algorithm.""" + for category in crypto_knowledge.AlgorithmCategory: + if category == crypto_knowledge.AlgorithmCategory.PAKE: + # PAKE operations are not implemented yet + pass + elif category.requires_key(): + yield from self.one_key_test_cases(alg, category) + else: + yield from self.no_key_test_cases(alg, category) + + def all_test_cases(self) -> Iterator[test_case.TestCase]: + """Generate all test cases for operations that must fail.""" + algorithms = sorted(self.constructors.algorithms) + for expr in self.constructors.generate_expressions(algorithms): + alg = crypto_knowledge.Algorithm(expr) + yield from self.test_cases_for_algorithm(alg) + + +class StorageKey(psa_storage.Key): + """Representation of a key for storage format testing.""" + + IMPLICIT_USAGE_FLAGS = { + 'PSA_KEY_USAGE_SIGN_HASH': 'PSA_KEY_USAGE_SIGN_MESSAGE', + 'PSA_KEY_USAGE_VERIFY_HASH': 'PSA_KEY_USAGE_VERIFY_MESSAGE' + } #type: Dict[str, str] + """Mapping of usage flags to the flags that they imply.""" + + def __init__( + self, + usage: Iterable[str], + without_implicit_usage: Optional[bool] = False, + **kwargs + ) -> None: + """Prepare to generate a key. + + * `usage` : The usage flags used for the key. + * `without_implicit_usage`: Flag to define to apply the usage extension + """ + usage_flags = set(usage) + if not without_implicit_usage: + for flag in sorted(usage_flags): + if flag in self.IMPLICIT_USAGE_FLAGS: + usage_flags.add(self.IMPLICIT_USAGE_FLAGS[flag]) + if usage_flags: + usage_expression = ' | '.join(sorted(usage_flags)) + else: + usage_expression = '0' + super().__init__(usage=usage_expression, **kwargs) + +class StorageTestData(StorageKey): + """Representation of test case data for storage format testing.""" + + def __init__( + self, + description: str, + expected_usage: Optional[List[str]] = None, + **kwargs + ) -> None: + """Prepare to generate test data + + * `description` : used for the test case names + * `expected_usage`: the usage flags generated as the expected usage flags + in the test cases. CAn differ from the usage flags + stored in the keys because of the usage flags extension. + """ + super().__init__(**kwargs) + self.description = description #type: str + if expected_usage is None: + self.expected_usage = self.usage #type: psa_storage.Expr + elif expected_usage: + self.expected_usage = psa_storage.Expr(' | '.join(expected_usage)) + else: + self.expected_usage = psa_storage.Expr(0) + +class StorageFormat: + """Storage format stability test cases.""" + + def __init__(self, info: psa_information.Information, version: int, forward: bool) -> None: + """Prepare to generate test cases for storage format stability. + + * `info`: information about the API. See the `Information` class. + * `version`: the storage format version to generate test cases for. + * `forward`: if true, generate forward compatibility test cases which + save a key and check that its representation is as intended. Otherwise + generate backward compatibility test cases which inject a key + representation and check that it can be read and used. + """ + self.constructors = info.constructors #type: macro_collector.PSAMacroEnumerator + self.version = version #type: int + self.forward = forward #type: bool + + RSA_OAEP_RE = re.compile(r'PSA_ALG_RSA_OAEP\((.*)\)\Z') + BRAINPOOL_RE = re.compile(r'PSA_KEY_TYPE_\w+\(PSA_ECC_FAMILY_BRAINPOOL_\w+\)\Z') + @classmethod + def exercise_key_with_algorithm( + cls, + key_type: psa_storage.Expr, bits: int, + alg: psa_storage.Expr + ) -> bool: + """Whether to exercise the given key with the given algorithm. + + Normally only the type and algorithm matter for compatibility, and + this is handled in crypto_knowledge.KeyType.can_do(). This function + exists to detect exceptional cases. Exceptional cases detected here + are not tested in OpFail and should therefore have manually written + test cases. + """ + # Some test keys have the RAW_DATA type and attributes that don't + # necessarily make sense. We do this to validate numerical + # encodings of the attributes. + # Raw data keys have no useful exercise anyway so there is no + # loss of test coverage. + if key_type.string == 'PSA_KEY_TYPE_RAW_DATA': + return False + # OAEP requires room for two hashes plus wrapping + m = cls.RSA_OAEP_RE.match(alg.string) + if m: + hash_alg = m.group(1) + hash_length = crypto_knowledge.Algorithm.hash_length(hash_alg) + key_length = (bits + 7) // 8 + # Leave enough room for at least one byte of plaintext + return key_length > 2 * hash_length + 2 + # There's nothing wrong with ECC keys on Brainpool curves, + # but operations with them are very slow. So we only exercise them + # with a single algorithm, not with all possible hashes. We do + # exercise other curves with all algorithms so test coverage is + # perfectly adequate like this. + m = cls.BRAINPOOL_RE.match(key_type.string) + if m and alg.string != 'PSA_ALG_ECDSA_ANY': + return False + return True + + def make_test_case(self, key: StorageTestData) -> test_case.TestCase: + """Construct a storage format test case for the given key. + + If ``forward`` is true, generate a forward compatibility test case: + create a key and validate that it has the expected representation. + Otherwise generate a backward compatibility test case: inject the + key representation into storage and validate that it can be read + correctly. + """ + verb = 'save' if self.forward else 'read' + tc = test_case.TestCase() + tc.set_description(verb + ' ' + key.description) + dependencies = psa_information.automatic_dependencies( + key.lifetime.string, key.type.string, + key.alg.string, key.alg2.string, + ) + dependencies = psa_information.finish_family_dependencies(dependencies, key.bits) + dependencies += psa_information.generate_deps_from_description(key.description) + dependencies = psa_information.fix_key_pair_dependencies(dependencies, 'BASIC') + tc.set_dependencies(dependencies) + tc.set_function('key_storage_' + verb) + if self.forward: + extra_arguments = [] + else: + flags = [] + if self.exercise_key_with_algorithm(key.type, key.bits, key.alg): + flags.append('TEST_FLAG_EXERCISE') + if 'READ_ONLY' in key.lifetime.string: + flags.append('TEST_FLAG_READ_ONLY') + extra_arguments = [' | '.join(flags) if flags else '0'] + tc.set_arguments([key.lifetime.string, + key.type.string, str(key.bits), + key.expected_usage.string, + key.alg.string, key.alg2.string, + '"' + key.material.hex() + '"', + '"' + key.hex() + '"', + *extra_arguments]) + return tc + + def key_for_lifetime( + self, + lifetime: str, + ) -> StorageTestData: + """Construct a test key for the given lifetime.""" + short = lifetime + short = re.sub(r'PSA_KEY_LIFETIME_FROM_PERSISTENCE_AND_LOCATION', + r'', short) + short = crypto_knowledge.short_expression(short) + description = 'lifetime: ' + short + key = StorageTestData(version=self.version, + id=1, lifetime=lifetime, + type='PSA_KEY_TYPE_RAW_DATA', bits=8, + usage=['PSA_KEY_USAGE_EXPORT'], alg=0, alg2=0, + material=b'L', + description=description) + return key + + def all_keys_for_lifetimes(self) -> Iterator[StorageTestData]: + """Generate test keys covering lifetimes.""" + lifetimes = sorted(self.constructors.lifetimes) + expressions = self.constructors.generate_expressions(lifetimes) + for lifetime in expressions: + # Don't attempt to create or load a volatile key in storage + if 'VOLATILE' in lifetime: + continue + # Don't attempt to create a read-only key in storage, + # but do attempt to load one. + if 'READ_ONLY' in lifetime and self.forward: + continue + yield self.key_for_lifetime(lifetime) + + def key_for_usage_flags( + self, + usage_flags: List[str], + short: Optional[str] = None, + test_implicit_usage: Optional[bool] = True + ) -> StorageTestData: + """Construct a test key for the given key usage.""" + extra_desc = ' without implication' if test_implicit_usage else '' + description = 'usage' + extra_desc + ': ' + key1 = StorageTestData(version=self.version, + id=1, lifetime=0x00000001, + type='PSA_KEY_TYPE_RAW_DATA', bits=8, + expected_usage=usage_flags, + without_implicit_usage=not test_implicit_usage, + usage=usage_flags, alg=0, alg2=0, + material=b'K', + description=description) + if short is None: + usage_expr = key1.expected_usage.string + key1.description += crypto_knowledge.short_expression(usage_expr) + else: + key1.description += short + return key1 + + def generate_keys_for_usage_flags(self, **kwargs) -> Iterator[StorageTestData]: + """Generate test keys covering usage flags.""" + known_flags = sorted(self.constructors.key_usage_flags) + yield self.key_for_usage_flags(['0'], **kwargs) + for usage_flag in known_flags: + yield self.key_for_usage_flags([usage_flag], **kwargs) + for flag1, flag2 in zip(known_flags, + known_flags[1:] + [known_flags[0]]): + yield self.key_for_usage_flags([flag1, flag2], **kwargs) + + def generate_key_for_all_usage_flags(self) -> Iterator[StorageTestData]: + known_flags = sorted(self.constructors.key_usage_flags) + yield self.key_for_usage_flags(known_flags, short='all known') + + def all_keys_for_usage_flags(self) -> Iterator[StorageTestData]: + yield from self.generate_keys_for_usage_flags() + yield from self.generate_key_for_all_usage_flags() + + def key_for_type_and_alg( + self, + kt: crypto_knowledge.KeyType, + bits: int, + alg: Optional[crypto_knowledge.Algorithm] = None, + ) -> StorageTestData: + """Construct a test key of the given type. + + If alg is not None, this key allows it. + """ + usage_flags = ['PSA_KEY_USAGE_EXPORT'] + alg1 = 0 #type: psa_storage.Exprable + alg2 = 0 + if alg is not None: + alg1 = alg.expression + usage_flags += alg.usage_flags(public=kt.is_public()) + key_material = kt.key_material(bits) + description = 'type: {} {}-bit'.format(kt.short_expression(1), bits) + if alg is not None: + description += ', ' + alg.short_expression(1) + key = StorageTestData(version=self.version, + id=1, lifetime=0x00000001, + type=kt.expression, bits=bits, + usage=usage_flags, alg=alg1, alg2=alg2, + material=key_material, + description=description) + return key + + def keys_for_type( + self, + key_type: str, + all_algorithms: List[crypto_knowledge.Algorithm], + ) -> Iterator[StorageTestData]: + """Generate test keys for the given key type.""" + kt = crypto_knowledge.KeyType(key_type) + for bits in kt.sizes_to_test(): + # Test a non-exercisable key, as well as exercisable keys for + # each compatible algorithm. + # To do: test reading a key from storage with an incompatible + # or unsupported algorithm. + yield self.key_for_type_and_alg(kt, bits) + compatible_algorithms = [alg for alg in all_algorithms + if kt.can_do(alg)] + for alg in compatible_algorithms: + yield self.key_for_type_and_alg(kt, bits, alg) + + def all_keys_for_types(self) -> Iterator[StorageTestData]: + """Generate test keys covering key types and their representations.""" + key_types = sorted(self.constructors.key_types) + all_algorithms = [crypto_knowledge.Algorithm(alg) + for alg in self.constructors.generate_expressions( + sorted(self.constructors.algorithms) + )] + for key_type in self.constructors.generate_expressions(key_types): + yield from self.keys_for_type(key_type, all_algorithms) + + def keys_for_algorithm(self, alg: str) -> Iterator[StorageTestData]: + """Generate test keys for the encoding of the specified algorithm.""" + # These test cases only validate the encoding of algorithms, not + # whether the key read from storage is suitable for an operation. + # `keys_for_types` generate read tests with an algorithm and a + # compatible key. + descr = crypto_knowledge.short_expression(alg, 1) + usage = ['PSA_KEY_USAGE_EXPORT'] + key1 = StorageTestData(version=self.version, + id=1, lifetime=0x00000001, + type='PSA_KEY_TYPE_RAW_DATA', bits=8, + usage=usage, alg=alg, alg2=0, + material=b'K', + description='alg: ' + descr) + yield key1 + key2 = StorageTestData(version=self.version, + id=1, lifetime=0x00000001, + type='PSA_KEY_TYPE_RAW_DATA', bits=8, + usage=usage, alg=0, alg2=alg, + material=b'L', + description='alg2: ' + descr) + yield key2 + + def all_keys_for_algorithms(self) -> Iterator[StorageTestData]: + """Generate test keys covering algorithm encodings.""" + algorithms = sorted(self.constructors.algorithms) + for alg in self.constructors.generate_expressions(algorithms): + yield from self.keys_for_algorithm(alg) + + def generate_all_keys(self) -> Iterator[StorageTestData]: + """Generate all keys for the test cases.""" + yield from self.all_keys_for_lifetimes() + yield from self.all_keys_for_usage_flags() + yield from self.all_keys_for_types() + yield from self.all_keys_for_algorithms() + + def all_test_cases(self) -> Iterator[test_case.TestCase]: + """Generate all storage format test cases.""" + # First build a list of all keys, then construct all the corresponding + # test cases. This allows all required information to be obtained in + # one go, which is a significant performance gain as the information + # includes numerical values obtained by compiling a C program. + all_keys = list(self.generate_all_keys()) + for key in all_keys: + if key.location_value() != 0: + # Skip keys with a non-default location, because they + # require a driver and we currently have no mechanism to + # determine whether a driver is available. + continue + yield self.make_test_case(key) + +class StorageFormatForward(StorageFormat): + """Storage format stability test cases for forward compatibility.""" + + def __init__(self, info: psa_information.Information, version: int) -> None: + super().__init__(info, version, True) + +class StorageFormatV0(StorageFormat): + """Storage format stability test cases for version 0 compatibility.""" + + def __init__(self, info: psa_information.Information) -> None: + super().__init__(info, 0, False) + + def all_keys_for_usage_flags(self) -> Iterator[StorageTestData]: + """Generate test keys covering usage flags.""" + yield from super().all_keys_for_usage_flags() + yield from self.generate_keys_for_usage_flags(test_implicit_usage=False) + + def keys_for_implicit_usage( + self, + implyer_usage: str, + alg: str, + key_type: crypto_knowledge.KeyType + ) -> StorageTestData: + # pylint: disable=too-many-locals + """Generate test keys for the specified implicit usage flag, + algorithm and key type combination. + """ + bits = key_type.sizes_to_test()[0] + implicit_usage = StorageKey.IMPLICIT_USAGE_FLAGS[implyer_usage] + usage_flags = ['PSA_KEY_USAGE_EXPORT'] + material_usage_flags = usage_flags + [implyer_usage] + expected_usage_flags = material_usage_flags + [implicit_usage] + alg2 = 0 + key_material = key_type.key_material(bits) + usage_expression = crypto_knowledge.short_expression(implyer_usage, 1) + alg_expression = crypto_knowledge.short_expression(alg, 1) + key_type_expression = key_type.short_expression(1) + description = 'implied by {}: {} {} {}-bit'.format( + usage_expression, alg_expression, key_type_expression, bits) + key = StorageTestData(version=self.version, + id=1, lifetime=0x00000001, + type=key_type.expression, bits=bits, + usage=material_usage_flags, + expected_usage=expected_usage_flags, + without_implicit_usage=True, + alg=alg, alg2=alg2, + material=key_material, + description=description) + return key + + def gather_key_types_for_sign_alg(self) -> Dict[str, List[str]]: + # pylint: disable=too-many-locals + """Match possible key types for sign algorithms.""" + # To create a valid combination both the algorithms and key types + # must be filtered. Pair them with keywords created from its names. + incompatible_alg_keyword = frozenset(['RAW', 'ANY', 'PURE']) + incompatible_key_type_keywords = frozenset(['MONTGOMERY']) + keyword_translation = { + 'ECDSA': 'ECC', + 'ED[0-9]*.*' : 'EDWARDS' + } + exclusive_keywords = { + 'EDWARDS': 'ECC' + } + key_types = set(self.constructors.generate_expressions(self.constructors.key_types)) + algorithms = set(self.constructors.generate_expressions(self.constructors.sign_algorithms)) + alg_with_keys = {} #type: Dict[str, List[str]] + translation_table = str.maketrans('(', '_', ')') + for alg in algorithms: + # Generate keywords from the name of the algorithm + alg_keywords = set(alg.partition('(')[0].split(sep='_')[2:]) + # Translate keywords for better matching with the key types + for keyword in alg_keywords.copy(): + for pattern, replace in keyword_translation.items(): + if re.match(pattern, keyword): + alg_keywords.remove(keyword) + alg_keywords.add(replace) + # Filter out incompatible algorithms + if not alg_keywords.isdisjoint(incompatible_alg_keyword): + continue + + for key_type in key_types: + # Generate keywords from the of the key type + key_type_keywords = set(key_type.translate(translation_table).split(sep='_')[3:]) + + # Remove ambiguous keywords + for keyword1, keyword2 in exclusive_keywords.items(): + if keyword1 in key_type_keywords: + key_type_keywords.remove(keyword2) + + if key_type_keywords.isdisjoint(incompatible_key_type_keywords) and\ + not key_type_keywords.isdisjoint(alg_keywords): + if alg in alg_with_keys: + alg_with_keys[alg].append(key_type) + else: + alg_with_keys[alg] = [key_type] + return alg_with_keys + + def all_keys_for_implicit_usage(self) -> Iterator[StorageTestData]: + """Generate test keys for usage flag extensions.""" + # Generate a key type and algorithm pair for each extendable usage + # flag to generate a valid key for exercising. The key is generated + # without usage extension to check the extension compatibility. + alg_with_keys = self.gather_key_types_for_sign_alg() + + for usage in sorted(StorageKey.IMPLICIT_USAGE_FLAGS, key=str): + for alg in sorted(alg_with_keys): + for key_type in sorted(alg_with_keys[alg]): + # The key types must be filtered to fit the specific usage flag. + kt = crypto_knowledge.KeyType(key_type) + if kt.is_public() and '_SIGN_' in usage: + # Can't sign with a public key + continue + yield self.keys_for_implicit_usage(usage, alg, kt) + + def generate_all_keys(self) -> Iterator[StorageTestData]: + yield from super().generate_all_keys() + yield from self.all_keys_for_implicit_usage() + + +class PSATestGenerator(test_data_generation.TestGenerator): + """Test generator subclass including PSA targets and info.""" + # Note that targets whose names contain 'test_format' have their content + # validated by `abi_check.py`. + targets = { + 'test_suite_psa_crypto_generate_key.generated': + lambda info: KeyGenerate(info).test_cases_for_key_generation(), + 'test_suite_psa_crypto_not_supported.generated': + lambda info: KeyTypeNotSupported(info).test_cases_for_not_supported(), + 'test_suite_psa_crypto_low_hash.generated': + lambda info: crypto_data_tests.HashPSALowLevel(info).all_test_cases(), + 'test_suite_psa_crypto_op_fail.generated': + lambda info: OpFail(info).all_test_cases(), + 'test_suite_psa_crypto_storage_format.current': + lambda info: StorageFormatForward(info, 0).all_test_cases(), + 'test_suite_psa_crypto_storage_format.v0': + lambda info: StorageFormatV0(info).all_test_cases(), + } #type: Dict[str, Callable[[psa_information.Information], Iterable[test_case.TestCase]]] + + def __init__(self, options): + super().__init__(options) + self.info = psa_information.Information() + + def generate_target(self, name: str, *target_args) -> None: + super().generate_target(name, self.info) + + +if __name__ == '__main__': + test_data_generation.main(sys.argv[1:], __doc__, PSATestGenerator) diff --git a/scripts/generate_psa_wrappers.py b/scripts/generate_psa_wrappers.py new file mode 100755 index 000000000..701747d4a --- /dev/null +++ b/scripts/generate_psa_wrappers.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Generate wrapper functions for PSA function calls. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +### WARNING: the code in this file has not been extensively reviewed yet. +### We do not think it is harmful, but it may be below our normal standards +### for robustness and maintainability. + +import argparse +import itertools +import os +from typing import Iterator, List, Optional, Tuple + +from mbedtls_framework import build_tree +from mbedtls_framework import c_parsing_helper +from mbedtls_framework import c_wrapper_generator +from mbedtls_framework import typing_util + + +class BufferParameter: + """Description of an input or output buffer parameter sequence to a PSA function.""" + #pylint: disable=too-few-public-methods + + def __init__(self, i: int, is_output: bool, + buffer_name: str, size_name: str) -> None: + """Initialize the parameter information. + + i is the index of the function argument that is the pointer to the buffer. + The size is argument i+1. For a variable-size output, the actual length + goes in argument i+2. + + buffer_name and size_names are the names of arguments i and i+1. + This class does not yet help with the output length. + """ + self.index = i + self.buffer_name = buffer_name + self.size_name = size_name + self.is_output = is_output + + +class PSAWrapperGenerator(c_wrapper_generator.Base): + """Generate a C source file containing wrapper functions for PSA Crypto API calls.""" + + _CPP_GUARDS = ('defined(MBEDTLS_PSA_CRYPTO_C) && ' + + 'defined(MBEDTLS_TEST_HOOKS) && \\\n ' + + '!defined(RECORD_PSA_STATUS_COVERAGE_LOG)') + _WRAPPER_NAME_PREFIX = 'mbedtls_test_wrap_' + _WRAPPER_NAME_SUFFIX = '' + + def gather_data(self) -> None: + root_dir = build_tree.guess_mbedtls_root() + for header_name in ['crypto.h', 'crypto_extra.h']: + header_path = os.path.join(root_dir, 'include', 'psa', header_name) + c_parsing_helper.read_function_declarations(self.functions, header_path) + + _SKIP_FUNCTIONS = frozenset([ + 'mbedtls_psa_external_get_random', # not a library function + 'psa_get_key_domain_parameters', # client-side function + 'psa_get_key_slot_number', # client-side function + 'psa_key_derivation_verify_bytes', # not implemented yet + 'psa_key_derivation_verify_key', # not implemented yet + 'psa_set_key_domain_parameters', # client-side function + ]) + + def _skip_function(self, function: c_wrapper_generator.FunctionInfo) -> bool: + if function.return_type != 'psa_status_t': + return True + if function.name in self._SKIP_FUNCTIONS: + return True + return False + + # PAKE stuff: not implemented yet + _PAKE_STUFF = frozenset([ + 'psa_crypto_driver_pake_inputs_t *', + 'psa_pake_cipher_suite_t *', + ]) + + def _return_variable_name(self, + function: c_wrapper_generator.FunctionInfo) -> str: + """The name of the variable that will contain the return value.""" + if function.return_type == 'psa_status_t': + return 'status' + return super()._return_variable_name(function) + + _FUNCTION_GUARDS = c_wrapper_generator.Base._FUNCTION_GUARDS.copy() \ + #pylint: disable=protected-access + _FUNCTION_GUARDS.update({ + 'mbedtls_psa_register_se_key': 'defined(MBEDTLS_PSA_CRYPTO_SE_C)', + 'mbedtls_psa_inject_entropy': 'defined(MBEDTLS_PSA_INJECT_ENTROPY)', + 'mbedtls_psa_external_get_random': 'defined(MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG)', + 'mbedtls_psa_platform_get_builtin_key': 'defined(MBEDTLS_PSA_CRYPTO_BUILTIN_KEYS)', + }) + + @staticmethod + def _detect_buffer_parameters(arguments: List[c_parsing_helper.ArgumentInfo], + argument_names: List[str]) -> Iterator[BufferParameter]: + """Detect function arguments that are buffers (pointer, size [,length]).""" + types = ['' if arg.suffix else arg.type for arg in arguments] + # pairs = list of (type_of_arg_N, type_of_arg_N+1) + # where each type_of_arg_X is the empty string if the type is an array + # or there is no argument X. + pairs = enumerate(itertools.zip_longest(types, types[1:], fillvalue='')) + for i, t01 in pairs: + if (t01[0] == 'const uint8_t *' or t01[0] == 'uint8_t *') and \ + t01[1] == 'size_t': + yield BufferParameter(i, not t01[0].startswith('const '), + argument_names[i], argument_names[i+1]) + + @staticmethod + def _write_poison_buffer_parameter(out: typing_util.Writable, + param: BufferParameter, + poison: bool) -> None: + """Write poisoning or unpoisoning code for a buffer parameter. + + Write poisoning code if poison is true, unpoisoning code otherwise. + """ + out.write(' MBEDTLS_TEST_MEMORY_{}({}, {});\n'.format( + 'POISON' if poison else 'UNPOISON', + param.buffer_name, param.size_name + )) + + def _write_poison_buffer_parameters(self, out: typing_util.Writable, + buffer_parameters: List[BufferParameter], + poison: bool) -> None: + """Write poisoning or unpoisoning code for the buffer parameters. + + Write poisoning code if poison is true, unpoisoning code otherwise. + """ + if not buffer_parameters: + return + out.write('#if !defined(MBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERS)\n') + for param in buffer_parameters: + self._write_poison_buffer_parameter(out, param, poison) + out.write('#endif /* !defined(MBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERS) */\n') + + @staticmethod + def _parameter_should_be_copied(function_name: str, + _buffer_name: Optional[str]) -> bool: + """Whether the specified buffer argument to a PSA function should be copied. + """ + # False-positives that do not need buffer copying + if function_name in ('mbedtls_psa_inject_entropy', + 'psa_crypto_driver_pake_get_password', + 'psa_crypto_driver_pake_get_user', + 'psa_crypto_driver_pake_get_peer'): + return False + + return True + + def _write_function_call(self, out: typing_util.Writable, + function: c_wrapper_generator.FunctionInfo, + argument_names: List[str]) -> None: + buffer_parameters = list( + param + for param in self._detect_buffer_parameters(function.arguments, + argument_names) + if self._parameter_should_be_copied(function.name, + function.arguments[param.index].name)) + self._write_poison_buffer_parameters(out, buffer_parameters, True) + super()._write_function_call(out, function, argument_names) + self._write_poison_buffer_parameters(out, buffer_parameters, False) + + def _write_prologue(self, out: typing_util.Writable, header: bool) -> None: + super()._write_prologue(out, header) + out.write(""" +#if {} + +#include + +#include +#include +#include +""" + .format(self._CPP_GUARDS)) + + def _write_epilogue(self, out: typing_util.Writable, header: bool) -> None: + out.write(""" +#endif /* {} */ +""" + .format(self._CPP_GUARDS)) + super()._write_epilogue(out, header) + + +class PSALoggingWrapperGenerator(PSAWrapperGenerator, c_wrapper_generator.Logging): + """Generate a C source file containing wrapper functions that log PSA Crypto API calls.""" + + def __init__(self, stream: str) -> None: + super().__init__() + self.set_stream(stream) + + _PRINTF_TYPE_CAST = c_wrapper_generator.Logging._PRINTF_TYPE_CAST.copy() + _PRINTF_TYPE_CAST.update({ + 'mbedtls_svc_key_id_t': 'unsigned', + 'psa_algorithm_t': 'unsigned', + 'psa_drv_slot_number_t': 'unsigned long long', + 'psa_key_derivation_step_t': 'int', + 'psa_key_id_t': 'unsigned', + 'psa_key_slot_number_t': 'unsigned long long', + 'psa_key_lifetime_t': 'unsigned', + 'psa_key_type_t': 'unsigned', + 'psa_key_usage_flags_t': 'unsigned', + 'psa_pake_role_t': 'int', + 'psa_pake_step_t': 'int', + 'psa_status_t': 'int', + }) + + def _printf_parameters(self, typ: str, var: str) -> Tuple[str, List[str]]: + if typ.startswith('const '): + typ = typ[6:] + if typ == 'uint8_t *': + # Skip buffers + return '', [] + if typ.endswith('operation_t *'): + return '', [] + if typ in self._PAKE_STUFF: + return '', [] + if typ == 'psa_key_attributes_t *': + return (var + '={id=%u, lifetime=0x%08x, type=0x%08x, bits=%u, alg=%08x, usage=%08x}', + ['(unsigned) psa_get_key_{}({})'.format(field, var) + for field in ['id', 'lifetime', 'type', 'bits', 'algorithm', 'usage_flags']]) + return super()._printf_parameters(typ, var) + + +DEFAULT_C_OUTPUT_FILE_NAME = 'tests/src/psa_test_wrappers.c' +DEFAULT_H_OUTPUT_FILE_NAME = 'tests/include/test/psa_test_wrappers.h' + +def main() -> None: + parser = argparse.ArgumentParser(description=globals()['__doc__']) + parser.add_argument('--log', + help='Stream to log to (default: no logging code)') + parser.add_argument('--output-c', + metavar='FILENAME', + default=DEFAULT_C_OUTPUT_FILE_NAME, + help=('Output .c file path (default: {}; skip .c output if empty)' + .format(DEFAULT_C_OUTPUT_FILE_NAME))) + parser.add_argument('--output-h', + metavar='FILENAME', + default=DEFAULT_H_OUTPUT_FILE_NAME, + help=('Output .h file path (default: {}; skip .h output if empty)' + .format(DEFAULT_H_OUTPUT_FILE_NAME))) + options = parser.parse_args() + if options.log: + generator = PSALoggingWrapperGenerator(options.log) #type: PSAWrapperGenerator + else: + generator = PSAWrapperGenerator() + generator.gather_data() + if options.output_h: + generator.write_h_file(options.output_h) + if options.output_c: + generator.write_c_file(options.output_c) + +if __name__ == '__main__': + main() diff --git a/scripts/generate_test_cert_macros.py b/scripts/generate_test_cert_macros.py new file mode 100755 index 000000000..da3aecc40 --- /dev/null +++ b/scripts/generate_test_cert_macros.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +""" +Generate `tests/src/test_certs.h` which includes certficaties/keys/certificate list for testing. +""" + +# +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + + +import os +import sys +import argparse +import jinja2 +from mbedtls_framework.build_tree import guess_project_root + +TEST_DIR = os.path.join(guess_project_root(), 'tests') +DATA_FILES_PATH = os.path.join(TEST_DIR, 'data_files') + +INPUT_ARGS = [ + ("string", "TEST_CA_CRT_EC_PEM", DATA_FILES_PATH + "/test-ca2.crt"), + ("binary", "TEST_CA_CRT_EC_DER", DATA_FILES_PATH + "/test-ca2.crt.der"), + ("string", "TEST_CA_KEY_EC_PEM", DATA_FILES_PATH + "/test-ca2.key.enc"), + ("password", "TEST_CA_PWD_EC_PEM", "PolarSSLTest"), + ("binary", "TEST_CA_KEY_EC_DER", DATA_FILES_PATH + "/test-ca2.key.der"), + ("string", "TEST_CA_CRT_RSA_SHA256_PEM", DATA_FILES_PATH + "/test-ca-sha256.crt"), + ("binary", "TEST_CA_CRT_RSA_SHA256_DER", DATA_FILES_PATH + "/test-ca-sha256.crt.der"), + ("string", "TEST_CA_CRT_RSA_SHA1_PEM", DATA_FILES_PATH + "/test-ca-sha1.crt"), + ("binary", "TEST_CA_CRT_RSA_SHA1_DER", DATA_FILES_PATH + "/test-ca-sha1.crt.der"), + ("string", "TEST_CA_KEY_RSA_PEM", DATA_FILES_PATH + "/test-ca.key"), + ("password", "TEST_CA_PWD_RSA_PEM", "PolarSSLTest"), + ("binary", "TEST_CA_KEY_RSA_DER", DATA_FILES_PATH + "/test-ca.key.der"), + ("string", "TEST_SRV_CRT_EC_PEM", DATA_FILES_PATH + "/server5.crt"), + ("binary", "TEST_SRV_CRT_EC_DER", DATA_FILES_PATH + "/server5.crt.der"), + ("string", "TEST_SRV_KEY_EC_PEM", DATA_FILES_PATH + "/server5.key"), + ("binary", "TEST_SRV_KEY_EC_DER", DATA_FILES_PATH + "/server5.key.der"), + ("string", "TEST_SRV_CRT_RSA_SHA256_PEM", DATA_FILES_PATH + "/server2-sha256.crt"), + ("binary", "TEST_SRV_CRT_RSA_SHA256_DER", DATA_FILES_PATH + "/server2-sha256.crt.der"), + ("string", "TEST_SRV_CRT_RSA_SHA1_PEM", DATA_FILES_PATH + "/server2.crt"), + ("binary", "TEST_SRV_CRT_RSA_SHA1_DER", DATA_FILES_PATH + "/server2.crt.der"), + ("string", "TEST_SRV_KEY_RSA_PEM", DATA_FILES_PATH + "/server2.key"), + ("binary", "TEST_SRV_KEY_RSA_DER", DATA_FILES_PATH + "/server2.key.der"), + ("string", "TEST_CLI_CRT_EC_PEM", DATA_FILES_PATH + "/cli2.crt"), + ("binary", "TEST_CLI_CRT_EC_DER", DATA_FILES_PATH + "/cli2.crt.der"), + ("string", "TEST_CLI_KEY_EC_PEM", DATA_FILES_PATH + "/cli2.key"), + ("binary", "TEST_CLI_KEY_EC_DER", DATA_FILES_PATH + "/cli2.key.der"), + ("string", "TEST_CLI_CRT_RSA_PEM", DATA_FILES_PATH + "/cli-rsa-sha256.crt"), + ("binary", "TEST_CLI_CRT_RSA_DER", DATA_FILES_PATH + "/cli-rsa-sha256.crt.der"), + ("string", "TEST_CLI_KEY_RSA_PEM", DATA_FILES_PATH + "/cli-rsa.key"), + ("binary", "TEST_CLI_KEY_RSA_DER", DATA_FILES_PATH + "/cli-rsa.key.der"), +] + +def main(): + parser = argparse.ArgumentParser() + default_output_path = os.path.join(TEST_DIR, 'src', 'test_certs.h') + parser.add_argument('--output', type=str, default=default_output_path) + parser.add_argument('--list-dependencies', action='store_true') + args = parser.parse_args() + + if args.list_dependencies: + files_list = [arg[2] for arg in INPUT_ARGS] + print(" ".join(files_list)) + return + + generate(INPUT_ARGS, output=args.output) + +#pylint: disable=dangerous-default-value, unused-argument +def generate(values=[], output=None): + """Generate C header file. + """ + template_loader = jinja2.FileSystemLoader(DATA_FILES_PATH) + template_env = jinja2.Environment( + loader=template_loader, lstrip_blocks=True, trim_blocks=True, + keep_trailing_newline=True) + + def read_as_c_array(filename): + with open(filename, 'rb') as f: + data = f.read(12) + while data: + yield ', '.join(['{:#04x}'.format(b) for b in data]) + data = f.read(12) + + def read_lines(filename): + with open(filename) as f: + try: + for line in f: + yield line.strip() + except: + print(filename) + raise + + def put_to_column(value, position=0): + return ' '*position + value + + template_env.filters['read_as_c_array'] = read_as_c_array + template_env.filters['read_lines'] = read_lines + template_env.filters['put_to_column'] = put_to_column + + template = template_env.get_template('test_certs.h.jinja2') + + with open(output, 'w') as f: + f.write(template.render(macros=values)) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/generate_test_code.py b/scripts/generate_test_code.py new file mode 100755 index 000000000..5f711bfb1 --- /dev/null +++ b/scripts/generate_test_code.py @@ -0,0 +1,1277 @@ +#!/usr/bin/env python3 +# Test suites code generator. +# +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +""" +This script is a key part of Mbed TLS test suites framework. For +understanding the script it is important to understand the +framework. This doc string contains a summary of the framework +and explains the function of this script. + +Mbed TLS test suites: +===================== +Scope: +------ +The test suites focus on unit testing the crypto primitives and also +include x509 parser tests. Tests can be added to test any Mbed TLS +module. However, the framework is not capable of testing SSL +protocol, since that requires full stack execution and that is best +tested as part of the system test. + +Test case definition: +--------------------- +Tests are defined in a test_suite_[.].data +file. A test definition contains: + test name + optional build macro dependencies + test function + test parameters + +Test dependencies are build macros that can be specified to indicate +the build config in which the test is valid. For example if a test +depends on a feature that is only enabled by defining a macro. Then +that macro should be specified as a dependency of the test. + +Test function is the function that implements the test steps. This +function is specified for different tests that perform same steps +with different parameters. + +Test parameters are specified in string form separated by ':'. +Parameters can be of type string, binary data specified as hex +string and integer constants specified as integer, macro or +as an expression. Following is an example test definition: + + AES 128 GCM Encrypt and decrypt 8 bytes + depends_on:MBEDTLS_AES_C:MBEDTLS_GCM_C + enc_dec_buf:MBEDTLS_CIPHER_AES_128_GCM:"AES-128-GCM":128:8:-1 + +Test functions: +--------------- +Test functions are coded in C in test_suite_.function files. +Functions file is itself not compilable and contains special +format patterns to specify test suite dependencies, start and end +of functions and function dependencies. Check any existing functions +file for example. + +Execution: +---------- +Tests are executed in 3 steps: +- Generating test_suite_[.].c file + for each corresponding .data file. +- Building each source file into executables. +- Running each executable and printing report. + +Generating C test source requires more than just the test functions. +Following extras are required: +- Process main() +- Reading .data file and dispatching test cases. +- Platform specific test case execution +- Dependency checking +- Integer expression evaluation +- Test function dispatch + +Build dependencies and integer expressions (in the test parameters) +are specified as strings in the .data file. Their run time value is +not known at the generation stage. Hence, they need to be translated +into run time evaluations. This script generates the run time checks +for dependencies and integer expressions. + +Similarly, function names have to be translated into function calls. +This script also generates code for function dispatch. + +The extra code mentioned here is either generated by this script +or it comes from the input files: helpers file, platform file and +the template file. + +Helper file: +------------ +Helpers file contains common helper/utility functions and data. + +Platform file: +-------------- +Platform file contains platform specific setup code and test case +dispatch code. For example, host_test.function reads test data +file from host's file system and dispatches tests. + +Template file: +--------- +Template file for example main_test.function is a template C file in +which generated code and code from input files is substituted to +generate a compilable C file. It also contains skeleton functions for +dependency checks, expression evaluation and function dispatch. These +functions are populated with checks and return codes by this script. + +Template file contains "replacement" fields that are formatted +strings processed by Python string.Template.substitute() method. + +This script: +============ +Core function of this script is to fill the template file with +code that is generated or read from helpers and platform files. + +This script replaces following fields in the template and generates +the test source file: + +__MBEDTLS_TEST_TEMPLATE__TEST_COMMON_HELPERS + All common code from helpers.function + is substituted here. +__MBEDTLS_TEST_TEMPLATE__FUNCTIONS_CODE + Test functions are substituted here + from the input test_suit_xyz.function + file. C preprocessor checks are generated + for the build dependencies specified + in the input file. This script also + generates wrappers for the test + functions with code to expand the + string parameters read from the data + file. +__MBEDTLS_TEST_TEMPLATE__EXPRESSION_CODE + This script enumerates the + expressions in the .data file and + generates code to handle enumerated + expression Ids and return the values. +__MBEDTLS_TEST_TEMPLATE__DEP_CHECK_CODE + This script enumerates all + build dependencies and generate + code to handle enumerated build + dependency Id and return status: if + the dependency is defined or not. +__MBEDTLS_TEST_TEMPLATE__DISPATCH_CODE + This script enumerates the functions + specified in the input test data file + and generates the initializer for the + function table in the template + file. +__MBEDTLS_TEST_TEMPLATE__PLATFORM_CODE + Platform specific setup and test + dispatch code. + +""" + + +import os +import re +import sys +import string +import argparse + + +# Types recognized as signed integer arguments in test functions. +SIGNED_INTEGER_TYPES = frozenset([ + 'char', + 'short', + 'short int', + 'int', + 'int8_t', + 'int16_t', + 'int32_t', + 'int64_t', + 'intmax_t', + 'long', + 'long int', + 'long long int', + 'mbedtls_mpi_sint', + 'psa_status_t', +]) +# Types recognized as string arguments in test functions. +STRING_TYPES = frozenset(['char*', 'const char*', 'char const*']) +# Types recognized as hex data arguments in test functions. +DATA_TYPES = frozenset(['data_t*', 'const data_t*', 'data_t const*']) + +BEGIN_HEADER_REGEX = r'/\*\s*BEGIN_HEADER\s*\*/' +END_HEADER_REGEX = r'/\*\s*END_HEADER\s*\*/' + +BEGIN_SUITE_HELPERS_REGEX = r'/\*\s*BEGIN_SUITE_HELPERS\s*\*/' +END_SUITE_HELPERS_REGEX = r'/\*\s*END_SUITE_HELPERS\s*\*/' + +BEGIN_DEP_REGEX = r'BEGIN_DEPENDENCIES' +END_DEP_REGEX = r'END_DEPENDENCIES' + +BEGIN_CASE_REGEX = r'/\*\s*BEGIN_CASE\s*(?P.*?)\s*\*/' +END_CASE_REGEX = r'/\*\s*END_CASE\s*\*/' + +DEPENDENCY_REGEX = r'depends_on:(?P.*)' +C_IDENTIFIER_REGEX = r'!?[a-z_][a-z0-9_]*' +CONDITION_OPERATOR_REGEX = r'[!=]=|[<>]=?' +# forbid 0ddd which might be accidentally octal or accidentally decimal +CONDITION_VALUE_REGEX = r'[-+]?(0x[0-9a-f]+|0|[1-9][0-9]*)' +CONDITION_REGEX = r'({})(?:\s*({})\s*({}))?$'.format(C_IDENTIFIER_REGEX, + CONDITION_OPERATOR_REGEX, + CONDITION_VALUE_REGEX) +TEST_FUNCTION_VALIDATION_REGEX = r'\s*void\s+(?P\w+)\s*\(' +FUNCTION_ARG_LIST_END_REGEX = r'.*\)' +EXIT_LABEL_REGEX = r'^exit:' + + +class GeneratorInputError(Exception): + """ + 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: + """ + This class extends the file object with attribute line_no, + that indicates line number for the line that is read. + """ + + def __init__(self, file_name) -> None: + """ + Instantiate the file object and initialize the line number to 0. + + :param file_name: File path to open. + """ + # private mix-in file object + self._f = open(file_name, 'rb') + self._line_no = 0 + + def __iter__(self): + return self + + def __next__(self): + """ + This method makes FileWrapper iterable. + It counts the line numbers as each line is read. + + :return: Line read from file. + """ + line = self._f.__next__() + self._line_no += 1 + # Convert byte array to string with correct encoding and + # strip any whitespaces added in the decoding process. + return line.decode(sys.getdefaultencoding()).rstrip()+ '\n' + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._f.__exit__(exc_type, exc_val, exc_tb) + + @property + def line_no(self): + """ + Property that indicates line number for the line that is read. + """ + return self._line_no + + @property + def name(self): + """ + Property that indicates name of the file that is read. + """ + return self._f.name + + +def split_dep(dep): + """ + Split NOT character '!' from dependency. Used by gen_dependencies() + + :param dep: Dependency list + :return: string tuple. Ex: ('!', MACRO) for !MACRO and ('', MACRO) for + MACRO. + """ + return ('!', dep[1:]) if dep[0] == '!' else ('', dep) + + +def gen_dependencies(dependencies): + """ + 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 dependencies: List of dependencies. + :return: if defined and endif code with macro annotations for + readability. + """ + dep_start = ''.join(['#if %sdefined(%s)\n' % (x, y) for x, y in + map(split_dep, dependencies)]) + dep_end = ''.join(['#endif /* %s */\n' % + x for x in reversed(dependencies)]) + + return dep_start, dep_end + + +def gen_dependencies_one_line(dependencies): + """ + Similar to gen_dependencies() but generates dependency checks in one line. + Useful for generating code with #else block. + + :param dependencies: List of dependencies. + :return: Preprocessor check code + """ + defines = '#if ' if dependencies else '' + defines += ' && '.join(['%sdefined(%s)' % (x, y) for x, y in map( + split_dep, dependencies)]) + return defines + + +def gen_function_wrapper(name, local_vars, args_dispatch): + """ + Creates test function wrapper code. A wrapper has the code to + unpack parameters from parameters[] array. + + :param name: Test function name + :param local_vars: Local variables declaration code + :param args_dispatch: List of dispatch arguments. + Ex: ['(char *) params[0]', '*((int *) params[1])'] + :return: Test function wrapper. + """ + # Then create the wrapper + wrapper = ''' +void {name}_wrapper( void ** params ) +{{ +{unused_params}{locals} + {name}( {args} ); +}} +'''.format(name=name, + unused_params='' if args_dispatch else ' (void)params;\n', + args=', '.join(args_dispatch), + locals=local_vars) + return wrapper + + +def gen_dispatch(name, dependencies): + """ + 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 dependencies: List of dependencies + :return: Dispatch code. + """ + if dependencies: + preprocessor_check = gen_dependencies_one_line(dependencies) + dispatch_code = ''' +{preprocessor_check} + {name}_wrapper, +#else + NULL, +#endif +'''.format(preprocessor_check=preprocessor_check, name=name) + else: + dispatch_code = ''' + {name}_wrapper, +'''.format(name=name) + + return dispatch_code + + +def parse_until_pattern(funcs_f, end_regex): + """ + 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 .function file + :param end_regex: Pattern to stop parsing + :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: + if re.search(end_regex, line): + break + headers += line + else: + raise GeneratorInputError("file: %s - end pattern [%s] not found!" % + (funcs_f.name, end_regex)) + + return headers + + +def validate_dependency(dependency): + """ + Validates a C macro and raises GeneratorInputError on invalid input. + :param dependency: Input macro dependency + :return: input dependency stripped of leading & trailing white spaces. + """ + dependency = dependency.strip() + if not re.match(CONDITION_REGEX, dependency, re.I): + raise GeneratorInputError('Invalid dependency %s' % dependency) + return dependency + + +def parse_dependencies(inp_str): + """ + Parses dependencies out of inp_str, validates them and returns a + list of macros. + + :param inp_str: Input string with macros delimited by ':'. + :return: list of dependencies + """ + dependencies = list(map(validate_dependency, inp_str.split(':'))) + return dependencies + + +def parse_suite_dependencies(funcs_f): + """ + 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 .function file + :return: List of test suite dependencies. + """ + dependencies = [] + for line in funcs_f: + match = re.search(DEPENDENCY_REGEX, line.strip()) + if match: + try: + dependencies = parse_dependencies(match.group('dependencies')) + except GeneratorInputError as error: + raise GeneratorInputError( + str(error) + " - %s:%d" % (funcs_f.name, funcs_f.line_no)) + if re.search(END_DEP_REGEX, line): + break + else: + raise GeneratorInputError("file: %s - end dependency pattern [%s]" + " not found!" % (funcs_f.name, + END_DEP_REGEX)) + + return dependencies + + +def parse_function_dependencies(line): + """ + 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 .function file that has dependencies. + :return: List of dependencies. + """ + dependencies = [] + match = re.search(BEGIN_CASE_REGEX, line) + dep_str = match.group('depends_on') + if dep_str: + match = re.search(DEPENDENCY_REGEX, dep_str) + if match: + dependencies += parse_dependencies(match.group('dependencies')) + + return dependencies + + +ARGUMENT_DECLARATION_REGEX = re.compile(r'(.+?) ?(?:\bconst\b)? ?(\w+)\Z', re.S) +def parse_function_argument(arg, arg_idx, args, local_vars, args_dispatch): + """ + Parses one test function's argument declaration. + + :param arg: argument declaration. + :param arg_idx: current wrapper argument index. + :param args: accumulator of arguments' internal types. + :param local_vars: accumulator of internal variable declarations. + :param args_dispatch: accumulator of argument usage expressions. + :return: the number of new wrapper arguments, + or None if the argument declaration is invalid. + """ + # Normalize whitespace + arg = arg.strip() + arg = re.sub(r'\s*\*\s*', r'*', arg) + arg = re.sub(r'\s+', r' ', arg) + # Extract name and type + m = ARGUMENT_DECLARATION_REGEX.search(arg) + if not m: + # E.g. "int x[42]" + return None + typ, _ = m.groups() + if typ in SIGNED_INTEGER_TYPES: + args.append('int') + args_dispatch.append('((mbedtls_test_argument_t *) params[%d])->sint' % arg_idx) + return 1 + if typ in STRING_TYPES: + args.append('char*') + args_dispatch.append('(char *) params[%d]' % arg_idx) + return 1 + if typ in DATA_TYPES: + args.append('hex') + # create a structure + pointer_initializer = '(uint8_t *) params[%d]' % arg_idx + len_initializer = '((mbedtls_test_argument_t *) params[%d])->len' % (arg_idx+1) + local_vars.append(' data_t data%d = {%s, %s};\n' % + (arg_idx, pointer_initializer, len_initializer)) + args_dispatch.append('&data%d' % arg_idx) + return 2 + return None + +ARGUMENT_LIST_REGEX = re.compile(r'\((.*?)\)', re.S) +def parse_function_arguments(line): + """ + 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 .function file that has a function + signature. + :return: argument list, local variables for + wrapper function and argument dispatch code. + """ + # Process arguments, ex: arg1, arg2 ) + # This script assumes that the argument list is terminated by ')' + # i.e. the test functions will not have a function pointer + # argument. + m = ARGUMENT_LIST_REGEX.search(line) + arg_list = m.group(1).strip() + if arg_list in ['', 'void']: + return [], '', [] + args = [] + local_vars = [] + args_dispatch = [] + arg_idx = 0 + for arg in arg_list.split(','): + indexes = parse_function_argument(arg, arg_idx, + args, local_vars, args_dispatch) + if indexes is None: + raise ValueError("Test function arguments can only be 'int', " + "'char *' or 'data_t'\n%s" % line) + arg_idx += indexes + + return args, ''.join(local_vars), args_dispatch + + +def generate_function_code(name, code, local_vars, args_dispatch, + dependencies): + """ + Generate function code with preprocessor checks and parameter dispatch + wrapper. + + :param name: Function name + :param code: Function code + :param local_vars: Local variables for function wrapper + :param args_dispatch: Argument dispatch code + :param dependencies: Preprocessor dependencies list + :return: Final function code + """ + # Add exit label if not present + if code.find('exit:') == -1: + split_code = code.rsplit('}', 1) + if len(split_code) == 2: + code = """exit: + ; +}""".join(split_code) + + code += gen_function_wrapper(name, local_vars, args_dispatch) + preprocessor_check_start, preprocessor_check_end = \ + gen_dependencies(dependencies) + return preprocessor_check_start + code + preprocessor_check_end + +COMMENT_START_REGEX = re.compile(r'/[*/]') + +def skip_comments(line, stream): + """Remove comments in line. + + If the line contains an unfinished comment, read more lines from stream + until the line that contains the comment. + + :return: The original line with inner comments replaced by spaces. + Trailing comments and whitespace may be removed completely. + """ + pos = 0 + while True: + opening = COMMENT_START_REGEX.search(line, pos) + if not opening: + break + if line[opening.start(0) + 1] == '/': # //... + continuation = line + # Count the number of line breaks, to keep line numbers aligned + # in the output. + line_count = 1 + while continuation.endswith('\\\n'): + # This errors out if the file ends with an unfinished line + # comment. That's acceptable to not complicate the code further. + continuation = next(stream) + line_count += 1 + return line[:opening.start(0)].rstrip() + '\n' * line_count + # Parsing /*...*/, looking for the end + closing = line.find('*/', opening.end(0)) + while closing == -1: + # This errors out if the file ends with an unfinished block + # comment. That's acceptable to not complicate the code further. + line += next(stream) + closing = line.find('*/', opening.end(0)) + pos = closing + 2 + # Replace inner comment by spaces. There needs to be at least one space + # for things like 'int/*ihatespaces*/foo'. Go further and preserve the + # width of the comment and line breaks, this way positions in error + # messages remain correct. + line = (line[:opening.start(0)] + + re.sub(r'.', r' ', line[opening.start(0):pos]) + + line[pos:]) + # Strip whitespace at the end of lines (it's irrelevant to error messages). + return re.sub(r' +(\n|\Z)', r'\1', line) + +def parse_function_code(funcs_f, dependencies, suite_dependencies): + """ + Parses out a function from function file object and generates + function and dispatch code. + + :param funcs_f: file object of the functions file. + :param dependencies: List of dependencies + :param suite_dependencies: List of test suite dependencies + :return: Function name, arguments, function code and dispatch code. + """ + line_directive = '#line %d "%s"\n' % (funcs_f.line_no + 1, funcs_f.name) + code = '' + has_exit_label = False + for line in funcs_f: + # Check function signature. Function signature may be split + # across multiple lines. Here we try to find the start of + # arguments list, then remove '\n's and apply the regex to + # detect function start. + line = skip_comments(line, funcs_f) + up_to_arg_list_start = code + line[:line.find('(') + 1] + match = re.match(TEST_FUNCTION_VALIDATION_REGEX, + up_to_arg_list_start.replace('\n', ' '), re.I) + if match: + # check if we have full signature i.e. split in more lines + name = match.group('func_name') + if not re.match(FUNCTION_ARG_LIST_END_REGEX, line): + for lin in funcs_f: + line += skip_comments(lin, funcs_f) + if re.search(FUNCTION_ARG_LIST_END_REGEX, line): + break + args, local_vars, args_dispatch = parse_function_arguments( + line) + code += line + break + code += line + else: + raise GeneratorInputError("file: %s - Test functions not found!" % + funcs_f.name) + + # Prefix test function name with 'test_' + code = code.replace(name, 'test_' + name, 1) + name = 'test_' + name + + # If a test function has no arguments then add 'void' argument to + # avoid "-Wstrict-prototypes" warnings from clang + if len(args) == 0: + code = code.replace('()', '(void)', 1) + + for line in funcs_f: + if re.search(END_CASE_REGEX, line): + break + if not has_exit_label: + has_exit_label = \ + re.search(EXIT_LABEL_REGEX, line.strip()) is not None + code += line + else: + raise GeneratorInputError("file: %s - end case pattern [%s] not " + "found!" % (funcs_f.name, END_CASE_REGEX)) + + code = line_directive + code + code = generate_function_code(name, code, local_vars, args_dispatch, + dependencies) + dispatch_code = gen_dispatch(name, suite_dependencies + dependencies) + return (name, args, code, dispatch_code) + + +def parse_functions(funcs_f): + """ + 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 + code, function code and a dict with function identifiers + and arguments info. + """ + suite_helpers = '' + suite_dependencies = [] + suite_functions = '' + func_info = {} + function_idx = 0 + dispatch_code = '' + for line in funcs_f: + if re.search(BEGIN_HEADER_REGEX, line): + suite_helpers += parse_until_pattern(funcs_f, END_HEADER_REGEX) + elif re.search(BEGIN_SUITE_HELPERS_REGEX, line): + suite_helpers += parse_until_pattern(funcs_f, + END_SUITE_HELPERS_REGEX) + elif re.search(BEGIN_DEP_REGEX, line): + suite_dependencies += parse_suite_dependencies(funcs_f) + elif re.search(BEGIN_CASE_REGEX, line): + try: + dependencies = parse_function_dependencies(line) + except GeneratorInputError as error: + raise GeneratorInputError( + "%s:%d: %s" % (funcs_f.name, funcs_f.line_no, + str(error))) + func_name, args, func_code, func_dispatch =\ + parse_function_code(funcs_f, dependencies, suite_dependencies) + suite_functions += func_code + # Generate dispatch code and enumeration info + if func_name in func_info: + raise GeneratorInputError( + "file: %s - function %s re-declared at line %d" % + (funcs_f.name, func_name, funcs_f.line_no)) + func_info[func_name] = (function_idx, args) + dispatch_code += '/* Function Id: %d */\n' % function_idx + dispatch_code += func_dispatch + function_idx += 1 + + func_code = (suite_helpers + + suite_functions).join(gen_dependencies(suite_dependencies)) + return suite_dependencies, dispatch_code, func_code, func_info + + +def escaped_split(inp_str, split_char): + """ + Split inp_str on character split_char but ignore if escaped. + Since, return value is used to write back to the intermediate + data file, any escape characters in the input are retained in the + output. + + :param inp_str: String to split + :param split_char: Split character + :return: List of splits + """ + if len(split_char) > 1: + raise ValueError('Expected split character. Found string!') + out = re.sub(r'(\\.)|' + split_char, + lambda m: m.group(1) or '\n', inp_str, + len(inp_str)).split('\n') + out = [x for x in out if x] + return out + + +def parse_test_data(data_f): + """ + 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 line number, test name, function name, + dependency list and function argument list. + """ + __state_read_name = 0 + __state_read_args = 1 + state = __state_read_name + dependencies = [] + name = '' + for line in data_f: + line = line.strip() + # Skip comments + if line.startswith('#'): + continue + + # Blank line indicates end of test + if not line: + if state == __state_read_args: + raise GeneratorInputError("[%s:%d] Newline before arguments. " + "Test function and arguments " + "missing for %s" % + (data_f.name, data_f.line_no, name)) + continue + + if state == __state_read_name: + # Read test name + name = line + state = __state_read_args + elif state == __state_read_args: + # Check dependencies + match = re.search(DEPENDENCY_REGEX, line) + if match: + try: + dependencies = parse_dependencies( + match.group('dependencies')) + except GeneratorInputError as error: + raise GeneratorInputError( + str(error) + " - %s:%d" % + (data_f.name, data_f.line_no)) + else: + # Read test vectors + parts = escaped_split(line, ':') + test_function = parts[0] + args = parts[1:] + yield data_f.line_no, name, test_function, dependencies, args + dependencies = [] + state = __state_read_name + if state == __state_read_args: + raise GeneratorInputError("[%s:%d] Newline before arguments. " + "Test function and arguments missing for " + "%s" % (data_f.name, data_f.line_no, name)) + + +def gen_dep_check(dep_id, dep): + """ + Generate code for checking dependency with the associated + identifier. + + :param dep_id: Dependency identifier + :param dep: Dependency macro + :return: Dependency check code + """ + if dep_id < 0: + raise GeneratorInputError("Dependency Id should be a positive " + "integer.") + _not, dep = ('!', dep[1:]) if dep[0] == '!' else ('', dep) + if not dep: + raise GeneratorInputError("Dependency should not be an empty string.") + + dependency = re.match(CONDITION_REGEX, dep, re.I) + if not dependency: + raise GeneratorInputError('Invalid dependency %s' % dep) + + _defined = '' if dependency.group(2) else 'defined' + _cond = dependency.group(2) if dependency.group(2) else '' + _value = dependency.group(3) if dependency.group(3) else '' + + dep_check = ''' + case {id}: + {{ +#if {_not}{_defined}({macro}{_cond}{_value}) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + }} + break;'''.format(_not=_not, _defined=_defined, + macro=dependency.group(1), id=dep_id, + _cond=_cond, _value=_value) + return dep_check + + +def gen_expression_check(exp_id, exp): + """ + Generates code for evaluating an integer expression using + associated expression Id. + + :param exp_id: Expression Identifier + :param exp: Expression/Macro + :return: Expression check code + """ + if exp_id < 0: + raise GeneratorInputError("Expression Id should be a positive " + "integer.") + if not exp: + raise GeneratorInputError("Expression should not be an empty string.") + exp_code = ''' + case {exp_id}: + {{ + *out_value = {expression}; + }} + break;'''.format(exp_id=exp_id, expression=exp) + return exp_code + + +def write_dependencies(out_data_f, test_dependencies, unique_dependencies): + """ + 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_dependencies: Dependencies + :param unique_dependencies: Mutable list to track unique dependencies + that are global to this re-entrant function. + :return: returns dependency check code. + """ + dep_check_code = '' + if test_dependencies: + out_data_f.write('depends_on') + for dep in test_dependencies: + if dep not in unique_dependencies: + unique_dependencies.append(dep) + dep_id = unique_dependencies.index(dep) + dep_check_code += gen_dep_check(dep_id, dep) + else: + dep_id = unique_dependencies.index(dep) + out_data_f.write(':' + str(dep_id)) + out_data_f.write('\n') + return dep_check_code + + +INT_VAL_REGEX = re.compile(r'-?(\d+|0x[0-9a-f]+)$', re.I) +def val_is_int(val: str) -> bool: + """Whether val is suitable as an 'int' parameter in the .datax file.""" + if not INT_VAL_REGEX.match(val): + return False + # Limit the range to what is guaranteed to get through strtol() + return abs(int(val, 0)) <= 0x7fffffff + +def write_parameters(out_data_f, test_args, func_args, unique_expressions): + """ + 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 + :param func_args: Function arguments + :param unique_expressions: Mutable list to track unique + expressions that are global to this re-entrant function. + :return: Returns expression check code. + """ + expression_code = '' + for i, _ in enumerate(test_args): + typ = func_args[i] + val = test_args[i] + + # Pass small integer constants literally. This reduces the size of + # the C code. Register anything else as an expression. + if typ == 'int' and not val_is_int(val): + typ = 'exp' + if val not in unique_expressions: + unique_expressions.append(val) + # exp_id can be derived from len(). But for + # readability and consistency with case of existing + # let's use index(). + exp_id = unique_expressions.index(val) + expression_code += gen_expression_check(exp_id, val) + val = exp_id + else: + val = unique_expressions.index(val) + out_data_f.write(':' + typ + ':' + str(val)) + out_data_f.write('\n') + return expression_code + + +def gen_suite_dep_checks(suite_dependencies, dep_check_code, expression_code): + """ + Generates preprocessor checks for test suite dependencies. + + :param suite_dependencies: Test suite dependencies read from the + .function file. + :param dep_check_code: Dependency check code + :param expression_code: Expression check code + :return: Dependency and expression code guarded by test suite + dependencies. + """ + if suite_dependencies: + preprocessor_check = gen_dependencies_one_line(suite_dependencies) + dep_check_code = ''' +{preprocessor_check} +{code} +#endif +'''.format(preprocessor_check=preprocessor_check, code=dep_check_code) + expression_code = ''' +{preprocessor_check} +{code} +#endif +'''.format(preprocessor_check=preprocessor_check, code=expression_code) + return dep_check_code, expression_code + + +def get_function_info(func_info, function_name, line_no): + """Look up information about a test function by name. + + Raise an informative expression if function_name is not found. + + :param func_info: dictionary mapping function names to their information. + :param function_name: the function name as written in the .function and + .data files. + :param line_no: line number for error messages. + :return Function information (id, args). + """ + test_function_name = 'test_' + function_name + if test_function_name not in func_info: + raise GeneratorInputError("%d: Function %s not found!" % + (line_no, test_function_name)) + return func_info[test_function_name] + + +def gen_from_test_data(data_f, out_data_f, func_info, suite_dependencies): + """ + 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 + :param func_info: Dict keyed by function and with function id + and arguments info + :param suite_dependencies: Test suite dependencies + :return: Returns dependency and expression check code + """ + unique_dependencies = [] + unique_expressions = [] + dep_check_code = '' + expression_code = '' + for line_no, test_name, function_name, test_dependencies, test_args in \ + parse_test_data(data_f): + out_data_f.write(test_name + '\n') + + # Write dependencies + dep_check_code += write_dependencies(out_data_f, test_dependencies, + unique_dependencies) + + # Write test function name + func_id, func_args = \ + get_function_info(func_info, function_name, line_no) + out_data_f.write(str(func_id)) + + # Write parameters + if len(test_args) != len(func_args): + raise GeneratorInputError("%d: Invalid number of arguments in test " + "%s. See function %s signature." % + (line_no, test_name, function_name)) + expression_code += write_parameters(out_data_f, test_args, func_args, + unique_expressions) + + # Write a newline as test case separator + out_data_f.write('\n') + + dep_check_code, expression_code = gen_suite_dep_checks( + suite_dependencies, dep_check_code, expression_code) + return dep_check_code, expression_code + + +def add_input_info(funcs_file, data_file, template_file, + c_file, snippets): + """ + Add generator input info in snippets. + + :param funcs_file: Functions file object + :param data_file: Data file object + :param template_file: Template file object + :param c_file: Output C file object + :param snippets: Dictionary to contain code pieces to be + substituted in the template. + :return: + """ + snippets['test_file'] = c_file + snippets['test_main_file'] = template_file + snippets['test_case_file'] = funcs_file + snippets['test_case_data_file'] = data_file + + +def read_code_from_input_files(platform_file, helpers_file, + out_data_file, snippets): + """ + Read code from input files and create substitutions for replacement + strings in the template file. + + :param platform_file: Platform file object + :param helpers_file: Helper functions file object + :param out_data_file: Output intermediate data file object + :param snippets: Dictionary to contain code pieces to be + substituted in the template. + :return: + """ + # Read helpers + with open(helpers_file, 'r') as help_f, open(platform_file, 'r') as \ + platform_f: + 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( + 'DATA_FILE', out_data_file.replace('\\', '\\\\')) # escape '\' + + +def write_test_source_file(template_file, c_file, snippets): + """ + Write output source file with generated source code. + + :param template_file: Template file name + :param c_file: Output source file + :param snippets: Generated and code snippets + :return: + """ + + # Create a placeholder pattern with the correct named capture groups + # to override the default provided with Template. + # Match nothing (no way of escaping placeholders). + escaped = "(?P(?!))" + # Match the "__MBEDTLS_TEST_TEMPLATE__PLACEHOLDER_NAME" pattern. + named = "__MBEDTLS_TEST_TEMPLATE__(?P[A-Z][_A-Z0-9]*)" + # Match nothing (no braced placeholder syntax). + braced = "(?P(?!))" + # If not already matched, a "__MBEDTLS_TEST_TEMPLATE__" prefix is invalid. + invalid = "(?P__MBEDTLS_TEST_TEMPLATE__)" + placeholder_pattern = re.compile("|".join([escaped, named, braced, invalid])) + + with open(template_file, 'r') as template_f, open(c_file, 'w') as c_f: + for line_no, line in enumerate(template_f.readlines(), 1): + # Update line number. +1 as #line directive sets next line number + snippets['line_no'] = line_no + 1 + template = string.Template(line) + template.pattern = placeholder_pattern + snippets = {k.upper():v for (k, v) in snippets.items()} + code = template.substitute(**snippets) + c_f.write(code) + + +def parse_function_file(funcs_file, snippets): + """ + Parse function file and generate function dispatch code. + + :param funcs_file: Functions file name + :param snippets: Dictionary to contain code pieces to be + substituted in the template. + :return: + """ + with FileWrapper(funcs_file) as funcs_f: + suite_dependencies, dispatch_code, func_code, func_info = \ + parse_functions(funcs_f) + snippets['functions_code'] = func_code + snippets['dispatch_code'] = dispatch_code + return suite_dependencies, func_info + + +def generate_intermediate_data_file(data_file, out_data_file, + suite_dependencies, func_info, snippets): + """ + Generates intermediate data file from input data file and + information read from functions file. + + :param data_file: Data file name + :param out_data_file: Output/Intermediate data file + :param suite_dependencies: List of suite dependencies. + :param func_info: Function info parsed from functions file. + :param snippets: Dictionary to contain code pieces to be + substituted in the template. + :return: + """ + with FileWrapper(data_file) as data_f, \ + open(out_data_file, 'w') as out_data_f: + dep_check_code, expression_code = gen_from_test_data( + data_f, out_data_f, func_info, suite_dependencies) + snippets['dep_check_code'] = dep_check_code + snippets['expression_code'] = expression_code + + +def generate_code(**input_info): + """ + Generates C source code from test suite file, data file, common + helpers file and platform file. + + input_info expands to following parameters: + funcs_file: Functions file object + data_file: Data file object + template_file: Template file object + platform_file: Platform file object + helpers_file: Helper functions file object + suites_dir: Test suites dir + c_file: Output C file object + out_data_file: Output intermediate data file object + :return: + """ + funcs_file = input_info['funcs_file'] + data_file = input_info['data_file'] + template_file = input_info['template_file'] + platform_file = input_info['platform_file'] + helpers_file = input_info['helpers_file'] + suites_dir = input_info['suites_dir'] + c_file = input_info['c_file'] + out_data_file = input_info['out_data_file'] + for name, path in [('Functions file', funcs_file), + ('Data file', data_file), + ('Template file', template_file), + ('Platform file', platform_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)) + + snippets = {'generator_script': os.path.basename(__file__)} + read_code_from_input_files(platform_file, helpers_file, + out_data_file, snippets) + add_input_info(funcs_file, data_file, template_file, + c_file, snippets) + suite_dependencies, func_info = parse_function_file(funcs_file, snippets) + generate_intermediate_data_file(data_file, out_data_file, + suite_dependencies, func_info, snippets) + write_test_source_file(template_file, c_file, snippets) + + +def main(): + """ + Command line parser. + + :return: + """ + parser = argparse.ArgumentParser( + description='Dynamically generate test suite code.') + + parser.add_argument("-f", "--functions-file", + dest="funcs_file", + help="Functions file", + metavar="FUNCTIONS_FILE", + required=True) + + parser.add_argument("-d", "--data-file", + dest="data_file", + help="Data file", + metavar="DATA_FILE", + required=True) + + parser.add_argument("-t", "--template-file", + dest="template_file", + help="Template file", + metavar="TEMPLATE_FILE", + required=True) + + parser.add_argument("-s", "--suites-dir", + dest="suites_dir", + help="Suites dir", + metavar="SUITES_DIR", + required=True) + + parser.add_argument("--helpers-file", + dest="helpers_file", + help="Helpers file", + metavar="HELPERS_FILE", + required=True) + + parser.add_argument("-p", "--platform-file", + dest="platform_file", + help="Platform code file", + metavar="PLATFORM_FILE", + required=True) + + parser.add_argument("-o", "--out-dir", + dest="out_dir", + help="Dir where generated code and scripts are copied", + metavar="OUT_DIR", + required=True) + + args = parser.parse_args() + + data_file_name = os.path.basename(args.data_file) + data_name = os.path.splitext(data_file_name)[0] + + out_c_file = os.path.join(args.out_dir, data_name + '.c') + out_data_file = os.path.join(args.out_dir, data_name + '.datax') + + out_c_file_dir = os.path.dirname(out_c_file) + out_data_file_dir = os.path.dirname(out_data_file) + for directory in [out_c_file_dir, out_data_file_dir]: + if not os.path.exists(directory): + os.makedirs(directory) + + generate_code(funcs_file=args.funcs_file, data_file=args.data_file, + template_file=args.template_file, + platform_file=args.platform_file, + helpers_file=args.helpers_file, suites_dir=args.suites_dir, + c_file=out_c_file, out_data_file=out_data_file) + + +if __name__ == "__main__": + try: + main() + except GeneratorInputError as err: + sys.exit("%s: input error: %s" % + (os.path.basename(sys.argv[0]), str(err))) diff --git a/scripts/generate_test_keys.py b/scripts/generate_test_keys.py new file mode 100755 index 000000000..effc65ac3 --- /dev/null +++ b/scripts/generate_test_keys.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +"""Module generating EC and RSA keys to be used in test_suite_pk instead of +generating the required key at run time. This helps speeding up testing.""" + +from typing import Iterator, List, Tuple +import re +import argparse +from mbedtls_framework.asymmetric_key_data import ASYMMETRIC_KEY_DATA +from mbedtls_framework.build_tree import guess_project_root + +BYTES_PER_LINE = 16 + +def c_byte_array_literal_content(array_name: str, key_data: bytes) -> Iterator[str]: + yield 'const unsigned char ' + yield array_name + yield '[] = {' + for index in range(0, len(key_data), BYTES_PER_LINE): + yield '\n ' + for b in key_data[index:index + BYTES_PER_LINE]: + yield ' {:#04x},'.format(b) + yield '\n};' + +def convert_der_to_c(array_name: str, key_data: bytes) -> str: + return ''.join(c_byte_array_literal_content(array_name, key_data)) + +def get_key_type(key: str) -> str: + if re.match('PSA_KEY_TYPE_RSA_.*', key): + return "rsa" + elif re.match('PSA_KEY_TYPE_ECC_.*', key): + return "ec" + else: + print("Unhandled key type {}".format(key)) + return "unknown" + +def get_ec_key_family(key: str) -> str: + match = re.search(r'.*\((.*)\)', key) + if match is None: + raise Exception("Unable to get EC family from {}".format(key)) + return match.group(1) + +# Legacy EC group ID do not support all the key types that PSA does, so the +# following dictionaries are used for: +# - getting prefix/suffix for legacy curve names +# - understand if the curve is supported in legacy symbols (MBEDTLS_ECP_DP_...) +EC_NAME_CONVERSION = { + 'PSA_ECC_FAMILY_SECP_K1': { + 192: ('secp', 'k1'), + 224: ('secp', 'k1'), + 256: ('secp', 'k1') + }, + 'PSA_ECC_FAMILY_SECP_R1': { + 192: ('secp', 'r1'), + 224: ('secp', 'r1'), + 256: ('secp', 'r1'), + 384: ('secp', 'r1'), + 521: ('secp', 'r1') + }, + 'PSA_ECC_FAMILY_BRAINPOOL_P_R1': { + 256: ('bp', 'r1'), + 384: ('bp', 'r1'), + 512: ('bp', 'r1') + }, + 'PSA_ECC_FAMILY_MONTGOMERY': { + 255: ('curve', '19'), + 448: ('curve', '') + } +} + +def get_ec_curve_name(priv_key: str, bits: int) -> str: + ec_family = get_ec_key_family(priv_key) + try: + prefix = EC_NAME_CONVERSION[ec_family][bits][0] + suffix = EC_NAME_CONVERSION[ec_family][bits][1] + except KeyError: + return "" + return prefix + str(bits) + suffix + +def get_look_up_table_entry(key_type: str, group_id_or_keybits: str, + priv_array_name: str, pub_array_name: str) -> Iterator[str]: + if key_type == "ec": + yield " {{ {}, 0,\n".format(group_id_or_keybits) + else: + yield " {{ 0, {},\n".format(group_id_or_keybits) + yield " {0}, sizeof({0}),\n".format(priv_array_name) + yield " {0}, sizeof({0}) }},".format(pub_array_name) + + +def write_output_file(output_file_name: str, arrays: str, look_up_table: str): + with open(output_file_name, 'wt') as output: + output.write("""\ +/********************************************************************************* + * This file was automatically generated from framework/scripts/generate_test_keys.py. + * Please do not edit it manually. + *********************************************************************************/ +""") + output.write(arrays) + output.write(""" +struct predefined_key_element {{ + int group_id; // EC group ID; 0 for RSA keys + int keybits; // bits size of RSA key; 0 for EC keys + const unsigned char *priv_key; + size_t priv_key_len; + const unsigned char *pub_key; + size_t pub_key_len; +}}; + +struct predefined_key_element predefined_keys[] = {{ +{} +}}; + +/* End of generated file */ +""".format(look_up_table)) + +def collect_keys() -> Tuple[str, str]: + """" + This function reads key data from ASYMMETRIC_KEY_DATA and, only for the + keys supported in legacy ECP/RSA modules, it returns 2 strings: + - the 1st contains C arrays declaration of these keys and + - the 2nd contains the final look-up table for all these arrays. + """ + arrays = [] + look_up_table = [] + + # Get a list of private keys only in order to get a single item for every + # (key type, key bits) pair. We know that ASYMMETRIC_KEY_DATA + # contains also the public counterpart. + priv_keys = [key for key in ASYMMETRIC_KEY_DATA if '_KEY_PAIR' in key] + priv_keys = sorted(priv_keys) + + for priv_key in priv_keys: + key_type = get_key_type(priv_key) + # Ignore keys which are not EC or RSA + if key_type == "unknown": + continue + + pub_key = re.sub('_KEY_PAIR', '_PUBLIC_KEY', priv_key) + + for bits in ASYMMETRIC_KEY_DATA[priv_key]: + if key_type == "ec": + curve = get_ec_curve_name(priv_key, bits) + # Ignore EC curves unsupported in legacy symbols + if curve == "": + continue + # Create output array name + if key_type == "rsa": + array_name_base = "_".join(["test", key_type, str(bits)]) + else: + array_name_base = "_".join(["test", key_type, curve]) + array_name_priv = array_name_base + "_priv" + array_name_pub = array_name_base + "_pub" + # Convert bytearray to C array + c_array_priv = convert_der_to_c(array_name_priv, ASYMMETRIC_KEY_DATA[priv_key][bits]) + c_array_pub = convert_der_to_c(array_name_pub, ASYMMETRIC_KEY_DATA[pub_key][bits]) + # Write the C array to the output file + arrays.append(''.join(["\n", c_array_priv, "\n", c_array_pub, "\n"])) + # Update the lookup table + if key_type == "ec": + group_id_or_keybits = "MBEDTLS_ECP_DP_" + curve.upper() + else: + group_id_or_keybits = str(bits) + look_up_table.append(''.join(get_look_up_table_entry(key_type, group_id_or_keybits, + array_name_priv, array_name_pub))) + + return ''.join(arrays), '\n'.join(look_up_table) + +def main() -> None: + default_output_path = guess_project_root() + "/tests/src/test_keys.h" + + argparser = argparse.ArgumentParser() + argparser.add_argument("--output", help="Output file", default=default_output_path) + args = argparser.parse_args() + + output_file = args.output + + arrays, look_up_table = collect_keys() + + write_output_file(output_file, arrays, look_up_table) + +if __name__ == '__main__': + main() diff --git a/scripts/test_generate_test_code.py b/scripts/test_generate_test_code.py new file mode 100755 index 000000000..abc46a729 --- /dev/null +++ b/scripts/test_generate_test_code.py @@ -0,0 +1,1915 @@ +#!/usr/bin/env python3 +# Unit test for generate_test_code.py +# +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +""" +Unit tests for generate_test_code.py +""" + +from io import StringIO +from unittest import TestCase, main as unittest_main +from unittest.mock import patch + +from generate_test_code import gen_dependencies, gen_dependencies_one_line +from generate_test_code import gen_function_wrapper, gen_dispatch +from generate_test_code import parse_until_pattern, GeneratorInputError +from generate_test_code import parse_suite_dependencies +from generate_test_code import parse_function_dependencies +from generate_test_code import parse_function_arguments, parse_function_code +from generate_test_code import parse_functions, END_HEADER_REGEX +from generate_test_code import END_SUITE_HELPERS_REGEX, escaped_split +from generate_test_code import parse_test_data, gen_dep_check +from generate_test_code import gen_expression_check, write_dependencies +from generate_test_code import write_parameters, gen_suite_dep_checks +from generate_test_code import gen_from_test_data + + +class GenDep(TestCase): + """ + Test suite for function gen_dep() + """ + + def test_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = ['DEP1', 'DEP2'] + dep_start, dep_end = gen_dependencies(dependencies) + preprocessor1, preprocessor2 = dep_start.splitlines() + endif1, endif2 = dep_end.splitlines() + self.assertEqual(preprocessor1, '#if defined(DEP1)', + 'Preprocessor generated incorrectly') + self.assertEqual(preprocessor2, '#if defined(DEP2)', + 'Preprocessor generated incorrectly') + self.assertEqual(endif1, '#endif /* DEP2 */', + 'Preprocessor generated incorrectly') + self.assertEqual(endif2, '#endif /* DEP1 */', + 'Preprocessor generated incorrectly') + + def test_disabled_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = ['!DEP1', '!DEP2'] + dep_start, dep_end = gen_dependencies(dependencies) + preprocessor1, preprocessor2 = dep_start.splitlines() + endif1, endif2 = dep_end.splitlines() + self.assertEqual(preprocessor1, '#if !defined(DEP1)', + 'Preprocessor generated incorrectly') + self.assertEqual(preprocessor2, '#if !defined(DEP2)', + 'Preprocessor generated incorrectly') + self.assertEqual(endif1, '#endif /* !DEP2 */', + 'Preprocessor generated incorrectly') + self.assertEqual(endif2, '#endif /* !DEP1 */', + 'Preprocessor generated incorrectly') + + def test_mixed_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = ['!DEP1', 'DEP2'] + dep_start, dep_end = gen_dependencies(dependencies) + preprocessor1, preprocessor2 = dep_start.splitlines() + endif1, endif2 = dep_end.splitlines() + self.assertEqual(preprocessor1, '#if !defined(DEP1)', + 'Preprocessor generated incorrectly') + self.assertEqual(preprocessor2, '#if defined(DEP2)', + 'Preprocessor generated incorrectly') + self.assertEqual(endif1, '#endif /* DEP2 */', + 'Preprocessor generated incorrectly') + self.assertEqual(endif2, '#endif /* !DEP1 */', + 'Preprocessor generated incorrectly') + + def test_empty_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = [] + dep_start, dep_end = gen_dependencies(dependencies) + self.assertEqual(dep_start, '', 'Preprocessor generated incorrectly') + self.assertEqual(dep_end, '', 'Preprocessor generated incorrectly') + + def test_large_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = [] + count = 10 + for i in range(count): + dependencies.append('DEP%d' % i) + dep_start, dep_end = gen_dependencies(dependencies) + self.assertEqual(len(dep_start.splitlines()), count, + 'Preprocessor generated incorrectly') + self.assertEqual(len(dep_end.splitlines()), count, + 'Preprocessor generated incorrectly') + + +class GenDepOneLine(TestCase): + """ + Test Suite for testing gen_dependencies_one_line() + """ + + def test_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = ['DEP1', 'DEP2'] + dep_str = gen_dependencies_one_line(dependencies) + self.assertEqual(dep_str, '#if defined(DEP1) && defined(DEP2)', + 'Preprocessor generated incorrectly') + + def test_disabled_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = ['!DEP1', '!DEP2'] + dep_str = gen_dependencies_one_line(dependencies) + self.assertEqual(dep_str, '#if !defined(DEP1) && !defined(DEP2)', + 'Preprocessor generated incorrectly') + + def test_mixed_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = ['!DEP1', 'DEP2'] + dep_str = gen_dependencies_one_line(dependencies) + self.assertEqual(dep_str, '#if !defined(DEP1) && defined(DEP2)', + 'Preprocessor generated incorrectly') + + def test_empty_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = [] + dep_str = gen_dependencies_one_line(dependencies) + self.assertEqual(dep_str, '', 'Preprocessor generated incorrectly') + + def test_large_dependencies_list(self): + """ + Test that gen_dep() correctly creates dependencies for given + dependency list. + :return: + """ + dependencies = [] + count = 10 + for i in range(count): + dependencies.append('DEP%d' % i) + dep_str = gen_dependencies_one_line(dependencies) + expected = '#if ' + ' && '.join(['defined(%s)' % + x for x in dependencies]) + self.assertEqual(dep_str, expected, + 'Preprocessor generated incorrectly') + + +class GenFunctionWrapper(TestCase): + """ + Test Suite for testing gen_function_wrapper() + """ + + def test_params_unpack(self): + """ + Test that params are properly unpacked in the function call. + + :return: + """ + code = gen_function_wrapper('test_a', '', ('a', 'b', 'c', 'd')) + expected = ''' +void test_a_wrapper( void ** params ) +{ + + test_a( a, b, c, d ); +} +''' + self.assertEqual(code, expected) + + def test_local(self): + """ + Test that params are properly unpacked in the function call. + + :return: + """ + code = gen_function_wrapper('test_a', + 'int x = 1;', ('x', 'b', 'c', 'd')) + expected = ''' +void test_a_wrapper( void ** params ) +{ +int x = 1; + test_a( x, b, c, d ); +} +''' + self.assertEqual(code, expected) + + def test_empty_params(self): + """ + Test that params are properly unpacked in the function call. + + :return: + """ + code = gen_function_wrapper('test_a', '', ()) + expected = ''' +void test_a_wrapper( void ** params ) +{ + (void)params; + + test_a( ); +} +''' + self.assertEqual(code, expected) + + +class GenDispatch(TestCase): + """ + Test suite for testing gen_dispatch() + """ + + def test_dispatch(self): + """ + Test that dispatch table entry is generated correctly. + :return: + """ + code = gen_dispatch('test_a', ['DEP1', 'DEP2']) + expected = ''' +#if defined(DEP1) && defined(DEP2) + test_a_wrapper, +#else + NULL, +#endif +''' + self.assertEqual(code, expected) + + def test_empty_dependencies(self): + """ + Test empty dependency list. + :return: + """ + code = gen_dispatch('test_a', []) + expected = ''' + test_a_wrapper, +''' + self.assertEqual(code, expected) + + +class StringIOWrapper(StringIO): + """ + file like class to mock file object in tests. + """ + def __init__(self, file_name, data, line_no=0): + """ + Init file handle. + + :param file_name: + :param data: + :param line_no: + """ + super(StringIOWrapper, self).__init__(data) + self.line_no = line_no + self.name = file_name + + def next(self): + """ + 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. + + :return: Line read from file. + """ + parent = super(StringIOWrapper, self) + line = parent.__next__() + return line + + def readline(self, _length=0): + """ + Wrap the base class readline. + + :param length: + :return: + """ + line = super(StringIOWrapper, self).readline() + if line is not None: + self.line_no += 1 + return line + + +class ParseUntilPattern(TestCase): + """ + Test Suite for testing parse_until_pattern(). + """ + + def test_suite_headers(self): + """ + Test that suite headers are parsed correctly. + + :return: + """ + data = '''#include "mbedtls/ecp.h" + +#define ECP_PF_UNKNOWN -1 +/* END_HEADER */ +''' + expected = '''#line 1 "test_suite_ut.function" +#include "mbedtls/ecp.h" + +#define ECP_PF_UNKNOWN -1 +''' + stream = StringIOWrapper('test_suite_ut.function', data, line_no=0) + headers = parse_until_pattern(stream, END_HEADER_REGEX) + self.assertEqual(headers, expected) + + def test_line_no(self): + """ + Test that #line is set to correct line no. in source .function file. + + :return: + """ + data = '''#include "mbedtls/ecp.h" + +#define ECP_PF_UNKNOWN -1 +/* END_HEADER */ +''' + offset_line_no = 5 + expected = '''#line %d "test_suite_ut.function" +#include "mbedtls/ecp.h" + +#define ECP_PF_UNKNOWN -1 +''' % (offset_line_no + 1) + stream = StringIOWrapper('test_suite_ut.function', data, + offset_line_no) + headers = parse_until_pattern(stream, END_HEADER_REGEX) + self.assertEqual(headers, expected) + + def test_no_end_header_comment(self): + """ + Test that InvalidFileFormat is raised when end header comment is + missing. + :return: + """ + data = '''#include "mbedtls/ecp.h" + +#define ECP_PF_UNKNOWN -1 + +''' + stream = StringIOWrapper('test_suite_ut.function', data) + self.assertRaises(GeneratorInputError, parse_until_pattern, stream, + END_HEADER_REGEX) + + +class ParseSuiteDependencies(TestCase): + """ + Test Suite for testing parse_suite_dependencies(). + """ + + def test_suite_dependencies(self): + """ + + :return: + """ + data = ''' + * depends_on:MBEDTLS_ECP_C + * END_DEPENDENCIES + */ +''' + expected = ['MBEDTLS_ECP_C'] + stream = StringIOWrapper('test_suite_ut.function', data) + dependencies = parse_suite_dependencies(stream) + self.assertEqual(dependencies, expected) + + def test_no_end_dep_comment(self): + """ + Test that InvalidFileFormat is raised when end dep comment is missing. + :return: + """ + data = ''' +* depends_on:MBEDTLS_ECP_C +''' + stream = StringIOWrapper('test_suite_ut.function', data) + self.assertRaises(GeneratorInputError, parse_suite_dependencies, + stream) + + def test_dependencies_split(self): + """ + Test that InvalidFileFormat is raised when end dep comment is missing. + :return: + """ + data = ''' + * depends_on:MBEDTLS_ECP_C:A:B: C : D :F : G: !H + * END_DEPENDENCIES + */ +''' + expected = ['MBEDTLS_ECP_C', 'A', 'B', 'C', 'D', 'F', 'G', '!H'] + stream = StringIOWrapper('test_suite_ut.function', data) + dependencies = parse_suite_dependencies(stream) + self.assertEqual(dependencies, expected) + + +class ParseFuncDependencies(TestCase): + """ + Test Suite for testing parse_function_dependencies() + """ + + def test_function_dependencies(self): + """ + Test that parse_function_dependencies() correctly parses function + dependencies. + :return: + """ + line = '/* BEGIN_CASE ' \ + 'depends_on:MBEDTLS_ENTROPY_NV_SEED:MBEDTLS_FS_IO */' + expected = ['MBEDTLS_ENTROPY_NV_SEED', 'MBEDTLS_FS_IO'] + dependencies = parse_function_dependencies(line) + self.assertEqual(dependencies, expected) + + def test_no_dependencies(self): + """ + Test that parse_function_dependencies() correctly parses function + dependencies. + :return: + """ + line = '/* BEGIN_CASE */' + dependencies = parse_function_dependencies(line) + self.assertEqual(dependencies, []) + + def test_tolerance(self): + """ + Test that parse_function_dependencies() correctly parses function + dependencies. + :return: + """ + line = '/* BEGIN_CASE depends_on:MBEDTLS_FS_IO: A : !B:C : F*/' + dependencies = parse_function_dependencies(line) + self.assertEqual(dependencies, ['MBEDTLS_FS_IO', 'A', '!B', 'C', 'F']) + + +class ParseFuncSignature(TestCase): + """ + Test Suite for parse_function_arguments(). + """ + + def test_int_and_char_params(self): + """ + Test int and char parameters parsing + :return: + """ + line = 'void entropy_threshold( char * a, int b, int result )' + args, local, arg_dispatch = parse_function_arguments(line) + self.assertEqual(args, ['char*', 'int', 'int']) + self.assertEqual(local, '') + self.assertEqual(arg_dispatch, + ['(char *) params[0]', + '((mbedtls_test_argument_t *) params[1])->sint', + '((mbedtls_test_argument_t *) params[2])->sint']) + + def test_hex_params(self): + """ + Test hex parameters parsing + :return: + """ + line = 'void entropy_threshold( char * a, data_t * h, int result )' + args, local, arg_dispatch = parse_function_arguments(line) + self.assertEqual(args, ['char*', 'hex', 'int']) + self.assertEqual(local, + ' data_t data1 = {(uint8_t *) params[1], ' + '((mbedtls_test_argument_t *) params[2])->len};\n') + self.assertEqual(arg_dispatch, ['(char *) params[0]', + '&data1', + '((mbedtls_test_argument_t *) params[3])->sint']) + + def test_unsupported_arg(self): + """ + Test unsupported argument type + :return: + """ + line = 'void entropy_threshold( char * a, data_t * h, unknown_t result )' + self.assertRaises(ValueError, parse_function_arguments, line) + + def test_empty_params(self): + """ + Test no parameters (nothing between parentheses). + :return: + """ + line = 'void entropy_threshold()' + args, local, arg_dispatch = parse_function_arguments(line) + self.assertEqual(args, []) + self.assertEqual(local, '') + self.assertEqual(arg_dispatch, []) + + def test_blank_params(self): + """ + Test no parameters (space between parentheses). + :return: + """ + line = 'void entropy_threshold( )' + args, local, arg_dispatch = parse_function_arguments(line) + self.assertEqual(args, []) + self.assertEqual(local, '') + self.assertEqual(arg_dispatch, []) + + def test_void_params(self): + """ + Test no parameters (void keyword). + :return: + """ + line = 'void entropy_threshold(void)' + args, local, arg_dispatch = parse_function_arguments(line) + self.assertEqual(args, []) + self.assertEqual(local, '') + self.assertEqual(arg_dispatch, []) + + def test_void_space_params(self): + """ + Test no parameters (void with spaces). + :return: + """ + line = 'void entropy_threshold( void )' + args, local, arg_dispatch = parse_function_arguments(line) + self.assertEqual(args, []) + self.assertEqual(local, '') + self.assertEqual(arg_dispatch, []) + + +class ParseFunctionCode(TestCase): + """ + Test suite for testing parse_function_code() + """ + + def test_no_function(self): + """ + Test no test function found. + :return: + """ + data = ''' +No +test +function +''' + stream = StringIOWrapper('test_suite_ut.function', data) + err_msg = 'file: test_suite_ut.function - Test functions not found!' + self.assertRaisesRegex(GeneratorInputError, err_msg, + parse_function_code, stream, [], []) + + def test_no_end_case_comment(self): + """ + Test missing end case. + :return: + """ + data = ''' +void test_func() +{ +} +''' + stream = StringIOWrapper('test_suite_ut.function', data) + err_msg = r'file: test_suite_ut.function - '\ + 'end case pattern .*? not found!' + self.assertRaisesRegex(GeneratorInputError, err_msg, + parse_function_code, stream, [], []) + + @patch("generate_test_code.parse_function_arguments") + def test_function_called(self, + parse_function_arguments_mock): + """ + Test parse_function_code() + :return: + """ + parse_function_arguments_mock.return_value = ([], '', []) + data = ''' +void test_func() +{ +} +''' + stream = StringIOWrapper('test_suite_ut.function', data) + self.assertRaises(GeneratorInputError, parse_function_code, + stream, [], []) + self.assertTrue(parse_function_arguments_mock.called) + parse_function_arguments_mock.assert_called_with('void test_func()\n') + + @patch("generate_test_code.gen_dispatch") + @patch("generate_test_code.gen_dependencies") + @patch("generate_test_code.gen_function_wrapper") + @patch("generate_test_code.parse_function_arguments") + def test_return(self, parse_function_arguments_mock, + gen_function_wrapper_mock, + gen_dependencies_mock, + gen_dispatch_mock): + """ + Test generated code. + :return: + """ + parse_function_arguments_mock.return_value = ([], '', []) + gen_function_wrapper_mock.return_value = '' + gen_dependencies_mock.side_effect = gen_dependencies + gen_dispatch_mock.side_effect = gen_dispatch + data = ''' +void func() +{ + ba ba black sheep + have you any wool +} +/* END_CASE */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + name, arg, code, dispatch_code = parse_function_code(stream, [], []) + + self.assertTrue(parse_function_arguments_mock.called) + parse_function_arguments_mock.assert_called_with('void func()\n') + gen_function_wrapper_mock.assert_called_with('test_func', '', []) + self.assertEqual(name, 'test_func') + self.assertEqual(arg, []) + expected = '''#line 1 "test_suite_ut.function" + +void test_func(void) +{ + ba ba black sheep + have you any wool +exit: + ; +} +''' + self.assertEqual(code, expected) + self.assertEqual(dispatch_code, "\n test_func_wrapper,\n") + + @patch("generate_test_code.gen_dispatch") + @patch("generate_test_code.gen_dependencies") + @patch("generate_test_code.gen_function_wrapper") + @patch("generate_test_code.parse_function_arguments") + def test_with_exit_label(self, parse_function_arguments_mock, + gen_function_wrapper_mock, + gen_dependencies_mock, + gen_dispatch_mock): + """ + Test when exit label is present. + :return: + """ + parse_function_arguments_mock.return_value = ([], '', []) + gen_function_wrapper_mock.return_value = '' + gen_dependencies_mock.side_effect = gen_dependencies + gen_dispatch_mock.side_effect = gen_dispatch + data = ''' +void func() +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +/* END_CASE */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + _, _, code, _ = parse_function_code(stream, [], []) + + expected = '''#line 1 "test_suite_ut.function" + +void test_func(void) +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +''' + self.assertEqual(code, expected) + + def test_non_void_function(self): + """ + Test invalid signature (non void). + :return: + """ + data = 'int entropy_threshold( char * a, data_t * h, int result )' + err_msg = 'file: test_suite_ut.function - Test functions not found!' + stream = StringIOWrapper('test_suite_ut.function', data) + self.assertRaisesRegex(GeneratorInputError, err_msg, + parse_function_code, stream, [], []) + + @patch("generate_test_code.gen_dispatch") + @patch("generate_test_code.gen_dependencies") + @patch("generate_test_code.gen_function_wrapper") + @patch("generate_test_code.parse_function_arguments") + def test_function_name_on_newline(self, parse_function_arguments_mock, + gen_function_wrapper_mock, + gen_dependencies_mock, + gen_dispatch_mock): + """ + Test with line break before the function name. + :return: + """ + parse_function_arguments_mock.return_value = ([], '', []) + gen_function_wrapper_mock.return_value = '' + gen_dependencies_mock.side_effect = gen_dependencies + gen_dispatch_mock.side_effect = gen_dispatch + data = ''' +void + + +func() +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +/* END_CASE */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + _, _, code, _ = parse_function_code(stream, [], []) + + expected = '''#line 1 "test_suite_ut.function" + +void + + +test_func(void) +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +''' + self.assertEqual(code, expected) + + @patch("generate_test_code.gen_dispatch") + @patch("generate_test_code.gen_dependencies") + @patch("generate_test_code.gen_function_wrapper") + @patch("generate_test_code.parse_function_arguments") + def test_case_starting_with_comment(self, parse_function_arguments_mock, + gen_function_wrapper_mock, + gen_dependencies_mock, + gen_dispatch_mock): + """ + Test with comments before the function signature + :return: + """ + parse_function_arguments_mock.return_value = ([], '', []) + gen_function_wrapper_mock.return_value = '' + gen_dependencies_mock.side_effect = gen_dependencies + gen_dispatch_mock.side_effect = gen_dispatch + data = '''/* comment */ +/* more + * comment */ +// this is\\ +still \\ +a comment +void func() +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +/* END_CASE */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + _, _, code, _ = parse_function_code(stream, [], []) + + expected = '''#line 1 "test_suite_ut.function" + + + + + + +void test_func(void) +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +''' + self.assertEqual(code, expected) + + @patch("generate_test_code.gen_dispatch") + @patch("generate_test_code.gen_dependencies") + @patch("generate_test_code.gen_function_wrapper") + @patch("generate_test_code.parse_function_arguments") + def test_comment_in_prototype(self, parse_function_arguments_mock, + gen_function_wrapper_mock, + gen_dependencies_mock, + gen_dispatch_mock): + """ + Test with comments in the function prototype + :return: + """ + parse_function_arguments_mock.return_value = ([], '', []) + gen_function_wrapper_mock.return_value = '' + gen_dependencies_mock.side_effect = gen_dependencies + gen_dispatch_mock.side_effect = gen_dispatch + data = ''' +void func( int x, // (line \\ + comment) + int y /* lone closing parenthesis) */ ) +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +/* END_CASE */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + _, _, code, _ = parse_function_code(stream, [], []) + + expected = '''#line 1 "test_suite_ut.function" + +void test_func( int x, + + int y ) +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +''' + self.assertEqual(code, expected) + + @patch("generate_test_code.gen_dispatch") + @patch("generate_test_code.gen_dependencies") + @patch("generate_test_code.gen_function_wrapper") + @patch("generate_test_code.parse_function_arguments") + def test_line_comment_in_block_comment(self, parse_function_arguments_mock, + gen_function_wrapper_mock, + gen_dependencies_mock, + gen_dispatch_mock): + """ + Test with line comment in block comment. + :return: + """ + parse_function_arguments_mock.return_value = ([], '', []) + gen_function_wrapper_mock.return_value = '' + gen_dependencies_mock.side_effect = gen_dependencies + gen_dispatch_mock.side_effect = gen_dispatch + data = ''' +void func( int x /* // */ ) +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +/* END_CASE */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + _, _, code, _ = parse_function_code(stream, [], []) + + expected = '''#line 1 "test_suite_ut.function" + +void test_func( int x ) +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +''' + self.assertEqual(code, expected) + + @patch("generate_test_code.gen_dispatch") + @patch("generate_test_code.gen_dependencies") + @patch("generate_test_code.gen_function_wrapper") + @patch("generate_test_code.parse_function_arguments") + def test_block_comment_in_line_comment(self, parse_function_arguments_mock, + gen_function_wrapper_mock, + gen_dependencies_mock, + gen_dispatch_mock): + """ + Test with block comment in line comment. + :return: + """ + parse_function_arguments_mock.return_value = ([], '', []) + gen_function_wrapper_mock.return_value = '' + gen_dependencies_mock.side_effect = gen_dependencies + gen_dispatch_mock.side_effect = gen_dispatch + data = ''' +// /* +void func( int x ) +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +/* END_CASE */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + _, _, code, _ = parse_function_code(stream, [], []) + + expected = '''#line 1 "test_suite_ut.function" + + +void test_func( int x ) +{ + ba ba black sheep + have you any wool +exit: + yes sir yes sir + 3 bags full +} +''' + self.assertEqual(code, expected) + + +class ParseFunction(TestCase): + """ + Test Suite for testing parse_functions() + """ + + @patch("generate_test_code.parse_until_pattern") + def test_begin_header(self, parse_until_pattern_mock): + """ + Test that begin header is checked and parse_until_pattern() is called. + :return: + """ + def stop(*_unused): + """Stop when parse_until_pattern is called.""" + raise Exception + parse_until_pattern_mock.side_effect = stop + data = '''/* BEGIN_HEADER */ +#include "mbedtls/ecp.h" + +#define ECP_PF_UNKNOWN -1 +/* END_HEADER */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + self.assertRaises(Exception, parse_functions, stream) + parse_until_pattern_mock.assert_called_with(stream, END_HEADER_REGEX) + self.assertEqual(stream.line_no, 1) + + @patch("generate_test_code.parse_until_pattern") + def test_begin_helper(self, parse_until_pattern_mock): + """ + Test that begin helper is checked and parse_until_pattern() is called. + :return: + """ + def stop(*_unused): + """Stop when parse_until_pattern is called.""" + raise Exception + parse_until_pattern_mock.side_effect = stop + data = '''/* BEGIN_SUITE_HELPERS */ +void print_hello_world() +{ + printf("Hello World!\n"); +} +/* END_SUITE_HELPERS */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + self.assertRaises(Exception, parse_functions, stream) + parse_until_pattern_mock.assert_called_with(stream, + END_SUITE_HELPERS_REGEX) + self.assertEqual(stream.line_no, 1) + + @patch("generate_test_code.parse_suite_dependencies") + def test_begin_dep(self, parse_suite_dependencies_mock): + """ + Test that begin dep is checked and parse_suite_dependencies() is + called. + :return: + """ + def stop(*_unused): + """Stop when parse_until_pattern is called.""" + raise Exception + parse_suite_dependencies_mock.side_effect = stop + data = '''/* BEGIN_DEPENDENCIES + * depends_on:MBEDTLS_ECP_C + * END_DEPENDENCIES + */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + self.assertRaises(Exception, parse_functions, stream) + parse_suite_dependencies_mock.assert_called_with(stream) + self.assertEqual(stream.line_no, 1) + + @patch("generate_test_code.parse_function_dependencies") + def test_begin_function_dep(self, func_mock): + """ + Test that begin dep is checked and parse_function_dependencies() is + called. + :return: + """ + def stop(*_unused): + """Stop when parse_until_pattern is called.""" + raise Exception + func_mock.side_effect = stop + + dependencies_str = '/* BEGIN_CASE ' \ + 'depends_on:MBEDTLS_ENTROPY_NV_SEED:MBEDTLS_FS_IO */\n' + data = '''%svoid test_func() +{ +} +''' % dependencies_str + stream = StringIOWrapper('test_suite_ut.function', data) + self.assertRaises(Exception, parse_functions, stream) + func_mock.assert_called_with(dependencies_str) + self.assertEqual(stream.line_no, 1) + + @patch("generate_test_code.parse_function_code") + @patch("generate_test_code.parse_function_dependencies") + def test_return(self, func_mock1, func_mock2): + """ + Test that begin case is checked and parse_function_code() is called. + :return: + """ + func_mock1.return_value = [] + in_func_code = '''void test_func() +{ +} +''' + func_dispatch = ''' + test_func_wrapper, +''' + func_mock2.return_value = 'test_func', [],\ + in_func_code, func_dispatch + dependencies_str = '/* BEGIN_CASE ' \ + 'depends_on:MBEDTLS_ENTROPY_NV_SEED:MBEDTLS_FS_IO */\n' + data = '''%svoid test_func() +{ +} +''' % dependencies_str + stream = StringIOWrapper('test_suite_ut.function', data) + suite_dependencies, dispatch_code, func_code, func_info = \ + parse_functions(stream) + func_mock1.assert_called_with(dependencies_str) + func_mock2.assert_called_with(stream, [], []) + self.assertEqual(stream.line_no, 5) + self.assertEqual(suite_dependencies, []) + expected_dispatch_code = '''/* Function Id: 0 */ + + test_func_wrapper, +''' + self.assertEqual(dispatch_code, expected_dispatch_code) + self.assertEqual(func_code, in_func_code) + self.assertEqual(func_info, {'test_func': (0, [])}) + + def test_parsing(self): + """ + Test case parsing. + :return: + """ + data = '''/* BEGIN_HEADER */ +#include "mbedtls/ecp.h" + +#define ECP_PF_UNKNOWN -1 +/* END_HEADER */ + +/* BEGIN_DEPENDENCIES + * depends_on:MBEDTLS_ECP_C + * END_DEPENDENCIES + */ + +/* BEGIN_CASE depends_on:MBEDTLS_ENTROPY_NV_SEED:MBEDTLS_FS_IO */ +void func1() +{ +} +/* END_CASE */ + +/* BEGIN_CASE depends_on:MBEDTLS_ENTROPY_NV_SEED:MBEDTLS_FS_IO */ +void func2() +{ +} +/* END_CASE */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + suite_dependencies, dispatch_code, func_code, func_info = \ + parse_functions(stream) + self.assertEqual(stream.line_no, 23) + self.assertEqual(suite_dependencies, ['MBEDTLS_ECP_C']) + + expected_dispatch_code = '''/* Function Id: 0 */ + +#if defined(MBEDTLS_ECP_C) && defined(MBEDTLS_ENTROPY_NV_SEED) && defined(MBEDTLS_FS_IO) + test_func1_wrapper, +#else + NULL, +#endif +/* Function Id: 1 */ + +#if defined(MBEDTLS_ECP_C) && defined(MBEDTLS_ENTROPY_NV_SEED) && defined(MBEDTLS_FS_IO) + test_func2_wrapper, +#else + NULL, +#endif +''' + self.assertEqual(dispatch_code, expected_dispatch_code) + expected_func_code = '''#if defined(MBEDTLS_ECP_C) +#line 2 "test_suite_ut.function" +#include "mbedtls/ecp.h" + +#define ECP_PF_UNKNOWN -1 +#if defined(MBEDTLS_ENTROPY_NV_SEED) +#if defined(MBEDTLS_FS_IO) +#line 13 "test_suite_ut.function" +void test_func1(void) +{ +exit: + ; +} + +void test_func1_wrapper( void ** params ) +{ + (void)params; + + test_func1( ); +} +#endif /* MBEDTLS_FS_IO */ +#endif /* MBEDTLS_ENTROPY_NV_SEED */ +#if defined(MBEDTLS_ENTROPY_NV_SEED) +#if defined(MBEDTLS_FS_IO) +#line 19 "test_suite_ut.function" +void test_func2(void) +{ +exit: + ; +} + +void test_func2_wrapper( void ** params ) +{ + (void)params; + + test_func2( ); +} +#endif /* MBEDTLS_FS_IO */ +#endif /* MBEDTLS_ENTROPY_NV_SEED */ +#endif /* MBEDTLS_ECP_C */ +''' + self.assertEqual(func_code, expected_func_code) + self.assertEqual(func_info, {'test_func1': (0, []), + 'test_func2': (1, [])}) + + def test_same_function_name(self): + """ + Test name conflict. + :return: + """ + data = '''/* BEGIN_HEADER */ +#include "mbedtls/ecp.h" + +#define ECP_PF_UNKNOWN -1 +/* END_HEADER */ + +/* BEGIN_DEPENDENCIES + * depends_on:MBEDTLS_ECP_C + * END_DEPENDENCIES + */ + +/* BEGIN_CASE depends_on:MBEDTLS_ENTROPY_NV_SEED:MBEDTLS_FS_IO */ +void func() +{ +} +/* END_CASE */ + +/* BEGIN_CASE depends_on:MBEDTLS_ENTROPY_NV_SEED:MBEDTLS_FS_IO */ +void func() +{ +} +/* END_CASE */ +''' + stream = StringIOWrapper('test_suite_ut.function', data) + self.assertRaises(GeneratorInputError, parse_functions, stream) + + +class EscapedSplit(TestCase): + """ + Test suite for testing escaped_split(). + Note: Since escaped_split() output is used to write back to the + intermediate data file. Any escape characters in the input are + retained in the output. + """ + + def test_invalid_input(self): + """ + Test when input split character is not a character. + :return: + """ + self.assertRaises(ValueError, escaped_split, '', 'string') + + def test_empty_string(self): + """ + Test empty string input. + :return: + """ + splits = escaped_split('', ':') + self.assertEqual(splits, []) + + def test_no_escape(self): + """ + Test with no escape character. The behaviour should be same as + str.split() + :return: + """ + test_str = 'yahoo:google' + splits = escaped_split(test_str, ':') + self.assertEqual(splits, test_str.split(':')) + + def test_escaped_input(self): + """ + Test input that has escaped delimiter. + :return: + """ + test_str = r'yahoo\:google:facebook' + splits = escaped_split(test_str, ':') + self.assertEqual(splits, [r'yahoo\:google', 'facebook']) + + def test_escaped_escape(self): + """ + Test input that has escaped delimiter. + :return: + """ + test_str = r'yahoo\\:google:facebook' + splits = escaped_split(test_str, ':') + self.assertEqual(splits, [r'yahoo\\', 'google', 'facebook']) + + def test_all_at_once(self): + """ + Test input that has escaped delimiter. + :return: + """ + test_str = r'yahoo\\:google:facebook\:instagram\\:bbc\\:wikipedia' + splits = escaped_split(test_str, ':') + self.assertEqual(splits, [r'yahoo\\', r'google', + r'facebook\:instagram\\', + r'bbc\\', r'wikipedia']) + + +class ParseTestData(TestCase): + """ + Test suite for parse test data. + """ + + def test_parser(self): + """ + Test that tests are parsed correctly from data file. + :return: + """ + data = """ +Diffie-Hellman full exchange #1 +dhm_do_dhm:10:"23":10:"5" + +Diffie-Hellman full exchange #2 +dhm_do_dhm:10:"93450983094850938450983409623":10:"9345098304850938450983409622" + +Diffie-Hellman full exchange #3 +dhm_do_dhm:10:"9345098382739712938719287391879381271":10:"9345098792137312973297123912791271" + +Diffie-Hellman selftest +dhm_selftest: +""" + stream = StringIOWrapper('test_suite_ut.function', data) + # List of (name, function_name, dependencies, args) + tests = list(parse_test_data(stream)) + test1, test2, test3, test4 = tests + self.assertEqual(test1[0], 3) + self.assertEqual(test1[1], 'Diffie-Hellman full exchange #1') + self.assertEqual(test1[2], 'dhm_do_dhm') + self.assertEqual(test1[3], []) + self.assertEqual(test1[4], ['10', '"23"', '10', '"5"']) + + self.assertEqual(test2[0], 6) + self.assertEqual(test2[1], 'Diffie-Hellman full exchange #2') + self.assertEqual(test2[2], 'dhm_do_dhm') + self.assertEqual(test2[3], []) + self.assertEqual(test2[4], ['10', '"93450983094850938450983409623"', + '10', '"9345098304850938450983409622"']) + + self.assertEqual(test3[0], 9) + self.assertEqual(test3[1], 'Diffie-Hellman full exchange #3') + self.assertEqual(test3[2], 'dhm_do_dhm') + self.assertEqual(test3[3], []) + self.assertEqual(test3[4], ['10', + '"9345098382739712938719287391879381271"', + '10', + '"9345098792137312973297123912791271"']) + + self.assertEqual(test4[0], 12) + self.assertEqual(test4[1], 'Diffie-Hellman selftest') + self.assertEqual(test4[2], 'dhm_selftest') + self.assertEqual(test4[3], []) + self.assertEqual(test4[4], []) + + def test_with_dependencies(self): + """ + Test that tests with dependencies are parsed. + :return: + """ + data = """ +Diffie-Hellman full exchange #1 +depends_on:YAHOO +dhm_do_dhm:10:"23":10:"5" + +Diffie-Hellman full exchange #2 +dhm_do_dhm:10:"93450983094850938450983409623":10:"9345098304850938450983409622" + +""" + stream = StringIOWrapper('test_suite_ut.function', data) + # List of (name, function_name, dependencies, args) + tests = list(parse_test_data(stream)) + test1, test2 = tests + self.assertEqual(test1[0], 4) + self.assertEqual(test1[1], 'Diffie-Hellman full exchange #1') + self.assertEqual(test1[2], 'dhm_do_dhm') + self.assertEqual(test1[3], ['YAHOO']) + self.assertEqual(test1[4], ['10', '"23"', '10', '"5"']) + + self.assertEqual(test2[0], 7) + self.assertEqual(test2[1], 'Diffie-Hellman full exchange #2') + self.assertEqual(test2[2], 'dhm_do_dhm') + self.assertEqual(test2[3], []) + self.assertEqual(test2[4], ['10', '"93450983094850938450983409623"', + '10', '"9345098304850938450983409622"']) + + def test_no_args(self): + """ + Test GeneratorInputError is raised when test function name and + args line is missing. + :return: + """ + data = """ +Diffie-Hellman full exchange #1 +depends_on:YAHOO + + +Diffie-Hellman full exchange #2 +dhm_do_dhm:10:"93450983094850938450983409623":10:"9345098304850938450983409622" + +""" + stream = StringIOWrapper('test_suite_ut.function', data) + err = None + try: + for _, _, _, _, _ in parse_test_data(stream): + pass + except GeneratorInputError as err: + self.assertEqual(type(err), GeneratorInputError) + + def test_incomplete_data(self): + """ + Test GeneratorInputError is raised when test function name + and args line is missing. + :return: + """ + data = """ +Diffie-Hellman full exchange #1 +depends_on:YAHOO +""" + stream = StringIOWrapper('test_suite_ut.function', data) + err = None + try: + for _, _, _, _, _ in parse_test_data(stream): + pass + except GeneratorInputError as err: + self.assertEqual(type(err), GeneratorInputError) + + +class GenDepCheck(TestCase): + """ + Test suite for gen_dep_check(). It is assumed this function is + called with valid inputs. + """ + + def test_gen_dep_check(self): + """ + Test that dependency check code generated correctly. + :return: + """ + expected = """ + case 5: + { +#if defined(YAHOO) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break;""" + out = gen_dep_check(5, 'YAHOO') + self.assertEqual(out, expected) + + def test_not_defined_dependency(self): + """ + Test dependency with !. + :return: + """ + expected = """ + case 5: + { +#if !defined(YAHOO) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break;""" + out = gen_dep_check(5, '!YAHOO') + self.assertEqual(out, expected) + + def test_empty_dependency(self): + """ + Test invalid dependency input. + :return: + """ + self.assertRaises(GeneratorInputError, gen_dep_check, 5, '!') + + def test_negative_dep_id(self): + """ + Test invalid dependency input. + :return: + """ + self.assertRaises(GeneratorInputError, gen_dep_check, -1, 'YAHOO') + + +class GenExpCheck(TestCase): + """ + Test suite for gen_expression_check(). It is assumed this function + is called with valid inputs. + """ + + def test_gen_exp_check(self): + """ + Test that expression check code generated correctly. + :return: + """ + expected = """ + case 5: + { + *out_value = YAHOO; + } + break;""" + out = gen_expression_check(5, 'YAHOO') + self.assertEqual(out, expected) + + def test_invalid_expression(self): + """ + Test invalid expression input. + :return: + """ + self.assertRaises(GeneratorInputError, gen_expression_check, 5, '') + + def test_negative_exp_id(self): + """ + Test invalid expression id. + :return: + """ + self.assertRaises(GeneratorInputError, gen_expression_check, + -1, 'YAHOO') + + +class WriteDependencies(TestCase): + """ + Test suite for testing write_dependencies. + """ + + def test_no_test_dependencies(self): + """ + Test when test dependencies input is empty. + :return: + """ + stream = StringIOWrapper('test_suite_ut.data', '') + unique_dependencies = [] + dep_check_code = write_dependencies(stream, [], unique_dependencies) + self.assertEqual(dep_check_code, '') + self.assertEqual(len(unique_dependencies), 0) + self.assertEqual(stream.getvalue(), '') + + def test_unique_dep_ids(self): + """ + + :return: + """ + stream = StringIOWrapper('test_suite_ut.data', '') + unique_dependencies = [] + dep_check_code = write_dependencies(stream, ['DEP3', 'DEP2', 'DEP1'], + unique_dependencies) + expect_dep_check_code = ''' + case 0: + { +#if defined(DEP3) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break; + case 1: + { +#if defined(DEP2) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break; + case 2: + { +#if defined(DEP1) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break;''' + self.assertEqual(dep_check_code, expect_dep_check_code) + self.assertEqual(len(unique_dependencies), 3) + self.assertEqual(stream.getvalue(), 'depends_on:0:1:2\n') + + def test_dep_id_repeat(self): + """ + + :return: + """ + stream = StringIOWrapper('test_suite_ut.data', '') + unique_dependencies = [] + dep_check_code = '' + dep_check_code += write_dependencies(stream, ['DEP3', 'DEP2'], + unique_dependencies) + dep_check_code += write_dependencies(stream, ['DEP2', 'DEP1'], + unique_dependencies) + dep_check_code += write_dependencies(stream, ['DEP1', 'DEP3'], + unique_dependencies) + expect_dep_check_code = ''' + case 0: + { +#if defined(DEP3) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break; + case 1: + { +#if defined(DEP2) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break; + case 2: + { +#if defined(DEP1) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break;''' + self.assertEqual(dep_check_code, expect_dep_check_code) + self.assertEqual(len(unique_dependencies), 3) + self.assertEqual(stream.getvalue(), + 'depends_on:0:1\ndepends_on:1:2\ndepends_on:2:0\n') + + +class WriteParams(TestCase): + """ + Test Suite for testing write_parameters(). + """ + + def test_no_params(self): + """ + Test with empty test_args + :return: + """ + stream = StringIOWrapper('test_suite_ut.data', '') + unique_expressions = [] + expression_code = write_parameters(stream, [], [], unique_expressions) + self.assertEqual(len(unique_expressions), 0) + self.assertEqual(expression_code, '') + self.assertEqual(stream.getvalue(), '\n') + + def test_no_exp_param(self): + """ + Test when there is no macro or expression in the params. + :return: + """ + stream = StringIOWrapper('test_suite_ut.data', '') + unique_expressions = [] + expression_code = write_parameters(stream, ['"Yahoo"', '"abcdef00"', + '0'], + ['char*', 'hex', 'int'], + unique_expressions) + self.assertEqual(len(unique_expressions), 0) + self.assertEqual(expression_code, '') + self.assertEqual(stream.getvalue(), + ':char*:"Yahoo":hex:"abcdef00":int:0\n') + + def test_hex_format_int_param(self): + """ + Test int parameter in hex format. + :return: + """ + stream = StringIOWrapper('test_suite_ut.data', '') + unique_expressions = [] + expression_code = write_parameters(stream, + ['"Yahoo"', '"abcdef00"', '0xAA'], + ['char*', 'hex', 'int'], + unique_expressions) + self.assertEqual(len(unique_expressions), 0) + self.assertEqual(expression_code, '') + self.assertEqual(stream.getvalue(), + ':char*:"Yahoo":hex:"abcdef00":int:0xAA\n') + + def test_with_exp_param(self): + """ + Test when there is macro or expression in the params. + :return: + """ + stream = StringIOWrapper('test_suite_ut.data', '') + unique_expressions = [] + expression_code = write_parameters(stream, + ['"Yahoo"', '"abcdef00"', '0', + 'MACRO1', 'MACRO2', 'MACRO3'], + ['char*', 'hex', 'int', + 'int', 'int', 'int'], + unique_expressions) + self.assertEqual(len(unique_expressions), 3) + self.assertEqual(unique_expressions, ['MACRO1', 'MACRO2', 'MACRO3']) + expected_expression_code = ''' + case 0: + { + *out_value = MACRO1; + } + break; + case 1: + { + *out_value = MACRO2; + } + break; + case 2: + { + *out_value = MACRO3; + } + break;''' + self.assertEqual(expression_code, expected_expression_code) + self.assertEqual(stream.getvalue(), + ':char*:"Yahoo":hex:"abcdef00":int:0:exp:0:exp:1' + ':exp:2\n') + + def test_with_repeat_calls(self): + """ + Test when write_parameter() is called with same macro or expression. + :return: + """ + stream = StringIOWrapper('test_suite_ut.data', '') + unique_expressions = [] + expression_code = '' + expression_code += write_parameters(stream, + ['"Yahoo"', 'MACRO1', 'MACRO2'], + ['char*', 'int', 'int'], + unique_expressions) + expression_code += write_parameters(stream, + ['"abcdef00"', 'MACRO2', 'MACRO3'], + ['hex', 'int', 'int'], + unique_expressions) + expression_code += write_parameters(stream, + ['0', 'MACRO3', 'MACRO1'], + ['int', 'int', 'int'], + unique_expressions) + self.assertEqual(len(unique_expressions), 3) + self.assertEqual(unique_expressions, ['MACRO1', 'MACRO2', 'MACRO3']) + expected_expression_code = ''' + case 0: + { + *out_value = MACRO1; + } + break; + case 1: + { + *out_value = MACRO2; + } + break; + case 2: + { + *out_value = MACRO3; + } + break;''' + self.assertEqual(expression_code, expected_expression_code) + expected_data_file = ''':char*:"Yahoo":exp:0:exp:1 +:hex:"abcdef00":exp:1:exp:2 +:int:0:exp:2:exp:0 +''' + self.assertEqual(stream.getvalue(), expected_data_file) + + +class GenTestSuiteDependenciesChecks(TestCase): + """ + Test suite for testing gen_suite_dep_checks() + """ + def test_empty_suite_dependencies(self): + """ + Test with empty suite_dependencies list. + + :return: + """ + dep_check_code, expression_code = \ + gen_suite_dep_checks([], 'DEP_CHECK_CODE', 'EXPRESSION_CODE') + self.assertEqual(dep_check_code, 'DEP_CHECK_CODE') + self.assertEqual(expression_code, 'EXPRESSION_CODE') + + def test_suite_dependencies(self): + """ + Test with suite_dependencies list. + + :return: + """ + dep_check_code, expression_code = \ + gen_suite_dep_checks(['SUITE_DEP'], 'DEP_CHECK_CODE', + 'EXPRESSION_CODE') + expected_dep_check_code = ''' +#if defined(SUITE_DEP) +DEP_CHECK_CODE +#endif +''' + expected_expression_code = ''' +#if defined(SUITE_DEP) +EXPRESSION_CODE +#endif +''' + self.assertEqual(dep_check_code, expected_dep_check_code) + self.assertEqual(expression_code, expected_expression_code) + + def test_no_dep_no_exp(self): + """ + Test when there are no dependency and expression code. + :return: + """ + dep_check_code, expression_code = gen_suite_dep_checks([], '', '') + self.assertEqual(dep_check_code, '') + self.assertEqual(expression_code, '') + + +class GenFromTestData(TestCase): + """ + Test suite for gen_from_test_data() + """ + + @staticmethod + @patch("generate_test_code.write_dependencies") + @patch("generate_test_code.write_parameters") + @patch("generate_test_code.gen_suite_dep_checks") + def test_intermediate_data_file(func_mock1, + write_parameters_mock, + write_dependencies_mock): + """ + Test that intermediate data file is written with expected data. + :return: + """ + data = ''' +My test +depends_on:DEP1 +func1:0 +''' + data_f = StringIOWrapper('test_suite_ut.data', data) + out_data_f = StringIOWrapper('test_suite_ut.datax', '') + func_info = {'test_func1': (1, ('int',))} + suite_dependencies = [] + write_parameters_mock.side_effect = write_parameters + write_dependencies_mock.side_effect = write_dependencies + func_mock1.side_effect = gen_suite_dep_checks + gen_from_test_data(data_f, out_data_f, func_info, suite_dependencies) + write_dependencies_mock.assert_called_with(out_data_f, + ['DEP1'], ['DEP1']) + write_parameters_mock.assert_called_with(out_data_f, ['0'], + ('int',), []) + expected_dep_check_code = ''' + case 0: + { +#if defined(DEP1) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break;''' + func_mock1.assert_called_with( + suite_dependencies, expected_dep_check_code, '') + + def test_function_not_found(self): + """ + Test that AssertError is raised when function info in not found. + :return: + """ + data = ''' +My test +depends_on:DEP1 +func1:0 +''' + data_f = StringIOWrapper('test_suite_ut.data', data) + out_data_f = StringIOWrapper('test_suite_ut.datax', '') + func_info = {'test_func2': (1, ('int',))} + suite_dependencies = [] + self.assertRaises(GeneratorInputError, gen_from_test_data, + data_f, out_data_f, func_info, suite_dependencies) + + def test_different_func_args(self): + """ + Test that AssertError is raised when no. of parameters and + function args differ. + :return: + """ + data = ''' +My test +depends_on:DEP1 +func1:0 +''' + data_f = StringIOWrapper('test_suite_ut.data', data) + out_data_f = StringIOWrapper('test_suite_ut.datax', '') + func_info = {'test_func2': (1, ('int', 'hex'))} + suite_dependencies = [] + self.assertRaises(GeneratorInputError, gen_from_test_data, data_f, + out_data_f, func_info, suite_dependencies) + + def test_output(self): + """ + Test that intermediate data file is written with expected data. + :return: + """ + data = ''' +My test 1 +depends_on:DEP1 +func1:0:0xfa:MACRO1:MACRO2 + +My test 2 +depends_on:DEP1:DEP2 +func2:"yahoo":88:MACRO1 +''' + data_f = StringIOWrapper('test_suite_ut.data', data) + out_data_f = StringIOWrapper('test_suite_ut.datax', '') + func_info = {'test_func1': (0, ('int', 'int', 'int', 'int')), + 'test_func2': (1, ('char*', 'int', 'int'))} + suite_dependencies = [] + dep_check_code, expression_code = \ + gen_from_test_data(data_f, out_data_f, func_info, + suite_dependencies) + expected_dep_check_code = ''' + case 0: + { +#if defined(DEP1) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break; + case 1: + { +#if defined(DEP2) + ret = DEPENDENCY_SUPPORTED; +#else + ret = DEPENDENCY_NOT_SUPPORTED; +#endif + } + break;''' + expected_data = '''My test 1 +depends_on:0 +0:int:0:int:0xfa:exp:0:exp:1 + +My test 2 +depends_on:0:1 +1:char*:"yahoo":int:88:exp:0 + +''' + expected_expression_code = ''' + case 0: + { + *out_value = MACRO1; + } + break; + case 1: + { + *out_value = MACRO2; + } + break;''' + self.assertEqual(dep_check_code, expected_dep_check_code) + self.assertEqual(out_data_f.getvalue(), expected_data) + self.assertEqual(expression_code, expected_expression_code) + + +if __name__ == '__main__': + unittest_main()