blob: 482932137467aac8f05c936a9c42a5dd6c4455ab [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 Peskine49af2d32019-12-06 19:20:13 +010046 def __init__(self, filename, binary=False):
Gilles Peskinea0a315c2018-10-19 11:27:10 +020047 self.filename = filename
48 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +020049 self.generator = None
Gilles Peskine49af2d32019-12-06 19:20:13 +010050 self.binary = binary
Gilles Peskinea0a315c2018-10-19 11:27:10 +020051 def __enter__(self):
Gilles Peskine49af2d32019-12-06 19:20:13 +010052 self.generator = enumerate(open(self.filename,
53 'rb' if self.binary else 'r'))
Gilles Peskinea0a315c2018-10-19 11:27:10 +020054 return self
55 def __iter__(self):
56 for line_number, content in self.generator:
57 self.line_number = line_number
58 yield content
59 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020060 def __exit__(self, exc_type, exc_value, exc_traceback):
61 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +020062 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020063 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +020064
Gilles Peskine24827022018-09-25 18:49:23 +020065class Inputs:
Gilles Peskine8c8694c2019-11-21 19:22:45 +010066 # pylint: disable=too-many-instance-attributes
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020067 """Accumulate information about macros to test.
Gilles Peskine4408dfd2019-11-21 17:16:21 +010068
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020069 This includes macro names as well as information about their arguments
70 when applicable.
71 """
72
Gilles Peskine24827022018-09-25 18:49:23 +020073 def __init__(self):
Gilles Peskine2bcfc712019-11-21 19:49:26 +010074 self.all_declared = set()
Gilles Peskine24827022018-09-25 18:49:23 +020075 # Sets of names per type
76 self.statuses = set(['PSA_SUCCESS'])
77 self.algorithms = set(['0xffffffff'])
78 self.ecc_curves = set(['0xffff'])
Gilles Peskinedcaefae2019-05-16 12:55:35 +020079 self.dh_groups = set(['0xffff'])
Gilles Peskine24827022018-09-25 18:49:23 +020080 self.key_types = set(['0xffffffff'])
81 self.key_usage_flags = set(['0x80000000'])
Gilles Peskine434899f2018-10-19 11:30:26 +020082 # Hard-coded value for unknown algorithms
Darryl Green61b7f612019-02-04 16:00:21 +000083 self.hash_algorithms = set(['0x010000fe'])
Gilles Peskine434899f2018-10-19 11:30:26 +020084 self.mac_algorithms = set(['0x02ff00ff'])
Gilles Peskine882e57e2019-04-12 00:12:07 +020085 self.ka_algorithms = set(['0x30fc0000'])
86 self.kdf_algorithms = set(['0x200000ff'])
Gilles Peskine434899f2018-10-19 11:30:26 +020087 # For AEAD algorithms, the only variability is over the tag length,
88 # and this only applies to known algorithms, so don't test an
89 # unknown algorithm.
90 self.aead_algorithms = set()
Gilles Peskine24827022018-09-25 18:49:23 +020091 # Identifier prefixes
92 self.table_by_prefix = {
93 'ERROR': self.statuses,
94 'ALG': self.algorithms,
Gilles Peskine98a710c2019-11-21 18:58:36 +010095 'ECC_CURVE': self.ecc_curves,
96 'DH_GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +020097 'KEY_TYPE': self.key_types,
98 'KEY_USAGE': self.key_usage_flags,
99 }
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100100 # Test functions
101 self.table_by_test_function = {
Gilles Peskine8fa13482019-11-25 17:10:12 +0100102 # Any function ending in _algorithm also gets added to
103 # self.algorithms.
104 'key_type': [self.key_types],
105 'ecc_key_types': [self.ecc_curves],
106 'dh_key_types': [self.dh_groups],
107 'hash_algorithm': [self.hash_algorithms],
108 'mac_algorithm': [self.mac_algorithms],
109 'cipher_algorithm': [],
110 'hmac_algorithm': [self.mac_algorithms],
111 'aead_algorithm': [self.aead_algorithms],
112 'key_derivation_algorithm': [self.kdf_algorithms],
113 'key_agreement_algorithm': [self.ka_algorithms],
114 'asymmetric_signature_algorithm': [],
115 'asymmetric_signature_wildcard': [self.algorithms],
116 'asymmetric_encryption_algorithm': [],
117 'other_algorithm': [],
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100118 }
Gilles Peskine24827022018-09-25 18:49:23 +0200119 # macro name -> list of argument names
120 self.argspecs = {}
121 # argument name -> list of values
Gilles Peskine434899f2018-10-19 11:30:26 +0200122 self.arguments_for = {
123 'mac_length': ['1', '63'],
124 'tag_length': ['1', '63'],
125 }
Gilles Peskine24827022018-09-25 18:49:23 +0200126
Gilles Peskineffe2d6e2019-11-21 17:17:01 +0100127 def get_names(self, type_word):
128 """Return the set of known names of values of the given type."""
129 return {
130 'status': self.statuses,
131 'algorithm': self.algorithms,
132 'ecc_curve': self.ecc_curves,
133 'dh_group': self.dh_groups,
134 'key_type': self.key_types,
135 'key_usage': self.key_usage_flags,
136 }[type_word]
137
Gilles Peskine24827022018-09-25 18:49:23 +0200138 def gather_arguments(self):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200139 """Populate the list of values for macro arguments.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100140
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200141 Call this after parsing all the inputs.
142 """
Gilles Peskine24827022018-09-25 18:49:23 +0200143 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200144 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
Gilles Peskine882e57e2019-04-12 00:12:07 +0200145 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
Gilles Peskine17542082019-01-04 19:46:31 +0100146 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200147 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
Gilles Peskine24827022018-09-25 18:49:23 +0200148 self.arguments_for['curve'] = sorted(self.ecc_curves)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200149 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine24827022018-09-25 18:49:23 +0200150
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200151 @staticmethod
152 def _format_arguments(name, arguments):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200153 """Format a macro call with arguments.."""
Gilles Peskine24827022018-09-25 18:49:23 +0200154 return name + '(' + ', '.join(arguments) + ')'
155
156 def distribute_arguments(self, name):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200157 """Generate macro calls with each tested argument set.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100158
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200159 If name is a macro without arguments, just yield "name".
160 If name is a macro with arguments, yield a series of
161 "name(arg1,...,argN)" where each argument takes each possible
162 value at least once.
163 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200164 try:
165 if name not in self.argspecs:
166 yield name
167 return
168 argspec = self.argspecs[name]
169 if argspec == []:
170 yield name + '()'
171 return
172 argument_lists = [self.arguments_for[arg] for arg in argspec]
173 arguments = [values[0] for values in argument_lists]
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200174 yield self._format_arguments(name, arguments)
Gilles Peskine54f54452019-05-27 18:31:59 +0200175 # Dear Pylint, enumerate won't work here since we're modifying
176 # the array.
177 # pylint: disable=consider-using-enumerate
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200178 for i in range(len(arguments)):
179 for value in argument_lists[i][1:]:
180 arguments[i] = value
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200181 yield self._format_arguments(name, arguments)
Gilles Peskinef96ed662018-10-19 11:29:56 +0200182 arguments[i] = argument_lists[0][0]
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200183 except BaseException as e:
184 raise Exception('distribute_arguments({})'.format(name)) from e
Gilles Peskine24827022018-09-25 18:49:23 +0200185
Gilles Peskine5a994c12019-11-21 16:46:51 +0100186 def generate_expressions(self, names):
187 return itertools.chain(*map(self.distribute_arguments, names))
188
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200189 _argument_split_re = re.compile(r' *, *')
190 @classmethod
191 def _argument_split(cls, arguments):
192 return re.split(cls._argument_split_re, arguments)
193
Gilles Peskine24827022018-09-25 18:49:23 +0200194 # Regex for interesting header lines.
195 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200196 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200197 re.compile(r'#define +' +
Gilles Peskine98a710c2019-11-21 18:58:36 +0100198 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
Gilles Peskine24827022018-09-25 18:49:23 +0200199 r'(?:\(([^\n()]*)\))?')
200 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200201 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200202 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200203 _excluded_names = set([
204 # Macros that provide an alternative way to build the same
205 # algorithm as another macro.
206 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH',
207 'PSA_ALG_FULL_LENGTH_MAC',
208 # Auxiliary macro whose name doesn't fit the usual patterns for
209 # auxiliary macros.
210 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH_CASE',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200211 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200212 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200213 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200214 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200215 if not m:
216 return
217 name = m.group(1)
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100218 self.all_declared.add(name)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200219 if re.search(self._excluded_name_re, name) or \
220 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200221 return
222 dest = self.table_by_prefix.get(m.group(2))
223 if dest is None:
224 return
225 dest.add(name)
226 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200227 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200228
Gilles Peskine49af2d32019-12-06 19:20:13 +0100229 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
Gilles Peskine24827022018-09-25 18:49:23 +0200230 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200231 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskine49af2d32019-12-06 19:20:13 +0100232 with read_file_lines(filename, binary=True) as lines:
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200233 for line in lines:
Gilles Peskine49af2d32019-12-06 19:20:13 +0100234 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
Gilles Peskine24827022018-09-25 18:49:23 +0200235 self.parse_header_line(line)
236
Gilles Peskine49af2d32019-12-06 19:20:13 +0100237 _macro_identifier_re = re.compile(r'[A-Z]\w+')
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100238 def generate_undeclared_names(self, expr):
239 for name in re.findall(self._macro_identifier_re, expr):
240 if name not in self.all_declared:
241 yield name
242
243 def accept_test_case_line(self, function, argument):
244 #pylint: disable=unused-argument
245 undeclared = list(self.generate_undeclared_names(argument))
246 if undeclared:
247 raise Exception('Undeclared names in test case', undeclared)
248 return True
249
Gilles Peskine24827022018-09-25 18:49:23 +0200250 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200251 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100252 sets = []
Gilles Peskine24827022018-09-25 18:49:23 +0200253 if function.endswith('_algorithm'):
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100254 sets.append(self.algorithms)
Gilles Peskine79616682019-11-21 20:08:10 +0100255 if function == 'key_agreement_algorithm' and \
256 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
257 # We only want *raw* key agreement algorithms as such, so
258 # exclude ones that are already chained with a KDF.
259 # Keep the expression as one to test as an algorithm.
260 function = 'other_algorithm'
Gilles Peskine8fa13482019-11-25 17:10:12 +0100261 sets += self.table_by_test_function[function]
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100262 if self.accept_test_case_line(function, argument):
263 for s in sets:
264 s.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200265
266 # Regex matching a *.data line containing a test function call and
267 # its arguments. The actual definition is partly positional, but this
268 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200269 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200270 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200271 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200272 with read_file_lines(filename) as lines:
273 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200274 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200275 if m:
276 self.add_test_case_line(m.group(1), m.group(2))
277
Gilles Peskine84a45812019-11-21 19:50:33 +0100278def gather_inputs(headers, test_suites, inputs_class=Inputs):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200279 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine84a45812019-11-21 19:50:33 +0100280 inputs = inputs_class()
Gilles Peskine24827022018-09-25 18:49:23 +0200281 for header in headers:
282 inputs.parse_header(header)
283 for test_cases in test_suites:
284 inputs.parse_test_cases(test_cases)
285 inputs.gather_arguments()
286 return inputs
287
288def remove_file_if_exists(filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200289 """Remove the specified file, ignoring errors."""
Gilles Peskine24827022018-09-25 18:49:23 +0200290 if not filename:
291 return
292 try:
293 os.remove(filename)
Gilles Peskine54f54452019-05-27 18:31:59 +0200294 except OSError:
Gilles Peskine24827022018-09-25 18:49:23 +0200295 pass
296
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100297def run_c(type_word, expressions, include_path=None, keep_c=False):
Gilles Peskine5a994c12019-11-21 16:46:51 +0100298 """Generate and run a program to print out numerical values for expressions."""
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100299 if include_path is None:
300 include_path = []
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200301 if type_word == 'status':
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100302 cast_to = 'long'
303 printf_format = '%ld'
304 else:
305 cast_to = 'unsigned long'
306 printf_format = '0x%08lx'
Gilles Peskine24827022018-09-25 18:49:23 +0200307 c_name = None
308 exe_name = None
309 try:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200310 c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(type_word),
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100311 suffix='.c',
Gilles Peskine24827022018-09-25 18:49:23 +0200312 dir='programs/psa')
313 exe_suffix = '.exe' if platform.system() == 'Windows' else ''
314 exe_name = c_name[:-2] + exe_suffix
315 remove_file_if_exists(exe_name)
316 c_file = os.fdopen(c_fd, 'w', encoding='ascii')
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100317 c_file.write('/* Generated by test_psa_constant_names.py for {} values */'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200318 .format(type_word))
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100319 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200320#include <stdio.h>
321#include <psa/crypto.h>
322int main(void)
323{
324''')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100325 for expr in expressions:
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100326 c_file.write(' printf("{}\\n", ({}) {});\n'
Gilles Peskine5a994c12019-11-21 16:46:51 +0100327 .format(printf_format, cast_to, expr))
Gilles Peskine24827022018-09-25 18:49:23 +0200328 c_file.write(''' return 0;
329}
330''')
331 c_file.close()
332 cc = os.getenv('CC', 'cc')
333 subprocess.check_call([cc] +
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100334 ['-I' + dir for dir in include_path] +
Gilles Peskine24827022018-09-25 18:49:23 +0200335 ['-o', exe_name, c_name])
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100336 if keep_c:
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200337 sys.stderr.write('List of {} tests kept at {}\n'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200338 .format(type_word, c_name))
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200339 else:
340 os.remove(c_name)
Gilles Peskine24827022018-09-25 18:49:23 +0200341 output = subprocess.check_output([exe_name])
342 return output.decode('ascii').strip().split('\n')
343 finally:
344 remove_file_if_exists(exe_name)
345
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200346NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200347def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200348 """Normalize the C expression so as not to care about trivial differences.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100349
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200350 Currently "trivial differences" means whitespace.
351 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100352 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200353
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100354def collect_values(inputs, type_word, include_path=None, keep_c=False):
Gilles Peskinec2317112019-11-21 17:17:39 +0100355 """Generate expressions using known macro names and calculate their values.
356
357 Return a list of pairs of (expr, value) where expr is an expression and
358 value is a string representation of its integer value.
359 """
360 names = inputs.get_names(type_word)
361 expressions = sorted(inputs.generate_expressions(names))
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100362 values = run_c(type_word, expressions,
363 include_path=include_path, keep_c=keep_c)
Gilles Peskinec2317112019-11-21 17:17:39 +0100364 return expressions, values
365
Gilles Peskine24609332019-11-21 17:44:21 +0100366class Tests:
367 """An object representing tests and their results."""
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100368
Gilles Peskinea5000f12019-11-21 17:51:11 +0100369 Error = namedtuple('Error',
370 ['type', 'expression', 'value', 'output'])
371
Gilles Peskine24609332019-11-21 17:44:21 +0100372 def __init__(self, options):
373 self.options = options
374 self.count = 0
375 self.errors = []
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100376
Gilles Peskine24609332019-11-21 17:44:21 +0100377 def run_one(self, inputs, type_word):
378 """Test psa_constant_names for the specified type.
Gilles Peskine24827022018-09-25 18:49:23 +0200379
Gilles Peskine24609332019-11-21 17:44:21 +0100380 Run the program on the names for this type.
381 Use the inputs to figure out what arguments to pass to macros that
382 take arguments.
383 """
384 expressions, values = collect_values(inputs, type_word,
385 include_path=self.options.include,
386 keep_c=self.options.keep_c)
387 output = subprocess.check_output([self.options.program, type_word] +
388 values)
389 outputs = output.decode('ascii').strip().split('\n')
390 self.count += len(expressions)
391 for expr, value, output in zip(expressions, values, outputs):
392 if normalize(expr) != normalize(output):
Gilles Peskinea5000f12019-11-21 17:51:11 +0100393 self.errors.append(self.Error(type=type_word,
394 expression=expr,
395 value=value,
396 output=output))
Gilles Peskine24827022018-09-25 18:49:23 +0200397
Gilles Peskine24609332019-11-21 17:44:21 +0100398 def run_all(self, inputs):
399 """Run psa_constant_names on all the gathered inputs."""
400 for type_word in ['status', 'algorithm', 'ecc_curve', 'dh_group',
401 'key_type', 'key_usage']:
402 self.run_one(inputs, type_word)
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100403
Gilles Peskine24609332019-11-21 17:44:21 +0100404 def report(self, out):
405 """Describe each case where the output is not as expected.
406
407 Write the errors to ``out``.
408 Also write a total.
409 """
Gilles Peskinea5000f12019-11-21 17:51:11 +0100410 for error in self.errors:
Gilles Peskine24609332019-11-21 17:44:21 +0100411 out.write('For {} "{}", got "{}" (value: {})\n'
Gilles Peskinea5000f12019-11-21 17:51:11 +0100412 .format(error.type, error.expression,
413 error.output, error.value))
Gilles Peskine24609332019-11-21 17:44:21 +0100414 out.write('{} test cases'.format(self.count))
415 if self.errors:
416 out.write(', {} FAIL\n'.format(len(self.errors)))
417 else:
418 out.write(' PASS\n')
Gilles Peskine24827022018-09-25 18:49:23 +0200419
Gilles Peskine69f93b52019-11-21 16:49:50 +0100420HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
421TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
422
Gilles Peskine54f54452019-05-27 18:31:59 +0200423def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200424 parser = argparse.ArgumentParser(description=globals()['__doc__'])
425 parser.add_argument('--include', '-I',
426 action='append', default=['include'],
427 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200428 parser.add_argument('--keep-c',
429 action='store_true', dest='keep_c', default=False,
430 help='Keep the intermediate C file')
431 parser.add_argument('--no-keep-c',
432 action='store_false', dest='keep_c',
433 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100434 parser.add_argument('--program',
435 default='programs/psa/psa_constant_names',
436 help='Program to test')
Gilles Peskine24827022018-09-25 18:49:23 +0200437 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100438 headers = [os.path.join(options.include[0], h) for h in HEADERS]
439 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24609332019-11-21 17:44:21 +0100440 tests = Tests(options)
441 tests.run_all(inputs)
442 tests.report(sys.stdout)
443 if tests.errors:
Gilles Peskine24827022018-09-25 18:49:23 +0200444 exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200445
446if __name__ == '__main__':
447 main()