blob: 32361ee9b018dd2f16c87a681e6c45488f91ccd0 [file] [log] [blame]
Gilles Peskinebd5147c2022-09-16 22:02:37 +02001"""Common code for test data generation.
2
3This module defines classes that are of general use to automatically
4generate .data files for unit tests, as well as a main function.
Werner Lewisdcad1e92022-08-24 11:30:03 +01005
6These are used both by generate_psa_tests.py and generate_bignum_tests.py.
7"""
8
9# Copyright The Mbed TLS Contributors
Dave Rodgman7ff79652023-11-03 12:04:52 +000010# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
Werner Lewisdcad1e92022-08-24 11:30:03 +010011#
Werner Lewisdcad1e92022-08-24 11:30:03 +010012
13import argparse
14import os
15import posixpath
16import re
Werner Lewis008d90d2022-08-23 16:07:37 +010017
Werner Lewis47e37b32022-08-24 12:18:25 +010018from abc import ABCMeta, abstractmethod
Werner Lewisc34d0372022-08-24 12:42:00 +010019from typing import Callable, Dict, Iterable, Iterator, List, Type, TypeVar
Werner Lewisdcad1e92022-08-24 11:30:03 +010020
Gilles Peskine239765a2022-09-16 22:35:18 +020021from . import build_tree
22from . import test_case
Werner Lewisdcad1e92022-08-24 11:30:03 +010023
24T = TypeVar('T') #pylint: disable=invalid-name
25
26
Werner Lewis47e37b32022-08-24 12:18:25 +010027class BaseTarget(metaclass=ABCMeta):
Werner Lewisdcad1e92022-08-24 11:30:03 +010028 """Base target for test case generation.
29
Werner Lewis64334d92022-09-14 16:26:54 +010030 Child classes of this class represent an output file, and can be referred
31 to as file targets. These indicate where test cases will be written to for
32 all subclasses of the file target, which is set by `target_basename`.
Werner Lewisb03420f2022-08-25 12:29:46 +010033
Werner Lewisdcad1e92022-08-24 11:30:03 +010034 Attributes:
Werner Lewis70d3f3d2022-08-23 14:21:53 +010035 count: Counter for test cases from this class.
36 case_description: Short description of the test case. This may be
37 automatically generated using the class, or manually set.
Werner Lewis486b3412022-08-31 17:01:38 +010038 dependencies: A list of dependencies required for the test case.
Werner Lewis113ddd02022-09-14 13:02:40 +010039 show_test_count: Toggle for inclusion of `count` in the test description.
Werner Lewis70d3f3d2022-08-23 14:21:53 +010040 target_basename: Basename of file to write generated tests to. This
41 should be specified in a child class of BaseTarget.
42 test_function: Test function which the class generates cases for.
43 test_name: A common name or description of the test function. This can
Werner Lewisb03420f2022-08-25 12:29:46 +010044 be `test_function`, a clearer equivalent, or a short summary of the
45 test function's purpose.
Werner Lewisdcad1e92022-08-24 11:30:03 +010046 """
47 count = 0
Werner Lewis70d3f3d2022-08-23 14:21:53 +010048 case_description = ""
Werner Lewis6cc5e5f2022-08-31 17:16:44 +010049 dependencies = [] # type: List[str]
Werner Lewis113ddd02022-09-14 13:02:40 +010050 show_test_count = True
Werner Lewis70d3f3d2022-08-23 14:21:53 +010051 target_basename = ""
52 test_function = ""
53 test_name = ""
Werner Lewisdcad1e92022-08-24 11:30:03 +010054
Werner Lewiscace1aa2022-08-24 17:04:07 +010055 def __new__(cls, *args, **kwargs):
Werner Lewis486d2582022-08-24 18:09:10 +010056 # pylint: disable=unused-argument
Werner Lewiscace1aa2022-08-24 17:04:07 +010057 cls.count += 1
58 return super().__new__(cls)
Werner Lewisdcad1e92022-08-24 11:30:03 +010059
Werner Lewis008d90d2022-08-23 16:07:37 +010060 @abstractmethod
Werner Lewis70d3f3d2022-08-23 14:21:53 +010061 def arguments(self) -> List[str]:
Werner Lewis008d90d2022-08-23 16:07:37 +010062 """Get the list of arguments for the test case.
63
64 Override this method to provide the list of arguments required for
Werner Lewisb03420f2022-08-25 12:29:46 +010065 the `test_function`.
Werner Lewis008d90d2022-08-23 16:07:37 +010066
67 Returns:
68 List of arguments required for the test function.
69 """
Werner Lewisd77d33d2022-08-25 09:56:51 +010070 raise NotImplementedError
Werner Lewisdcad1e92022-08-24 11:30:03 +010071
Werner Lewisdcad1e92022-08-24 11:30:03 +010072 def description(self) -> str:
Werner Lewisb03420f2022-08-25 12:29:46 +010073 """Create a test case description.
Werner Lewis008d90d2022-08-23 16:07:37 +010074
75 Creates a description of the test case, including a name for the test
Werner Lewis113ddd02022-09-14 13:02:40 +010076 function, an optional case count, and a description of the specific
77 test case. This should inform a reader what is being tested, and
78 provide context for the test case.
Werner Lewis008d90d2022-08-23 16:07:37 +010079
80 Returns:
81 Description for the test case.
82 """
Werner Lewis113ddd02022-09-14 13:02:40 +010083 if self.show_test_count:
84 return "{} #{} {}".format(
85 self.test_name, self.count, self.case_description
86 ).strip()
87 else:
88 return "{} {}".format(self.test_name, self.case_description).strip()
Werner Lewisdcad1e92022-08-24 11:30:03 +010089
Werner Lewis008d90d2022-08-23 16:07:37 +010090
Werner Lewisdcad1e92022-08-24 11:30:03 +010091 def create_test_case(self) -> test_case.TestCase:
Werner Lewisb03420f2022-08-25 12:29:46 +010092 """Generate TestCase from the instance."""
Werner Lewisdcad1e92022-08-24 11:30:03 +010093 tc = test_case.TestCase()
Werner Lewis70d3f3d2022-08-23 14:21:53 +010094 tc.set_description(self.description())
95 tc.set_function(self.test_function)
96 tc.set_arguments(self.arguments())
Werner Lewis486b3412022-08-31 17:01:38 +010097 tc.set_dependencies(self.dependencies)
Werner Lewisdcad1e92022-08-24 11:30:03 +010098
99 return tc
100
101 @classmethod
Werner Lewisc34d0372022-08-24 12:42:00 +0100102 @abstractmethod
103 def generate_function_tests(cls) -> Iterator[test_case.TestCase]:
Werner Lewisb03420f2022-08-25 12:29:46 +0100104 """Generate test cases for the class test function.
Werner Lewis008d90d2022-08-23 16:07:37 +0100105
Werner Lewisc34d0372022-08-24 12:42:00 +0100106 This will be called in classes where `test_function` is set.
107 Implementations should yield TestCase objects, by creating instances
108 of the class with appropriate input data, and then calling
109 `create_test_case()` on each.
Werner Lewis008d90d2022-08-23 16:07:37 +0100110 """
Werner Lewisd77d33d2022-08-25 09:56:51 +0100111 raise NotImplementedError
Werner Lewisc34d0372022-08-24 12:42:00 +0100112
113 @classmethod
114 def generate_tests(cls) -> Iterator[test_case.TestCase]:
115 """Generate test cases for the class and its subclasses.
116
117 In classes with `test_function` set, `generate_function_tests()` is
Werner Lewis2b0f7d82022-08-25 16:27:05 +0100118 called to generate test cases first.
Werner Lewisc34d0372022-08-24 12:42:00 +0100119
Werner Lewisb03420f2022-08-25 12:29:46 +0100120 In all classes, this method will iterate over its subclasses, and
121 yield from `generate_tests()` in each. Calling this method on a class X
122 will yield test cases from all classes derived from X.
Werner Lewisc34d0372022-08-24 12:42:00 +0100123 """
124 if cls.test_function:
125 yield from cls.generate_function_tests()
Werner Lewisdcad1e92022-08-24 11:30:03 +0100126 for subclass in sorted(cls.__subclasses__(), key=lambda c: c.__name__):
127 yield from subclass.generate_tests()
128
129
130class TestGenerator:
Werner Lewisf5182762022-09-14 12:59:32 +0100131 """Generate test cases and write to data files."""
Gilles Peskine48815402022-10-14 15:01:52 +0200132 def __init__(self, _options) -> None:
133 self.test_suite_directory = 'tests/suites'
Werner Lewisf5182762022-09-14 12:59:32 +0100134 # Update `targets` with an entry for each child class of BaseTarget.
135 # Each entry represents a file generated by the BaseTarget framework,
136 # and enables generating the .data files using the CLI.
Werner Lewis0d07e862022-09-02 11:56:34 +0100137 self.targets.update({
138 subclass.target_basename: subclass.generate_tests
139 for subclass in BaseTarget.__subclasses__()
140 })
Werner Lewisdcad1e92022-08-24 11:30:03 +0100141
Werner Lewisdcad1e92022-08-24 11:30:03 +0100142 def filename_for(self, basename: str) -> str:
143 """The location of the data file with the specified base name."""
144 return posixpath.join(self.test_suite_directory, basename + '.data')
145
146 def write_test_data_file(self, basename: str,
147 test_cases: Iterable[test_case.TestCase]) -> None:
148 """Write the test cases to a .data file.
149
150 The output file is ``basename + '.data'`` in the test suite directory.
151 """
152 filename = self.filename_for(basename)
153 test_case.write_data_file(filename, test_cases)
154
155 # Note that targets whose names contain 'test_format' have their content
156 # validated by `abi_check.py`.
Werner Lewis0d07e862022-09-02 11:56:34 +0100157 targets = {} # type: Dict[str, Callable[..., Iterable[test_case.TestCase]]]
Werner Lewisdcad1e92022-08-24 11:30:03 +0100158
159 def generate_target(self, name: str, *target_args) -> None:
160 """Generate cases and write to data file for a target.
161
162 For target callables which require arguments, override this function
163 and pass these arguments using super() (see PSATestGenerator).
164 """
Werner Lewis0d07e862022-09-02 11:56:34 +0100165 test_cases = self.targets[name](*target_args)
Werner Lewisdcad1e92022-08-24 11:30:03 +0100166 self.write_test_data_file(name, test_cases)
167
Werner Lewis4ed94a42022-09-16 17:03:54 +0100168def main(args, description: str, generator_class: Type[TestGenerator] = TestGenerator):
Werner Lewisdcad1e92022-08-24 11:30:03 +0100169 """Command line entry point."""
Werner Lewis4ed94a42022-09-16 17:03:54 +0100170 parser = argparse.ArgumentParser(description=description)
Werner Lewisdcad1e92022-08-24 11:30:03 +0100171 parser.add_argument('--list', action='store_true',
172 help='List available targets and exit')
173 parser.add_argument('targets', nargs='*', metavar='TARGET',
174 help='Target file to generate (default: all; "-": none)')
Gilles Peskinef8d031f2022-10-14 15:21:49 +0200175
176 # Change to the mbedtls root, to keep things simple.
177 # Note that if any command line options refer to paths, they need to
178 # be adjusted first.
179 build_tree.chdir_to_root()
180
Werner Lewisdcad1e92022-08-24 11:30:03 +0100181 options = parser.parse_args(args)
182 generator = generator_class(options)
183 if options.list:
Werner Lewis0d07e862022-09-02 11:56:34 +0100184 for name in sorted(generator.targets):
Werner Lewisdcad1e92022-08-24 11:30:03 +0100185 print(generator.filename_for(name))
186 return
Werner Lewis0d07e862022-09-02 11:56:34 +0100187 if options.targets:
188 # Allow "-" as a special case so you can run
189 # ``generate_xxx_tests.py - $targets`` and it works uniformly whether
190 # ``$targets`` is empty or not.
191 options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
Werner Lewise53be352022-09-02 12:57:37 +0100192 for target in options.targets
193 if target != '-']
Werner Lewis0d07e862022-09-02 11:56:34 +0100194 else:
195 options.targets = sorted(generator.targets)
Werner Lewisdcad1e92022-08-24 11:30:03 +0100196 for target in options.targets:
197 generator.generate_target(target)