blob: e15261500cb1d3a163be4316cd25f370a44bb819 [file] [log] [blame]
Werner Lewis545911f2022-07-08 13:54:57 +01001#!/usr/bin/env python3
2"""Generate test data for bignum functions.
3
4With no arguments, generate all test data. With non-option arguments,
5generate only the specified files.
6"""
7
8# Copyright The Mbed TLS Contributors
9# SPDX-License-Identifier: Apache-2.0
10#
11# Licensed under the Apache License, Version 2.0 (the "License"); you may
12# not use this file except in compliance with the License.
13# You may obtain a copy of the License at
14#
15# http://www.apache.org/licenses/LICENSE-2.0
16#
17# Unless required by applicable law or agreed to in writing, software
18# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
19# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20# See the License for the specific language governing permissions and
21# limitations under the License.
22
23import argparse
24import itertools
25import os
26import posixpath
27import re
28import sys
Werner Lewisd76c5ed2022-07-20 14:13:44 +010029from typing import Iterable, Iterator, List, Optional, Tuple, TypeVar
Werner Lewis545911f2022-07-08 13:54:57 +010030
31import scripts_path # pylint: disable=unused-import
32from mbedtls_dev import build_tree
33from mbedtls_dev import test_case
34
35T = TypeVar('T') #pylint: disable=invalid-name
36
37def hex_to_int(val):
38 return int(val, 16) if val else 0
39
40def quote_str(val):
41 return "\"{}\"".format(val)
42
43
44class BaseTarget:
45 """Base target for test case generation.
46
47 Attributes:
48 count: Counter for test class.
49 desc: Short description of test case.
50 func: Function which the class generates tests for.
51 gen_file: File to write generated tests to.
52 title: Description of the test function/purpose.
53 """
54 count = 0
Werner Lewisd76c5ed2022-07-20 14:13:44 +010055 desc = ""
56 func = ""
Werner Lewis545911f2022-07-08 13:54:57 +010057 gen_file = ""
Werner Lewisd76c5ed2022-07-20 14:13:44 +010058 title = ""
Werner Lewis545911f2022-07-08 13:54:57 +010059
60 def __init__(self) -> None:
61 type(self).count += 1
62
63 @property
Werner Lewisd76c5ed2022-07-20 14:13:44 +010064 def args(self) -> List[str]:
Werner Lewis545911f2022-07-08 13:54:57 +010065 """Create list of arguments for test case."""
66 return []
67
68 @property
69 def description(self) -> str:
70 """Create a numbered test description."""
71 return "{} #{} {}".format(self.title, self.count, self.desc)
72
73 def create_test_case(self) -> test_case.TestCase:
74 """Generate test case from the current object."""
75 tc = test_case.TestCase()
76 tc.set_description(self.description)
77 tc.set_function(self.func)
78 tc.set_arguments(self.args)
79
80 return tc
81
82 @classmethod
83 def generate_tests(cls):
84 """Generate test cases for the target subclasses."""
Werner Lewis1c413bd2022-07-20 13:35:22 +010085 for subclass in sorted(cls.__subclasses__(), key=lambda c: c.__name__):
Werner Lewis545911f2022-07-08 13:54:57 +010086 yield from subclass.generate_tests()
87
88
89class BignumTarget(BaseTarget):
90 """Target for bignum (mpi) test case generation."""
91 gen_file = 'test_suite_mpi.generated'
92
93
94class BignumOperation(BignumTarget):
95 """Common features for test cases covering bignum operations.
96
97 Attributes:
98 symb: Symbol used for operation in description.
99 input_vals: List of values used to generate test case args.
100 input_cases: List of tuples containing test case inputs. This
101 can be used to implement specific pairs of inputs.
102 """
103 symb = ""
104 input_vals = [
105 "", "0", "7b", "-7b",
106 "0000000000000000123", "-0000000000000000123",
107 "1230000000000000000", "-1230000000000000000"
Werner Lewisd76c5ed2022-07-20 14:13:44 +0100108 ] # type: List[str]
109 input_cases = [] # type: List[Tuple[str, ...]]
Werner Lewis545911f2022-07-08 13:54:57 +0100110
111 def __init__(self, val_l: str, val_r: str) -> None:
112 super().__init__()
113
114 self.arg_l = val_l
115 self.arg_r = val_r
116 self.int_l = hex_to_int(val_l)
117 self.int_r = hex_to_int(val_r)
118
119 @property
120 def args(self):
121 return [quote_str(self.arg_l), quote_str(self.arg_r), self.result]
122
123 @property
124 def description(self):
125 desc = self.desc if self.desc else "{} {} {}".format(
126 self.val_desc(self.arg_l),
127 self.symb,
128 self.val_desc(self.arg_r)
129 )
130 return "{} #{} {}".format(self.title, self.count, desc)
131
132 @property
133 def result(self) -> Optional[str]:
134 return None
135
136 @staticmethod
137 def val_desc(val) -> str:
138 """Generate description of the argument val."""
139 if val == "":
140 return "0 (null)"
141 if val == "0":
142 return "0 (1 limb)"
143
144 if val[0] == "-":
145 tmp = "negative"
146 val = val[1:]
147 else:
148 tmp = "positive"
149 if val[0] == "0":
150 tmp += " with leading zero limb"
151 elif len(val) > 10:
152 tmp = "large " + tmp
153 return tmp
154
155 @classmethod
156 def get_value_pairs(cls) -> Iterator[Tuple[str, ...]]:
157 """Generate value pairs."""
Werner Lewis1bdee222022-07-20 13:35:53 +0100158 for pair in list(
159 itertools.combinations(cls.input_vals, 2)
160 ) + cls.input_cases:
Werner Lewis545911f2022-07-08 13:54:57 +0100161 yield pair
162
163 @classmethod
164 def generate_tests(cls) -> Iterator[test_case.TestCase]:
165 if cls.func is not None:
166 # Generate tests for the current class
167 for l_value, r_value in cls.get_value_pairs():
168 cur_op = cls(l_value, r_value)
169 yield cur_op.create_test_case()
170 # Once current class completed, check descendants
171 yield from super().generate_tests()
172
173
174class BignumCmp(BignumOperation):
175 """Target for bignum comparison test cases."""
176 count = 0
177 func = "mbedtls_mpi_cmp_mpi"
178 title = "MPI compare"
179 input_cases = [
180 ("-2", "-3"),
181 ("-2", "-2"),
182 ("2b4", "2b5"),
183 ("2b5", "2b6")
184 ]
185
186 def __init__(self, val_l, val_r):
187 super().__init__(val_l, val_r)
188 self._result = (self.int_l > self.int_r) - (self.int_l < self.int_r)
189 self.symb = ["<", "==", ">"][self._result + 1]
190
191 @property
192 def result(self):
193 return str(self._result)
194
195
Werner Lewis423f99b2022-07-18 15:49:43 +0100196class BignumCmpAbs(BignumCmp):
197 """Target for abs comparison variant."""
198 count = 0
199 func = "mbedtls_mpi_cmp_abs"
200 title = "MPI compare (abs)"
201
202 def __init__(self, val_l, val_r):
203 super().__init__(val_l.strip("-"), val_r.strip("-"))
204
205
Werner Lewis5c1173b2022-07-18 17:22:58 +0100206class BignumAdd(BignumOperation):
207 """Target for bignum addition test cases."""
208 count = 0
209 func = "mbedtls_mpi_add_mpi"
210 title = "MPI add"
211 input_cases = list(itertools.combinations(
212 [
213 "1c67967269c6", "9cde3",
214 "-1c67967269c6", "-9cde3",
215 ], 2
216 ))
217
218 def __init__(self, val_l, val_r):
219 super().__init__(val_l, val_r)
220 self.symb = "+"
221
222 @property
223 def result(self):
224 return quote_str(hex(self.int_l + self.int_r).replace("0x", "", 1))
225
226
Werner Lewis545911f2022-07-08 13:54:57 +0100227class TestGenerator:
228 """Generate test data."""
229
230 def __init__(self, options) -> None:
231 self.test_suite_directory = self.get_option(options, 'directory',
232 'tests/suites')
233
234 @staticmethod
235 def get_option(options, name: str, default: T) -> T:
236 value = getattr(options, name, None)
237 return default if value is None else value
238
239 def filename_for(self, basename: str) -> str:
240 """The location of the data file with the specified base name."""
241 return posixpath.join(self.test_suite_directory, basename + '.data')
242
243 def write_test_data_file(self, basename: str,
244 test_cases: Iterable[test_case.TestCase]) -> None:
245 """Write the test cases to a .data file.
246
247 The output file is ``basename + '.data'`` in the test suite directory.
248 """
249 filename = self.filename_for(basename)
250 test_case.write_data_file(filename, test_cases)
251
252 # Note that targets whose names contain 'test_format' have their content
253 # validated by `abi_check.py`.
254 TARGETS = {
255 subclass.gen_file: subclass.generate_tests for subclass in
256 BaseTarget.__subclasses__()
257 }
258
259 def generate_target(self, name: str) -> None:
260 test_cases = self.TARGETS[name]()
261 self.write_test_data_file(name, test_cases)
262
263def main(args):
264 """Command line entry point."""
265 parser = argparse.ArgumentParser(description=__doc__)
266 parser.add_argument('--list', action='store_true',
267 help='List available targets and exit')
268 parser.add_argument('--list-for-cmake', action='store_true',
269 help='Print \';\'-separated list of available targets and exit')
270 parser.add_argument('--directory', metavar='DIR',
271 help='Output directory (default: tests/suites)')
272 parser.add_argument('targets', nargs='*', metavar='TARGET',
273 help='Target file to generate (default: all; "-": none)')
274 options = parser.parse_args(args)
275 build_tree.chdir_to_root()
276 generator = TestGenerator(options)
277 if options.list:
278 for name in sorted(generator.TARGETS):
279 print(generator.filename_for(name))
280 return
281 # List in a cmake list format (i.e. ';'-separated)
282 if options.list_for_cmake:
283 print(';'.join(generator.filename_for(name)
284 for name in sorted(generator.TARGETS)), end='')
285 return
286 if options.targets:
287 # Allow "-" as a special case so you can run
288 # ``generate_bignum_tests.py - $targets`` and it works uniformly whether
289 # ``$targets`` is empty or not.
290 options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
291 for target in options.targets
292 if target != '-']
293 else:
294 options.targets = sorted(generator.TARGETS)
295 for target in options.targets:
296 generator.generate_target(target)
297
298if __name__ == '__main__':
299 main(sys.argv[1:])