blob: 4497dad57a0b6a1b07a5116e1e3be268689c53a8 [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úti700ee442020-05-26 00:33:31 +020010# Copyright (C) 2018-2020, Arm Limited, All Rights Reserved
11#
12# This file is part of Mbed TLS (https://tls.mbed.org)
13
Gilles Peskine24827022018-09-25 18:49:23 +020014import argparse
Gilles Peskinea5000f12019-11-21 17:51:11 +010015from collections import namedtuple
Gilles Peskine24827022018-09-25 18:49:23 +020016import itertools
17import os
18import platform
19import re
20import subprocess
21import sys
22import tempfile
23
Gilles Peskinea0a315c2018-10-19 11:27:10 +020024class ReadFileLineException(Exception):
25 def __init__(self, filename, line_number):
26 message = 'in {} at {}'.format(filename, line_number)
27 super(ReadFileLineException, self).__init__(message)
28 self.filename = filename
29 self.line_number = line_number
30
31class read_file_lines:
Gilles Peskine54f54452019-05-27 18:31:59 +020032 # Dear Pylint, conventionally, a context manager class name is lowercase.
33 # pylint: disable=invalid-name,too-few-public-methods
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020034 """Context manager to read a text file line by line.
35
36 ```
37 with read_file_lines(filename) as lines:
38 for line in lines:
39 process(line)
40 ```
41 is equivalent to
42 ```
43 with open(filename, 'r') as input_file:
44 for line in input_file:
45 process(line)
46 ```
47 except that if process(line) raises an exception, then the read_file_lines
48 snippet annotates the exception with the file name and line number.
49 """
Gilles Peskine49af2d32019-12-06 19:20:13 +010050 def __init__(self, filename, binary=False):
Gilles Peskinea0a315c2018-10-19 11:27:10 +020051 self.filename = filename
52 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +020053 self.generator = None
Gilles Peskine49af2d32019-12-06 19:20:13 +010054 self.binary = binary
Gilles Peskinea0a315c2018-10-19 11:27:10 +020055 def __enter__(self):
Gilles Peskine49af2d32019-12-06 19:20:13 +010056 self.generator = enumerate(open(self.filename,
57 'rb' if self.binary else 'r'))
Gilles Peskinea0a315c2018-10-19 11:27:10 +020058 return self
59 def __iter__(self):
60 for line_number, content in self.generator:
61 self.line_number = line_number
62 yield content
63 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020064 def __exit__(self, exc_type, exc_value, exc_traceback):
65 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +020066 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020067 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +020068
Gilles Peskine24827022018-09-25 18:49:23 +020069class Inputs:
Gilles Peskine8c8694c2019-11-21 19:22:45 +010070 # pylint: disable=too-many-instance-attributes
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020071 """Accumulate information about macros to test.
Gilles Peskine4408dfd2019-11-21 17:16:21 +010072
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020073 This includes macro names as well as information about their arguments
74 when applicable.
75 """
76
Gilles Peskine24827022018-09-25 18:49:23 +020077 def __init__(self):
Gilles Peskine2bcfc712019-11-21 19:49:26 +010078 self.all_declared = set()
Gilles Peskine24827022018-09-25 18:49:23 +020079 # Sets of names per type
80 self.statuses = set(['PSA_SUCCESS'])
81 self.algorithms = set(['0xffffffff'])
Gilles Peskinef65ed6f2019-12-04 17:18:41 +010082 self.ecc_curves = set(['0xff'])
83 self.dh_groups = set(['0xff'])
84 self.key_types = set(['0xffff'])
Gilles Peskine24827022018-09-25 18:49:23 +020085 self.key_usage_flags = set(['0x80000000'])
Gilles Peskine434899f2018-10-19 11:30:26 +020086 # Hard-coded value for unknown algorithms
Darryl Green61b7f612019-02-04 16:00:21 +000087 self.hash_algorithms = set(['0x010000fe'])
Gilles Peskine434899f2018-10-19 11:30:26 +020088 self.mac_algorithms = set(['0x02ff00ff'])
Gilles Peskine882e57e2019-04-12 00:12:07 +020089 self.ka_algorithms = set(['0x30fc0000'])
90 self.kdf_algorithms = set(['0x200000ff'])
Gilles Peskine434899f2018-10-19 11:30:26 +020091 # For AEAD algorithms, the only variability is over the tag length,
92 # and this only applies to known algorithms, so don't test an
93 # unknown algorithm.
94 self.aead_algorithms = set()
Gilles Peskine24827022018-09-25 18:49:23 +020095 # Identifier prefixes
96 self.table_by_prefix = {
97 'ERROR': self.statuses,
98 'ALG': self.algorithms,
Gilles Peskine98a710c2019-11-21 18:58:36 +010099 'ECC_CURVE': self.ecc_curves,
100 'DH_GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +0200101 'KEY_TYPE': self.key_types,
102 'KEY_USAGE': self.key_usage_flags,
103 }
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100104 # Test functions
105 self.table_by_test_function = {
Gilles Peskine8fa13482019-11-25 17:10:12 +0100106 # Any function ending in _algorithm also gets added to
107 # self.algorithms.
108 'key_type': [self.key_types],
Gilles Peskinef8210f22019-12-02 17:26:44 +0100109 'block_cipher_key_type': [self.key_types],
110 'stream_cipher_key_type': [self.key_types],
Gilles Peskine228abc52019-12-03 17:24:19 +0100111 'ecc_key_family': [self.ecc_curves],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100112 'ecc_key_types': [self.ecc_curves],
Gilles Peskine228abc52019-12-03 17:24:19 +0100113 'dh_key_family': [self.dh_groups],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100114 'dh_key_types': [self.dh_groups],
115 'hash_algorithm': [self.hash_algorithms],
116 'mac_algorithm': [self.mac_algorithms],
117 'cipher_algorithm': [],
118 'hmac_algorithm': [self.mac_algorithms],
119 'aead_algorithm': [self.aead_algorithms],
120 'key_derivation_algorithm': [self.kdf_algorithms],
121 'key_agreement_algorithm': [self.ka_algorithms],
122 'asymmetric_signature_algorithm': [],
123 'asymmetric_signature_wildcard': [self.algorithms],
124 'asymmetric_encryption_algorithm': [],
125 'other_algorithm': [],
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100126 }
Gilles Peskine24827022018-09-25 18:49:23 +0200127 # macro name -> list of argument names
128 self.argspecs = {}
129 # argument name -> list of values
Gilles Peskine434899f2018-10-19 11:30:26 +0200130 self.arguments_for = {
131 'mac_length': ['1', '63'],
132 'tag_length': ['1', '63'],
133 }
Gilles Peskine24827022018-09-25 18:49:23 +0200134
Gilles Peskineffe2d6e2019-11-21 17:17:01 +0100135 def get_names(self, type_word):
136 """Return the set of known names of values of the given type."""
137 return {
138 'status': self.statuses,
139 'algorithm': self.algorithms,
140 'ecc_curve': self.ecc_curves,
141 'dh_group': self.dh_groups,
142 'key_type': self.key_types,
143 'key_usage': self.key_usage_flags,
144 }[type_word]
145
Gilles Peskine24827022018-09-25 18:49:23 +0200146 def gather_arguments(self):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200147 """Populate the list of values for macro arguments.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100148
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200149 Call this after parsing all the inputs.
150 """
Gilles Peskine24827022018-09-25 18:49:23 +0200151 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200152 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
Gilles Peskine882e57e2019-04-12 00:12:07 +0200153 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
Gilles Peskine17542082019-01-04 19:46:31 +0100154 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200155 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
Gilles Peskine24827022018-09-25 18:49:23 +0200156 self.arguments_for['curve'] = sorted(self.ecc_curves)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200157 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine24827022018-09-25 18:49:23 +0200158
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200159 @staticmethod
160 def _format_arguments(name, arguments):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200161 """Format a macro call with arguments.."""
Gilles Peskine24827022018-09-25 18:49:23 +0200162 return name + '(' + ', '.join(arguments) + ')'
163
164 def distribute_arguments(self, name):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200165 """Generate macro calls with each tested argument set.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100166
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200167 If name is a macro without arguments, just yield "name".
168 If name is a macro with arguments, yield a series of
169 "name(arg1,...,argN)" where each argument takes each possible
170 value at least once.
171 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200172 try:
173 if name not in self.argspecs:
174 yield name
175 return
176 argspec = self.argspecs[name]
177 if argspec == []:
178 yield name + '()'
179 return
180 argument_lists = [self.arguments_for[arg] for arg in argspec]
181 arguments = [values[0] for values in argument_lists]
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200182 yield self._format_arguments(name, arguments)
Gilles Peskine54f54452019-05-27 18:31:59 +0200183 # Dear Pylint, enumerate won't work here since we're modifying
184 # the array.
185 # pylint: disable=consider-using-enumerate
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200186 for i in range(len(arguments)):
187 for value in argument_lists[i][1:]:
188 arguments[i] = value
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200189 yield self._format_arguments(name, arguments)
Gilles Peskinef96ed662018-10-19 11:29:56 +0200190 arguments[i] = argument_lists[0][0]
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200191 except BaseException as e:
192 raise Exception('distribute_arguments({})'.format(name)) from e
Gilles Peskine24827022018-09-25 18:49:23 +0200193
Gilles Peskine5a994c12019-11-21 16:46:51 +0100194 def generate_expressions(self, names):
195 return itertools.chain(*map(self.distribute_arguments, names))
196
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200197 _argument_split_re = re.compile(r' *, *')
198 @classmethod
199 def _argument_split(cls, arguments):
200 return re.split(cls._argument_split_re, arguments)
201
Gilles Peskine24827022018-09-25 18:49:23 +0200202 # Regex for interesting header lines.
203 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200204 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200205 re.compile(r'#define +' +
Gilles Peskine98a710c2019-11-21 18:58:36 +0100206 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
Gilles Peskine24827022018-09-25 18:49:23 +0200207 r'(?:\(([^\n()]*)\))?')
208 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200209 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200210 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200211 _excluded_names = set([
212 # Macros that provide an alternative way to build the same
213 # algorithm as another macro.
214 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH',
215 'PSA_ALG_FULL_LENGTH_MAC',
216 # Auxiliary macro whose name doesn't fit the usual patterns for
217 # auxiliary macros.
218 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH_CASE',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200219 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200220 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200221 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200222 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200223 if not m:
224 return
225 name = m.group(1)
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100226 self.all_declared.add(name)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200227 if re.search(self._excluded_name_re, name) or \
228 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200229 return
230 dest = self.table_by_prefix.get(m.group(2))
231 if dest is None:
232 return
233 dest.add(name)
234 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200235 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200236
Gilles Peskine49af2d32019-12-06 19:20:13 +0100237 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
Gilles Peskine24827022018-09-25 18:49:23 +0200238 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200239 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskine49af2d32019-12-06 19:20:13 +0100240 with read_file_lines(filename, binary=True) as lines:
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200241 for line in lines:
Gilles Peskine49af2d32019-12-06 19:20:13 +0100242 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
Gilles Peskine24827022018-09-25 18:49:23 +0200243 self.parse_header_line(line)
244
Gilles Peskine49af2d32019-12-06 19:20:13 +0100245 _macro_identifier_re = re.compile(r'[A-Z]\w+')
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100246 def generate_undeclared_names(self, expr):
247 for name in re.findall(self._macro_identifier_re, expr):
248 if name not in self.all_declared:
249 yield name
250
251 def accept_test_case_line(self, function, argument):
252 #pylint: disable=unused-argument
253 undeclared = list(self.generate_undeclared_names(argument))
254 if undeclared:
255 raise Exception('Undeclared names in test case', undeclared)
256 return True
257
Gilles Peskine24827022018-09-25 18:49:23 +0200258 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200259 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100260 sets = []
Gilles Peskine24827022018-09-25 18:49:23 +0200261 if function.endswith('_algorithm'):
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100262 sets.append(self.algorithms)
Gilles Peskine79616682019-11-21 20:08:10 +0100263 if function == 'key_agreement_algorithm' and \
264 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
265 # We only want *raw* key agreement algorithms as such, so
266 # exclude ones that are already chained with a KDF.
267 # Keep the expression as one to test as an algorithm.
268 function = 'other_algorithm'
Gilles Peskine8fa13482019-11-25 17:10:12 +0100269 sets += self.table_by_test_function[function]
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100270 if self.accept_test_case_line(function, argument):
271 for s in sets:
272 s.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200273
274 # Regex matching a *.data line containing a test function call and
275 # its arguments. The actual definition is partly positional, but this
276 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200277 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200278 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200279 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200280 with read_file_lines(filename) as lines:
281 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200282 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200283 if m:
284 self.add_test_case_line(m.group(1), m.group(2))
285
Gilles Peskine84a45812019-11-21 19:50:33 +0100286def gather_inputs(headers, test_suites, inputs_class=Inputs):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200287 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine84a45812019-11-21 19:50:33 +0100288 inputs = inputs_class()
Gilles Peskine24827022018-09-25 18:49:23 +0200289 for header in headers:
290 inputs.parse_header(header)
291 for test_cases in test_suites:
292 inputs.parse_test_cases(test_cases)
293 inputs.gather_arguments()
294 return inputs
295
296def remove_file_if_exists(filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200297 """Remove the specified file, ignoring errors."""
Gilles Peskine24827022018-09-25 18:49:23 +0200298 if not filename:
299 return
300 try:
301 os.remove(filename)
Gilles Peskine54f54452019-05-27 18:31:59 +0200302 except OSError:
Gilles Peskine24827022018-09-25 18:49:23 +0200303 pass
304
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100305def run_c(type_word, expressions, include_path=None, keep_c=False):
Gilles Peskine5a994c12019-11-21 16:46:51 +0100306 """Generate and run a program to print out numerical values for expressions."""
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100307 if include_path is None:
308 include_path = []
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200309 if type_word == 'status':
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100310 cast_to = 'long'
311 printf_format = '%ld'
312 else:
313 cast_to = 'unsigned long'
314 printf_format = '0x%08lx'
Gilles Peskine24827022018-09-25 18:49:23 +0200315 c_name = None
316 exe_name = None
317 try:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200318 c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(type_word),
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100319 suffix='.c',
Gilles Peskine24827022018-09-25 18:49:23 +0200320 dir='programs/psa')
321 exe_suffix = '.exe' if platform.system() == 'Windows' else ''
322 exe_name = c_name[:-2] + exe_suffix
323 remove_file_if_exists(exe_name)
324 c_file = os.fdopen(c_fd, 'w', encoding='ascii')
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100325 c_file.write('/* Generated by test_psa_constant_names.py for {} values */'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200326 .format(type_word))
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100327 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200328#include <stdio.h>
329#include <psa/crypto.h>
330int main(void)
331{
332''')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100333 for expr in expressions:
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100334 c_file.write(' printf("{}\\n", ({}) {});\n'
Gilles Peskine5a994c12019-11-21 16:46:51 +0100335 .format(printf_format, cast_to, expr))
Gilles Peskine24827022018-09-25 18:49:23 +0200336 c_file.write(''' return 0;
337}
338''')
339 c_file.close()
340 cc = os.getenv('CC', 'cc')
341 subprocess.check_call([cc] +
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100342 ['-I' + dir for dir in include_path] +
Gilles Peskine24827022018-09-25 18:49:23 +0200343 ['-o', exe_name, c_name])
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100344 if keep_c:
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200345 sys.stderr.write('List of {} tests kept at {}\n'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200346 .format(type_word, c_name))
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200347 else:
348 os.remove(c_name)
Gilles Peskine24827022018-09-25 18:49:23 +0200349 output = subprocess.check_output([exe_name])
350 return output.decode('ascii').strip().split('\n')
351 finally:
352 remove_file_if_exists(exe_name)
353
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200354NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200355def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200356 """Normalize the C expression so as not to care about trivial differences.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100357
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200358 Currently "trivial differences" means whitespace.
359 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100360 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200361
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100362def collect_values(inputs, type_word, include_path=None, keep_c=False):
Gilles Peskinec2317112019-11-21 17:17:39 +0100363 """Generate expressions using known macro names and calculate their values.
364
365 Return a list of pairs of (expr, value) where expr is an expression and
366 value is a string representation of its integer value.
367 """
368 names = inputs.get_names(type_word)
369 expressions = sorted(inputs.generate_expressions(names))
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100370 values = run_c(type_word, expressions,
371 include_path=include_path, keep_c=keep_c)
Gilles Peskinec2317112019-11-21 17:17:39 +0100372 return expressions, values
373
Gilles Peskine24609332019-11-21 17:44:21 +0100374class Tests:
375 """An object representing tests and their results."""
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100376
Gilles Peskinea5000f12019-11-21 17:51:11 +0100377 Error = namedtuple('Error',
378 ['type', 'expression', 'value', 'output'])
379
Gilles Peskine24609332019-11-21 17:44:21 +0100380 def __init__(self, options):
381 self.options = options
382 self.count = 0
383 self.errors = []
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100384
Gilles Peskine24609332019-11-21 17:44:21 +0100385 def run_one(self, inputs, type_word):
386 """Test psa_constant_names for the specified type.
Gilles Peskine24827022018-09-25 18:49:23 +0200387
Gilles Peskine24609332019-11-21 17:44:21 +0100388 Run the program on the names for this type.
389 Use the inputs to figure out what arguments to pass to macros that
390 take arguments.
391 """
392 expressions, values = collect_values(inputs, type_word,
393 include_path=self.options.include,
394 keep_c=self.options.keep_c)
395 output = subprocess.check_output([self.options.program, type_word] +
396 values)
397 outputs = output.decode('ascii').strip().split('\n')
398 self.count += len(expressions)
399 for expr, value, output in zip(expressions, values, outputs):
Gilles Peskine32558482019-12-03 19:03:35 +0100400 if self.options.show:
401 sys.stdout.write('{} {}\t{}\n'.format(type_word, value, output))
Gilles Peskine24609332019-11-21 17:44:21 +0100402 if normalize(expr) != normalize(output):
Gilles Peskinea5000f12019-11-21 17:51:11 +0100403 self.errors.append(self.Error(type=type_word,
404 expression=expr,
405 value=value,
406 output=output))
Gilles Peskine24827022018-09-25 18:49:23 +0200407
Gilles Peskine24609332019-11-21 17:44:21 +0100408 def run_all(self, inputs):
409 """Run psa_constant_names on all the gathered inputs."""
410 for type_word in ['status', 'algorithm', 'ecc_curve', 'dh_group',
411 'key_type', 'key_usage']:
412 self.run_one(inputs, type_word)
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100413
Gilles Peskine24609332019-11-21 17:44:21 +0100414 def report(self, out):
415 """Describe each case where the output is not as expected.
416
417 Write the errors to ``out``.
418 Also write a total.
419 """
Gilles Peskinea5000f12019-11-21 17:51:11 +0100420 for error in self.errors:
Gilles Peskine24609332019-11-21 17:44:21 +0100421 out.write('For {} "{}", got "{}" (value: {})\n'
Gilles Peskinea5000f12019-11-21 17:51:11 +0100422 .format(error.type, error.expression,
423 error.output, error.value))
Gilles Peskine24609332019-11-21 17:44:21 +0100424 out.write('{} test cases'.format(self.count))
425 if self.errors:
426 out.write(', {} FAIL\n'.format(len(self.errors)))
427 else:
428 out.write(' PASS\n')
Gilles Peskine24827022018-09-25 18:49:23 +0200429
Gilles Peskine69f93b52019-11-21 16:49:50 +0100430HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
431TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
432
Gilles Peskine54f54452019-05-27 18:31:59 +0200433def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200434 parser = argparse.ArgumentParser(description=globals()['__doc__'])
435 parser.add_argument('--include', '-I',
436 action='append', default=['include'],
437 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200438 parser.add_argument('--keep-c',
439 action='store_true', dest='keep_c', default=False,
440 help='Keep the intermediate C file')
441 parser.add_argument('--no-keep-c',
442 action='store_false', dest='keep_c',
443 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100444 parser.add_argument('--program',
445 default='programs/psa/psa_constant_names',
446 help='Program to test')
Gilles Peskine32558482019-12-03 19:03:35 +0100447 parser.add_argument('--show',
448 action='store_true',
449 help='Keep the intermediate C file')
450 parser.add_argument('--no-show',
451 action='store_false', dest='show',
452 help='Don\'t show tested values (default)')
Gilles Peskine24827022018-09-25 18:49:23 +0200453 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100454 headers = [os.path.join(options.include[0], h) for h in HEADERS]
455 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24609332019-11-21 17:44:21 +0100456 tests = Tests(options)
457 tests.run_all(inputs)
458 tests.report(sys.stdout)
459 if tests.errors:
Gilles Peskine8b022352020-03-24 18:36:56 +0100460 sys.exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200461
462if __name__ == '__main__':
463 main()