blob: 21a2e8d254eef804dc9498c718f8bb8a0a829bc3 [file] [log] [blame]
Gilles Peskine24827022018-09-25 18:49:23 +02001#!/usr/bin/env python3
Gilles Peskinea3b93ff2019-06-03 11:23:56 +02002"""Test the program psa_constant_names.
Gilles Peskine24827022018-09-25 18:49:23 +02003Gather constant names from header files and test cases. Compile a C program
4to print out their numerical values, feed these numerical values to
5psa_constant_names, and check that the output is the original name.
6Return 0 if all test cases pass, 1 if the output was not always as expected,
Gilles Peskinea3b93ff2019-06-03 11:23:56 +02007or 1 (with a Python backtrace) if there was an operational error.
8"""
Gilles Peskine24827022018-09-25 18:49:23 +02009
Bence Szépkúti1e148272020-08-07 13:07:28 +020010# Copyright The Mbed TLS Contributors
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020011# SPDX-License-Identifier: Apache-2.0
12#
13# Licensed under the Apache License, Version 2.0 (the "License"); you may
14# not use this file except in compliance with the License.
15# You may obtain a copy of the License at
16#
17# http://www.apache.org/licenses/LICENSE-2.0
18#
19# Unless required by applicable law or agreed to in writing, software
20# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
21# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22# See the License for the specific language governing permissions and
23# limitations under the License.
Bence Szépkúti700ee442020-05-26 00:33:31 +020024
Gilles Peskine24827022018-09-25 18:49:23 +020025import argparse
Gilles Peskinea5000f12019-11-21 17:51:11 +010026from collections import namedtuple
Gilles Peskine24827022018-09-25 18:49:23 +020027import itertools
28import os
Gilles Peskine24827022018-09-25 18:49:23 +020029import re
30import subprocess
31import sys
Gilles Peskine10ab2672021-03-10 00:59:53 +010032from typing import Dict, Iterable, Iterator, List, Set
Gilles Peskine2adebc82020-12-11 00:30:53 +010033
34import scripts_path # pylint: disable=unused-import
35from mbedtls_dev import c_build_helper
Gilles Peskine24827022018-09-25 18:49:23 +020036
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +010037class PSAMacroEnumerator:
38 """Information about constructors of various PSA Crypto types.
39
40 This includes macro names as well as information about their arguments
41 when applicable.
42
43 This class only provides ways to enumerate expressions that evaluate to
44 values of the covered types. Derived classes are expected to populate
45 the set of known constructors of each kind, as well as populate
46 `self.arguments_for` for arguments that are not of a kind that is
47 enumerated here.
48 """
49
Gilles Peskine10ab2672021-03-10 00:59:53 +010050 def __init__(self) -> None:
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +010051 """Set up an empty set of known constructor macros.
52 """
Gilles Peskine10ab2672021-03-10 00:59:53 +010053 self.statuses = set() #type: Set[str]
54 self.algorithms = set() #type: Set[str]
55 self.ecc_curves = set() #type: Set[str]
56 self.dh_groups = set() #type: Set[str]
57 self.key_types = set() #type: Set[str]
58 self.key_usage_flags = set() #type: Set[str]
59 self.hash_algorithms = set() #type: Set[str]
60 self.mac_algorithms = set() #type: Set[str]
61 self.ka_algorithms = set() #type: Set[str]
62 self.kdf_algorithms = set() #type: Set[str]
63 self.aead_algorithms = set() #type: Set[str]
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +010064 # macro name -> list of argument names
Gilles Peskine10ab2672021-03-10 00:59:53 +010065 self.argspecs = {} #type: Dict[str, List[str]]
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +010066 # argument name -> list of values
67 self.arguments_for = {
68 'mac_length': [],
69 'min_mac_length': [],
70 'tag_length': [],
71 'min_tag_length': [],
Gilles Peskine10ab2672021-03-10 00:59:53 +010072 } #type: Dict[str, List[str]]
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +010073
Gilles Peskine10ab2672021-03-10 00:59:53 +010074 def gather_arguments(self) -> None:
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +010075 """Populate the list of values for macro arguments.
76
77 Call this after parsing all the inputs.
78 """
79 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
80 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
81 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
82 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
83 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
84 self.arguments_for['curve'] = sorted(self.ecc_curves)
85 self.arguments_for['group'] = sorted(self.dh_groups)
86
87 @staticmethod
Gilles Peskine10ab2672021-03-10 00:59:53 +010088 def _format_arguments(name: str, arguments: Iterable[str]) -> str:
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +010089 """Format a macro call with arguments.."""
90 return name + '(' + ', '.join(arguments) + ')'
91
92 _argument_split_re = re.compile(r' *, *')
93 @classmethod
Gilles Peskine10ab2672021-03-10 00:59:53 +010094 def _argument_split(cls, arguments: str) -> List[str]:
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +010095 return re.split(cls._argument_split_re, arguments)
96
Gilles Peskine10ab2672021-03-10 00:59:53 +010097 def distribute_arguments(self, name: str) -> Iterator[str]:
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +010098 """Generate macro calls with each tested argument set.
99
100 If name is a macro without arguments, just yield "name".
101 If name is a macro with arguments, yield a series of
102 "name(arg1,...,argN)" where each argument takes each possible
103 value at least once.
104 """
105 try:
106 if name not in self.argspecs:
107 yield name
108 return
109 argspec = self.argspecs[name]
110 if argspec == []:
111 yield name + '()'
112 return
113 argument_lists = [self.arguments_for[arg] for arg in argspec]
114 arguments = [values[0] for values in argument_lists]
115 yield self._format_arguments(name, arguments)
116 # Dear Pylint, enumerate won't work here since we're modifying
117 # the array.
118 # pylint: disable=consider-using-enumerate
119 for i in range(len(arguments)):
120 for value in argument_lists[i][1:]:
121 arguments[i] = value
122 yield self._format_arguments(name, arguments)
123 arguments[i] = argument_lists[0][0]
124 except BaseException as e:
125 raise Exception('distribute_arguments({})'.format(name)) from e
126
Gilles Peskine10ab2672021-03-10 00:59:53 +0100127 def generate_expressions(self, names: Iterable[str]) -> Iterator[str]:
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +0100128 """Generate expressions covering values constructed from the given names.
129
130 `names` can be any iterable collection of macro names.
131
132 For example:
133 * ``generate_expressions(['PSA_ALG_CMAC', 'PSA_ALG_HMAC'])``
134 generates ``'PSA_ALG_CMAC'`` as well as ``'PSA_ALG_HMAC(h)'`` for
135 every known hash algorithm ``h``.
136 * ``macros.generate_expressions(macros.key_types)`` generates all
137 key types.
138 """
139 return itertools.chain(*map(self.distribute_arguments, names))
140
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200141class ReadFileLineException(Exception):
142 def __init__(self, filename, line_number):
143 message = 'in {} at {}'.format(filename, line_number)
144 super(ReadFileLineException, self).__init__(message)
145 self.filename = filename
146 self.line_number = line_number
147
148class read_file_lines:
Gilles Peskine54f54452019-05-27 18:31:59 +0200149 # Dear Pylint, conventionally, a context manager class name is lowercase.
150 # pylint: disable=invalid-name,too-few-public-methods
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200151 """Context manager to read a text file line by line.
152
153 ```
154 with read_file_lines(filename) as lines:
155 for line in lines:
156 process(line)
157 ```
158 is equivalent to
159 ```
160 with open(filename, 'r') as input_file:
161 for line in input_file:
162 process(line)
163 ```
164 except that if process(line) raises an exception, then the read_file_lines
165 snippet annotates the exception with the file name and line number.
166 """
Gilles Peskine49af2d32019-12-06 19:20:13 +0100167 def __init__(self, filename, binary=False):
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200168 self.filename = filename
169 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +0200170 self.generator = None
Gilles Peskine49af2d32019-12-06 19:20:13 +0100171 self.binary = binary
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200172 def __enter__(self):
Gilles Peskine49af2d32019-12-06 19:20:13 +0100173 self.generator = enumerate(open(self.filename,
174 'rb' if self.binary else 'r'))
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200175 return self
176 def __iter__(self):
177 for line_number, content in self.generator:
178 self.line_number = line_number
179 yield content
180 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200181 def __exit__(self, exc_type, exc_value, exc_traceback):
182 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200183 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200184 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200185
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +0100186class InputsForTest(PSAMacroEnumerator):
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100187 # pylint: disable=too-many-instance-attributes
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200188 """Accumulate information about macros to test.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100189
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200190 This includes macro names as well as information about their arguments
191 when applicable.
192 """
193
Gilles Peskine24827022018-09-25 18:49:23 +0200194 def __init__(self):
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +0100195 super().__init__()
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100196 self.all_declared = set()
Gilles Peskine24827022018-09-25 18:49:23 +0200197 # Sets of names per type
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +0100198 self.statuses.add('PSA_SUCCESS')
199 self.algorithms.add('0xffffffff')
200 self.ecc_curves.add('0xff')
201 self.dh_groups.add('0xff')
202 self.key_types.add('0xffff')
203 self.key_usage_flags.add('0x80000000')
204
Bence Szépkúti4af65602020-12-08 11:10:21 +0100205 # Hard-coded values for unknown algorithms
206 #
207 # These have to have values that are correct for their respective
208 # PSA_ALG_IS_xxx macros, but are also not currently assigned and are
209 # not likely to be assigned in the near future.
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +0100210 self.hash_algorithms.add('0x020000fe') # 0x020000ff is PSA_ALG_ANY_HASH
211 self.mac_algorithms.add('0x03007fff')
212 self.ka_algorithms.add('0x09fc0000')
213 self.kdf_algorithms.add('0x080000ff')
Gilles Peskine434899f2018-10-19 11:30:26 +0200214 # For AEAD algorithms, the only variability is over the tag length,
215 # and this only applies to known algorithms, so don't test an
216 # unknown algorithm.
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +0100217
Gilles Peskine24827022018-09-25 18:49:23 +0200218 # Identifier prefixes
219 self.table_by_prefix = {
220 'ERROR': self.statuses,
221 'ALG': self.algorithms,
Gilles Peskine98a710c2019-11-21 18:58:36 +0100222 'ECC_CURVE': self.ecc_curves,
223 'DH_GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +0200224 'KEY_TYPE': self.key_types,
225 'KEY_USAGE': self.key_usage_flags,
226 }
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100227 # Test functions
228 self.table_by_test_function = {
Gilles Peskine8fa13482019-11-25 17:10:12 +0100229 # Any function ending in _algorithm also gets added to
230 # self.algorithms.
231 'key_type': [self.key_types],
Gilles Peskinef8210f22019-12-02 17:26:44 +0100232 'block_cipher_key_type': [self.key_types],
233 'stream_cipher_key_type': [self.key_types],
Gilles Peskine228abc52019-12-03 17:24:19 +0100234 'ecc_key_family': [self.ecc_curves],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100235 'ecc_key_types': [self.ecc_curves],
Gilles Peskine228abc52019-12-03 17:24:19 +0100236 'dh_key_family': [self.dh_groups],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100237 'dh_key_types': [self.dh_groups],
238 'hash_algorithm': [self.hash_algorithms],
239 'mac_algorithm': [self.mac_algorithms],
240 'cipher_algorithm': [],
241 'hmac_algorithm': [self.mac_algorithms],
242 'aead_algorithm': [self.aead_algorithms],
243 'key_derivation_algorithm': [self.kdf_algorithms],
244 'key_agreement_algorithm': [self.ka_algorithms],
245 'asymmetric_signature_algorithm': [],
246 'asymmetric_signature_wildcard': [self.algorithms],
247 'asymmetric_encryption_algorithm': [],
248 'other_algorithm': [],
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100249 }
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +0100250 self.arguments_for['mac_length'] += ['1', '63']
251 self.arguments_for['min_mac_length'] += ['1', '63']
252 self.arguments_for['tag_length'] += ['1', '63']
253 self.arguments_for['min_tag_length'] += ['1', '63']
Gilles Peskine24827022018-09-25 18:49:23 +0200254
Gilles Peskineffe2d6e2019-11-21 17:17:01 +0100255 def get_names(self, type_word):
256 """Return the set of known names of values of the given type."""
257 return {
258 'status': self.statuses,
259 'algorithm': self.algorithms,
260 'ecc_curve': self.ecc_curves,
261 'dh_group': self.dh_groups,
262 'key_type': self.key_types,
263 'key_usage': self.key_usage_flags,
264 }[type_word]
265
Gilles Peskine24827022018-09-25 18:49:23 +0200266 # Regex for interesting header lines.
267 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200268 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200269 re.compile(r'#define +' +
Gilles Peskine98a710c2019-11-21 18:58:36 +0100270 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
Gilles Peskine24827022018-09-25 18:49:23 +0200271 r'(?:\(([^\n()]*)\))?')
272 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200273 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200274 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200275 _excluded_names = set([
276 # Macros that provide an alternative way to build the same
277 # algorithm as another macro.
Bence Szépkútia63b20d2020-12-16 11:36:46 +0100278 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200279 'PSA_ALG_FULL_LENGTH_MAC',
280 # Auxiliary macro whose name doesn't fit the usual patterns for
281 # auxiliary macros.
Bence Szépkútia63b20d2020-12-16 11:36:46 +0100282 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG_CASE',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200283 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200284 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200285 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200286 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200287 if not m:
288 return
289 name = m.group(1)
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100290 self.all_declared.add(name)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200291 if re.search(self._excluded_name_re, name) or \
292 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200293 return
294 dest = self.table_by_prefix.get(m.group(2))
295 if dest is None:
296 return
297 dest.add(name)
298 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200299 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200300
Gilles Peskine49af2d32019-12-06 19:20:13 +0100301 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
Gilles Peskine24827022018-09-25 18:49:23 +0200302 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200303 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskine49af2d32019-12-06 19:20:13 +0100304 with read_file_lines(filename, binary=True) as lines:
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200305 for line in lines:
Gilles Peskine49af2d32019-12-06 19:20:13 +0100306 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
Gilles Peskine24827022018-09-25 18:49:23 +0200307 self.parse_header_line(line)
308
Gilles Peskine49af2d32019-12-06 19:20:13 +0100309 _macro_identifier_re = re.compile(r'[A-Z]\w+')
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100310 def generate_undeclared_names(self, expr):
311 for name in re.findall(self._macro_identifier_re, expr):
312 if name not in self.all_declared:
313 yield name
314
315 def accept_test_case_line(self, function, argument):
316 #pylint: disable=unused-argument
317 undeclared = list(self.generate_undeclared_names(argument))
318 if undeclared:
319 raise Exception('Undeclared names in test case', undeclared)
320 return True
321
Gilles Peskine24827022018-09-25 18:49:23 +0200322 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200323 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100324 sets = []
Gilles Peskine24827022018-09-25 18:49:23 +0200325 if function.endswith('_algorithm'):
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100326 sets.append(self.algorithms)
Gilles Peskine79616682019-11-21 20:08:10 +0100327 if function == 'key_agreement_algorithm' and \
328 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
329 # We only want *raw* key agreement algorithms as such, so
330 # exclude ones that are already chained with a KDF.
331 # Keep the expression as one to test as an algorithm.
332 function = 'other_algorithm'
Gilles Peskine8fa13482019-11-25 17:10:12 +0100333 sets += self.table_by_test_function[function]
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100334 if self.accept_test_case_line(function, argument):
335 for s in sets:
336 s.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200337
338 # Regex matching a *.data line containing a test function call and
339 # its arguments. The actual definition is partly positional, but this
340 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200341 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200342 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200343 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200344 with read_file_lines(filename) as lines:
345 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200346 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200347 if m:
348 self.add_test_case_line(m.group(1), m.group(2))
349
Gilles Peskine6f7ba5f2021-03-10 00:50:18 +0100350def gather_inputs(headers, test_suites, inputs_class=InputsForTest):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200351 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine84a45812019-11-21 19:50:33 +0100352 inputs = inputs_class()
Gilles Peskine24827022018-09-25 18:49:23 +0200353 for header in headers:
354 inputs.parse_header(header)
355 for test_cases in test_suites:
356 inputs.parse_test_cases(test_cases)
357 inputs.gather_arguments()
358 return inputs
359
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100360def run_c(type_word, expressions, include_path=None, keep_c=False):
Gilles Peskine2991b5f2021-01-19 21:19:02 +0100361 """Generate and run a program to print out numerical values of C expressions."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200362 if type_word == 'status':
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100363 cast_to = 'long'
364 printf_format = '%ld'
365 else:
366 cast_to = 'unsigned long'
367 printf_format = '0x%08lx'
Gilles Peskine2adebc82020-12-11 00:30:53 +0100368 return c_build_helper.get_c_expression_values(
Gilles Peskinefc622112020-12-11 00:27:14 +0100369 cast_to, printf_format,
370 expressions,
371 caller='test_psa_constant_names.py for {} values'.format(type_word),
372 file_label=type_word,
373 header='#include <psa/crypto.h>',
374 include_path=include_path,
375 keep_c=keep_c
376 )
Gilles Peskine24827022018-09-25 18:49:23 +0200377
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200378NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200379def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200380 """Normalize the C expression so as not to care about trivial differences.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100381
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200382 Currently "trivial differences" means whitespace.
383 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100384 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200385
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100386def collect_values(inputs, type_word, include_path=None, keep_c=False):
Gilles Peskinec2317112019-11-21 17:17:39 +0100387 """Generate expressions using known macro names and calculate their values.
388
389 Return a list of pairs of (expr, value) where expr is an expression and
390 value is a string representation of its integer value.
391 """
392 names = inputs.get_names(type_word)
393 expressions = sorted(inputs.generate_expressions(names))
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100394 values = run_c(type_word, expressions,
395 include_path=include_path, keep_c=keep_c)
Gilles Peskinec2317112019-11-21 17:17:39 +0100396 return expressions, values
397
Gilles Peskine24609332019-11-21 17:44:21 +0100398class Tests:
399 """An object representing tests and their results."""
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100400
Gilles Peskinea5000f12019-11-21 17:51:11 +0100401 Error = namedtuple('Error',
402 ['type', 'expression', 'value', 'output'])
403
Gilles Peskine24609332019-11-21 17:44:21 +0100404 def __init__(self, options):
405 self.options = options
406 self.count = 0
407 self.errors = []
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100408
Gilles Peskine24609332019-11-21 17:44:21 +0100409 def run_one(self, inputs, type_word):
410 """Test psa_constant_names for the specified type.
Gilles Peskine24827022018-09-25 18:49:23 +0200411
Gilles Peskine24609332019-11-21 17:44:21 +0100412 Run the program on the names for this type.
413 Use the inputs to figure out what arguments to pass to macros that
414 take arguments.
415 """
416 expressions, values = collect_values(inputs, type_word,
417 include_path=self.options.include,
418 keep_c=self.options.keep_c)
419 output = subprocess.check_output([self.options.program, type_word] +
420 values)
421 outputs = output.decode('ascii').strip().split('\n')
422 self.count += len(expressions)
423 for expr, value, output in zip(expressions, values, outputs):
Gilles Peskine32558482019-12-03 19:03:35 +0100424 if self.options.show:
425 sys.stdout.write('{} {}\t{}\n'.format(type_word, value, output))
Gilles Peskine24609332019-11-21 17:44:21 +0100426 if normalize(expr) != normalize(output):
Gilles Peskinea5000f12019-11-21 17:51:11 +0100427 self.errors.append(self.Error(type=type_word,
428 expression=expr,
429 value=value,
430 output=output))
Gilles Peskine24827022018-09-25 18:49:23 +0200431
Gilles Peskine24609332019-11-21 17:44:21 +0100432 def run_all(self, inputs):
433 """Run psa_constant_names on all the gathered inputs."""
434 for type_word in ['status', 'algorithm', 'ecc_curve', 'dh_group',
435 'key_type', 'key_usage']:
436 self.run_one(inputs, type_word)
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100437
Gilles Peskine24609332019-11-21 17:44:21 +0100438 def report(self, out):
439 """Describe each case where the output is not as expected.
440
441 Write the errors to ``out``.
442 Also write a total.
443 """
Gilles Peskinea5000f12019-11-21 17:51:11 +0100444 for error in self.errors:
Gilles Peskine24609332019-11-21 17:44:21 +0100445 out.write('For {} "{}", got "{}" (value: {})\n'
Gilles Peskinea5000f12019-11-21 17:51:11 +0100446 .format(error.type, error.expression,
447 error.output, error.value))
Gilles Peskine24609332019-11-21 17:44:21 +0100448 out.write('{} test cases'.format(self.count))
449 if self.errors:
450 out.write(', {} FAIL\n'.format(len(self.errors)))
451 else:
452 out.write(' PASS\n')
Gilles Peskine24827022018-09-25 18:49:23 +0200453
Gilles Peskine69f93b52019-11-21 16:49:50 +0100454HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
455TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
456
Gilles Peskine54f54452019-05-27 18:31:59 +0200457def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200458 parser = argparse.ArgumentParser(description=globals()['__doc__'])
459 parser.add_argument('--include', '-I',
460 action='append', default=['include'],
461 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200462 parser.add_argument('--keep-c',
463 action='store_true', dest='keep_c', default=False,
464 help='Keep the intermediate C file')
465 parser.add_argument('--no-keep-c',
466 action='store_false', dest='keep_c',
467 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100468 parser.add_argument('--program',
469 default='programs/psa/psa_constant_names',
470 help='Program to test')
Gilles Peskine32558482019-12-03 19:03:35 +0100471 parser.add_argument('--show',
472 action='store_true',
473 help='Keep the intermediate C file')
474 parser.add_argument('--no-show',
475 action='store_false', dest='show',
476 help='Don\'t show tested values (default)')
Gilles Peskine24827022018-09-25 18:49:23 +0200477 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100478 headers = [os.path.join(options.include[0], h) for h in HEADERS]
479 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24609332019-11-21 17:44:21 +0100480 tests = Tests(options)
481 tests.run_all(inputs)
482 tests.report(sys.stdout)
483 if tests.errors:
Gilles Peskine8b022352020-03-24 18:36:56 +0100484 sys.exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200485
486if __name__ == '__main__':
487 main()