blob: 8f393a1ab54b0aba52f890b8c7498da3774c6733 [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
10import argparse
Gilles Peskinea5000f12019-11-21 17:51:11 +010011from collections import namedtuple
Gilles Peskine24827022018-09-25 18:49:23 +020012import itertools
13import os
14import platform
15import re
16import subprocess
17import sys
18import tempfile
19
Gilles Peskinea0a315c2018-10-19 11:27:10 +020020class ReadFileLineException(Exception):
21 def __init__(self, filename, line_number):
22 message = 'in {} at {}'.format(filename, line_number)
23 super(ReadFileLineException, self).__init__(message)
24 self.filename = filename
25 self.line_number = line_number
26
27class read_file_lines:
Gilles Peskine54f54452019-05-27 18:31:59 +020028 # Dear Pylint, conventionally, a context manager class name is lowercase.
29 # pylint: disable=invalid-name,too-few-public-methods
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020030 """Context manager to read a text file line by line.
31
32 ```
33 with read_file_lines(filename) as lines:
34 for line in lines:
35 process(line)
36 ```
37 is equivalent to
38 ```
39 with open(filename, 'r') as input_file:
40 for line in input_file:
41 process(line)
42 ```
43 except that if process(line) raises an exception, then the read_file_lines
44 snippet annotates the exception with the file name and line number.
45 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +020046 def __init__(self, filename):
47 self.filename = filename
48 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +020049 self.generator = None
Gilles Peskinea0a315c2018-10-19 11:27:10 +020050 def __enter__(self):
51 self.generator = enumerate(open(self.filename, 'r'))
52 return self
53 def __iter__(self):
54 for line_number, content in self.generator:
55 self.line_number = line_number
56 yield content
57 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020058 def __exit__(self, exc_type, exc_value, exc_traceback):
59 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +020060 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020061 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +020062
Gilles Peskine24827022018-09-25 18:49:23 +020063class Inputs:
Gilles Peskine8c8694c2019-11-21 19:22:45 +010064 # pylint: disable=too-many-instance-attributes
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020065 """Accumulate information about macros to test.
Gilles Peskine4408dfd2019-11-21 17:16:21 +010066
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020067 This includes macro names as well as information about their arguments
68 when applicable.
69 """
70
Gilles Peskine24827022018-09-25 18:49:23 +020071 def __init__(self):
Gilles Peskine2bcfc712019-11-21 19:49:26 +010072 self.all_declared = set()
Gilles Peskine24827022018-09-25 18:49:23 +020073 # Sets of names per type
74 self.statuses = set(['PSA_SUCCESS'])
75 self.algorithms = set(['0xffffffff'])
76 self.ecc_curves = set(['0xffff'])
Gilles Peskinedcaefae2019-05-16 12:55:35 +020077 self.dh_groups = set(['0xffff'])
Gilles Peskine24827022018-09-25 18:49:23 +020078 self.key_types = set(['0xffffffff'])
79 self.key_usage_flags = set(['0x80000000'])
Gilles Peskine434899f2018-10-19 11:30:26 +020080 # Hard-coded value for unknown algorithms
Darryl Green61b7f612019-02-04 16:00:21 +000081 self.hash_algorithms = set(['0x010000fe'])
Gilles Peskine434899f2018-10-19 11:30:26 +020082 self.mac_algorithms = set(['0x02ff00ff'])
Gilles Peskine882e57e2019-04-12 00:12:07 +020083 self.ka_algorithms = set(['0x30fc0000'])
84 self.kdf_algorithms = set(['0x200000ff'])
Gilles Peskine434899f2018-10-19 11:30:26 +020085 # For AEAD algorithms, the only variability is over the tag length,
86 # and this only applies to known algorithms, so don't test an
87 # unknown algorithm.
88 self.aead_algorithms = set()
Gilles Peskine24827022018-09-25 18:49:23 +020089 # Identifier prefixes
90 self.table_by_prefix = {
91 'ERROR': self.statuses,
92 'ALG': self.algorithms,
Gilles Peskine98a710c2019-11-21 18:58:36 +010093 'ECC_CURVE': self.ecc_curves,
94 'DH_GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +020095 'KEY_TYPE': self.key_types,
96 'KEY_USAGE': self.key_usage_flags,
97 }
Gilles Peskine8c8694c2019-11-21 19:22:45 +010098 # Test functions
99 self.table_by_test_function = {
100 'key_type': self.key_types,
101 'ecc_key_types': self.ecc_curves,
102 'dh_key_types': self.dh_groups,
103 'hash_algorithm': self.hash_algorithms,
104 'mac_algorithm': self.mac_algorithms,
105 'hmac_algorithm': self.mac_algorithms,
106 'aead_algorithm': self.aead_algorithms,
107 }
Gilles Peskine24827022018-09-25 18:49:23 +0200108 # macro name -> list of argument names
109 self.argspecs = {}
110 # argument name -> list of values
Gilles Peskine434899f2018-10-19 11:30:26 +0200111 self.arguments_for = {
112 'mac_length': ['1', '63'],
113 'tag_length': ['1', '63'],
114 }
Gilles Peskine24827022018-09-25 18:49:23 +0200115
Gilles Peskineffe2d6e2019-11-21 17:17:01 +0100116 def get_names(self, type_word):
117 """Return the set of known names of values of the given type."""
118 return {
119 'status': self.statuses,
120 'algorithm': self.algorithms,
121 'ecc_curve': self.ecc_curves,
122 'dh_group': self.dh_groups,
123 'key_type': self.key_types,
124 'key_usage': self.key_usage_flags,
125 }[type_word]
126
Gilles Peskine24827022018-09-25 18:49:23 +0200127 def gather_arguments(self):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200128 """Populate the list of values for macro arguments.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100129
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200130 Call this after parsing all the inputs.
131 """
Gilles Peskine24827022018-09-25 18:49:23 +0200132 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200133 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
Gilles Peskine882e57e2019-04-12 00:12:07 +0200134 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
Gilles Peskine17542082019-01-04 19:46:31 +0100135 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200136 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
Gilles Peskine24827022018-09-25 18:49:23 +0200137 self.arguments_for['curve'] = sorted(self.ecc_curves)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200138 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine24827022018-09-25 18:49:23 +0200139
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200140 @staticmethod
141 def _format_arguments(name, arguments):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200142 """Format a macro call with arguments.."""
Gilles Peskine24827022018-09-25 18:49:23 +0200143 return name + '(' + ', '.join(arguments) + ')'
144
145 def distribute_arguments(self, name):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200146 """Generate macro calls with each tested argument set.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100147
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200148 If name is a macro without arguments, just yield "name".
149 If name is a macro with arguments, yield a series of
150 "name(arg1,...,argN)" where each argument takes each possible
151 value at least once.
152 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200153 try:
154 if name not in self.argspecs:
155 yield name
156 return
157 argspec = self.argspecs[name]
158 if argspec == []:
159 yield name + '()'
160 return
161 argument_lists = [self.arguments_for[arg] for arg in argspec]
162 arguments = [values[0] for values in argument_lists]
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200163 yield self._format_arguments(name, arguments)
Gilles Peskine54f54452019-05-27 18:31:59 +0200164 # Dear Pylint, enumerate won't work here since we're modifying
165 # the array.
166 # pylint: disable=consider-using-enumerate
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200167 for i in range(len(arguments)):
168 for value in argument_lists[i][1:]:
169 arguments[i] = value
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200170 yield self._format_arguments(name, arguments)
Gilles Peskinef96ed662018-10-19 11:29:56 +0200171 arguments[i] = argument_lists[0][0]
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200172 except BaseException as e:
173 raise Exception('distribute_arguments({})'.format(name)) from e
Gilles Peskine24827022018-09-25 18:49:23 +0200174
Gilles Peskine5a994c12019-11-21 16:46:51 +0100175 def generate_expressions(self, names):
176 return itertools.chain(*map(self.distribute_arguments, names))
177
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200178 _argument_split_re = re.compile(r' *, *')
179 @classmethod
180 def _argument_split(cls, arguments):
181 return re.split(cls._argument_split_re, arguments)
182
Gilles Peskine24827022018-09-25 18:49:23 +0200183 # Regex for interesting header lines.
184 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200185 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200186 re.compile(r'#define +' +
Gilles Peskine98a710c2019-11-21 18:58:36 +0100187 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
Gilles Peskine24827022018-09-25 18:49:23 +0200188 r'(?:\(([^\n()]*)\))?')
189 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200190 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200191 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200192 _excluded_names = set([
193 # Macros that provide an alternative way to build the same
194 # algorithm as another macro.
195 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH',
196 'PSA_ALG_FULL_LENGTH_MAC',
197 # Auxiliary macro whose name doesn't fit the usual patterns for
198 # auxiliary macros.
199 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH_CASE',
200 # PSA_ALG_ECDH and PSA_ALG_FFDH are excluded for now as the script
201 # currently doesn't support them.
202 'PSA_ALG_ECDH',
203 'PSA_ALG_FFDH',
204 # Deprecated aliases.
205 'PSA_ERROR_UNKNOWN_ERROR',
206 'PSA_ERROR_OCCUPIED_SLOT',
207 'PSA_ERROR_EMPTY_SLOT',
208 'PSA_ERROR_INSUFFICIENT_CAPACITY',
Gilles Peskine19835122019-05-17 12:06:55 +0200209 'PSA_ERROR_TAMPERING_DETECTED',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200210 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200211 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200212 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200213 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200214 if not m:
215 return
216 name = m.group(1)
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100217 self.all_declared.add(name)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200218 if re.search(self._excluded_name_re, name) or \
219 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200220 return
221 dest = self.table_by_prefix.get(m.group(2))
222 if dest is None:
223 return
224 dest.add(name)
225 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200226 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200227
228 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200229 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200230 with read_file_lines(filename) as lines:
231 for line in lines:
Gilles Peskine24827022018-09-25 18:49:23 +0200232 self.parse_header_line(line)
233
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100234 _macro_identifier_re = r'[A-Z]\w+'
235 def generate_undeclared_names(self, expr):
236 for name in re.findall(self._macro_identifier_re, expr):
237 if name not in self.all_declared:
238 yield name
239
240 def accept_test_case_line(self, function, argument):
241 #pylint: disable=unused-argument
242 undeclared = list(self.generate_undeclared_names(argument))
243 if undeclared:
244 raise Exception('Undeclared names in test case', undeclared)
245 return True
246
Gilles Peskine24827022018-09-25 18:49:23 +0200247 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200248 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100249 sets = []
Gilles Peskine24827022018-09-25 18:49:23 +0200250 if function.endswith('_algorithm'):
Darryl Greenb8fe0682019-02-06 13:21:31 +0000251 # As above, ECDH and FFDH algorithms are excluded for now.
252 # Support for them will be added in the future.
Darryl Greenec079502019-01-29 15:48:00 +0000253 if 'ECDH' in argument or 'FFDH' in argument:
254 return
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100255 sets.append(self.algorithms)
256 if function in self.table_by_test_function:
257 sets.append(self.table_by_test_function[function])
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100258 if self.accept_test_case_line(function, argument):
259 for s in sets:
260 s.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200261
262 # Regex matching a *.data line containing a test function call and
263 # its arguments. The actual definition is partly positional, but this
264 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200265 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200266 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200267 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200268 with read_file_lines(filename) as lines:
269 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200270 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200271 if m:
272 self.add_test_case_line(m.group(1), m.group(2))
273
Gilles Peskine84a45812019-11-21 19:50:33 +0100274def gather_inputs(headers, test_suites, inputs_class=Inputs):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200275 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine84a45812019-11-21 19:50:33 +0100276 inputs = inputs_class()
Gilles Peskine24827022018-09-25 18:49:23 +0200277 for header in headers:
278 inputs.parse_header(header)
279 for test_cases in test_suites:
280 inputs.parse_test_cases(test_cases)
281 inputs.gather_arguments()
282 return inputs
283
284def remove_file_if_exists(filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200285 """Remove the specified file, ignoring errors."""
Gilles Peskine24827022018-09-25 18:49:23 +0200286 if not filename:
287 return
288 try:
289 os.remove(filename)
Gilles Peskine54f54452019-05-27 18:31:59 +0200290 except OSError:
Gilles Peskine24827022018-09-25 18:49:23 +0200291 pass
292
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100293def run_c(type_word, expressions, include_path=None, keep_c=False):
Gilles Peskine5a994c12019-11-21 16:46:51 +0100294 """Generate and run a program to print out numerical values for expressions."""
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100295 if include_path is None:
296 include_path = []
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200297 if type_word == 'status':
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100298 cast_to = 'long'
299 printf_format = '%ld'
300 else:
301 cast_to = 'unsigned long'
302 printf_format = '0x%08lx'
Gilles Peskine24827022018-09-25 18:49:23 +0200303 c_name = None
304 exe_name = None
305 try:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200306 c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(type_word),
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100307 suffix='.c',
Gilles Peskine24827022018-09-25 18:49:23 +0200308 dir='programs/psa')
309 exe_suffix = '.exe' if platform.system() == 'Windows' else ''
310 exe_name = c_name[:-2] + exe_suffix
311 remove_file_if_exists(exe_name)
312 c_file = os.fdopen(c_fd, 'w', encoding='ascii')
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100313 c_file.write('/* Generated by test_psa_constant_names.py for {} values */'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200314 .format(type_word))
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100315 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200316#include <stdio.h>
317#include <psa/crypto.h>
318int main(void)
319{
320''')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100321 for expr in expressions:
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100322 c_file.write(' printf("{}\\n", ({}) {});\n'
Gilles Peskine5a994c12019-11-21 16:46:51 +0100323 .format(printf_format, cast_to, expr))
Gilles Peskine24827022018-09-25 18:49:23 +0200324 c_file.write(''' return 0;
325}
326''')
327 c_file.close()
328 cc = os.getenv('CC', 'cc')
329 subprocess.check_call([cc] +
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100330 ['-I' + dir for dir in include_path] +
Gilles Peskine24827022018-09-25 18:49:23 +0200331 ['-o', exe_name, c_name])
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100332 if keep_c:
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200333 sys.stderr.write('List of {} tests kept at {}\n'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200334 .format(type_word, c_name))
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200335 else:
336 os.remove(c_name)
Gilles Peskine24827022018-09-25 18:49:23 +0200337 output = subprocess.check_output([exe_name])
338 return output.decode('ascii').strip().split('\n')
339 finally:
340 remove_file_if_exists(exe_name)
341
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200342NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200343def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200344 """Normalize the C expression so as not to care about trivial differences.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100345
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200346 Currently "trivial differences" means whitespace.
347 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100348 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200349
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100350def collect_values(inputs, type_word, include_path=None, keep_c=False):
Gilles Peskinec2317112019-11-21 17:17:39 +0100351 """Generate expressions using known macro names and calculate their values.
352
353 Return a list of pairs of (expr, value) where expr is an expression and
354 value is a string representation of its integer value.
355 """
356 names = inputs.get_names(type_word)
357 expressions = sorted(inputs.generate_expressions(names))
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100358 values = run_c(type_word, expressions,
359 include_path=include_path, keep_c=keep_c)
Gilles Peskinec2317112019-11-21 17:17:39 +0100360 return expressions, values
361
Gilles Peskine24609332019-11-21 17:44:21 +0100362class Tests:
363 """An object representing tests and their results."""
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100364
Gilles Peskinea5000f12019-11-21 17:51:11 +0100365 Error = namedtuple('Error',
366 ['type', 'expression', 'value', 'output'])
367
Gilles Peskine24609332019-11-21 17:44:21 +0100368 def __init__(self, options):
369 self.options = options
370 self.count = 0
371 self.errors = []
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100372
Gilles Peskine24609332019-11-21 17:44:21 +0100373 def run_one(self, inputs, type_word):
374 """Test psa_constant_names for the specified type.
Gilles Peskine24827022018-09-25 18:49:23 +0200375
Gilles Peskine24609332019-11-21 17:44:21 +0100376 Run the program on the names for this type.
377 Use the inputs to figure out what arguments to pass to macros that
378 take arguments.
379 """
380 expressions, values = collect_values(inputs, type_word,
381 include_path=self.options.include,
382 keep_c=self.options.keep_c)
383 output = subprocess.check_output([self.options.program, type_word] +
384 values)
385 outputs = output.decode('ascii').strip().split('\n')
386 self.count += len(expressions)
387 for expr, value, output in zip(expressions, values, outputs):
388 if normalize(expr) != normalize(output):
Gilles Peskinea5000f12019-11-21 17:51:11 +0100389 self.errors.append(self.Error(type=type_word,
390 expression=expr,
391 value=value,
392 output=output))
Gilles Peskine24827022018-09-25 18:49:23 +0200393
Gilles Peskine24609332019-11-21 17:44:21 +0100394 def run_all(self, inputs):
395 """Run psa_constant_names on all the gathered inputs."""
396 for type_word in ['status', 'algorithm', 'ecc_curve', 'dh_group',
397 'key_type', 'key_usage']:
398 self.run_one(inputs, type_word)
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100399
Gilles Peskine24609332019-11-21 17:44:21 +0100400 def report(self, out):
401 """Describe each case where the output is not as expected.
402
403 Write the errors to ``out``.
404 Also write a total.
405 """
Gilles Peskinea5000f12019-11-21 17:51:11 +0100406 for error in self.errors:
Gilles Peskine24609332019-11-21 17:44:21 +0100407 out.write('For {} "{}", got "{}" (value: {})\n'
Gilles Peskinea5000f12019-11-21 17:51:11 +0100408 .format(error.type, error.expression,
409 error.output, error.value))
Gilles Peskine24609332019-11-21 17:44:21 +0100410 out.write('{} test cases'.format(self.count))
411 if self.errors:
412 out.write(', {} FAIL\n'.format(len(self.errors)))
413 else:
414 out.write(' PASS\n')
Gilles Peskine24827022018-09-25 18:49:23 +0200415
Gilles Peskine69f93b52019-11-21 16:49:50 +0100416HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
417TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
418
Gilles Peskine54f54452019-05-27 18:31:59 +0200419def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200420 parser = argparse.ArgumentParser(description=globals()['__doc__'])
421 parser.add_argument('--include', '-I',
422 action='append', default=['include'],
423 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200424 parser.add_argument('--keep-c',
425 action='store_true', dest='keep_c', default=False,
426 help='Keep the intermediate C file')
427 parser.add_argument('--no-keep-c',
428 action='store_false', dest='keep_c',
429 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100430 parser.add_argument('--program',
431 default='programs/psa/psa_constant_names',
432 help='Program to test')
Gilles Peskine24827022018-09-25 18:49:23 +0200433 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100434 headers = [os.path.join(options.include[0], h) for h in HEADERS]
435 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24609332019-11-21 17:44:21 +0100436 tests = Tests(options)
437 tests.run_all(inputs)
438 tests.report(sys.stdout)
439 if tests.errors:
Gilles Peskine24827022018-09-25 18:49:23 +0200440 exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200441
442if __name__ == '__main__':
443 main()