blob: a40a82959f6f1626ca1955f134e5d461e3c202b2 [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
11import itertools
12import os
13import platform
14import re
15import subprocess
16import sys
17import tempfile
18
Gilles Peskinea0a315c2018-10-19 11:27:10 +020019class ReadFileLineException(Exception):
20 def __init__(self, filename, line_number):
21 message = 'in {} at {}'.format(filename, line_number)
22 super(ReadFileLineException, self).__init__(message)
23 self.filename = filename
24 self.line_number = line_number
25
26class read_file_lines:
Gilles Peskine54f54452019-05-27 18:31:59 +020027 # Dear Pylint, conventionally, a context manager class name is lowercase.
28 # pylint: disable=invalid-name,too-few-public-methods
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020029 """Context manager to read a text file line by line.
30
31 ```
32 with read_file_lines(filename) as lines:
33 for line in lines:
34 process(line)
35 ```
36 is equivalent to
37 ```
38 with open(filename, 'r') as input_file:
39 for line in input_file:
40 process(line)
41 ```
42 except that if process(line) raises an exception, then the read_file_lines
43 snippet annotates the exception with the file name and line number.
44 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +020045 def __init__(self, filename):
46 self.filename = filename
47 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +020048 self.generator = None
Gilles Peskinea0a315c2018-10-19 11:27:10 +020049 def __enter__(self):
50 self.generator = enumerate(open(self.filename, 'r'))
51 return self
52 def __iter__(self):
53 for line_number, content in self.generator:
54 self.line_number = line_number
55 yield content
56 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020057 def __exit__(self, exc_type, exc_value, exc_traceback):
58 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +020059 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020060 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +020061
Gilles Peskine24827022018-09-25 18:49:23 +020062class Inputs:
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020063 """Accumulate information about macros to test.
Gilles Peskine4408dfd2019-11-21 17:16:21 +010064
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020065 This includes macro names as well as information about their arguments
66 when applicable.
67 """
68
Gilles Peskine24827022018-09-25 18:49:23 +020069 def __init__(self):
70 # Sets of names per type
71 self.statuses = set(['PSA_SUCCESS'])
72 self.algorithms = set(['0xffffffff'])
73 self.ecc_curves = set(['0xffff'])
Gilles Peskinedcaefae2019-05-16 12:55:35 +020074 self.dh_groups = set(['0xffff'])
Gilles Peskine24827022018-09-25 18:49:23 +020075 self.key_types = set(['0xffffffff'])
76 self.key_usage_flags = set(['0x80000000'])
Gilles Peskine434899f2018-10-19 11:30:26 +020077 # Hard-coded value for unknown algorithms
Darryl Green61b7f612019-02-04 16:00:21 +000078 self.hash_algorithms = set(['0x010000fe'])
Gilles Peskine434899f2018-10-19 11:30:26 +020079 self.mac_algorithms = set(['0x02ff00ff'])
Gilles Peskine882e57e2019-04-12 00:12:07 +020080 self.ka_algorithms = set(['0x30fc0000'])
81 self.kdf_algorithms = set(['0x200000ff'])
Gilles Peskine434899f2018-10-19 11:30:26 +020082 # For AEAD algorithms, the only variability is over the tag length,
83 # and this only applies to known algorithms, so don't test an
84 # unknown algorithm.
85 self.aead_algorithms = set()
Gilles Peskine24827022018-09-25 18:49:23 +020086 # Identifier prefixes
87 self.table_by_prefix = {
88 'ERROR': self.statuses,
89 'ALG': self.algorithms,
90 'CURVE': self.ecc_curves,
Gilles Peskinedcaefae2019-05-16 12:55:35 +020091 'GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +020092 'KEY_TYPE': self.key_types,
93 'KEY_USAGE': self.key_usage_flags,
94 }
95 # macro name -> list of argument names
96 self.argspecs = {}
97 # argument name -> list of values
Gilles Peskine434899f2018-10-19 11:30:26 +020098 self.arguments_for = {
99 'mac_length': ['1', '63'],
100 'tag_length': ['1', '63'],
101 }
Gilles Peskine24827022018-09-25 18:49:23 +0200102
103 def gather_arguments(self):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200104 """Populate the list of values for macro arguments.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100105
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200106 Call this after parsing all the inputs.
107 """
Gilles Peskine24827022018-09-25 18:49:23 +0200108 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200109 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
Gilles Peskine882e57e2019-04-12 00:12:07 +0200110 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
Gilles Peskine17542082019-01-04 19:46:31 +0100111 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200112 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
Gilles Peskine24827022018-09-25 18:49:23 +0200113 self.arguments_for['curve'] = sorted(self.ecc_curves)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200114 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine24827022018-09-25 18:49:23 +0200115
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200116 @staticmethod
117 def _format_arguments(name, arguments):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200118 """Format a macro call with arguments.."""
Gilles Peskine24827022018-09-25 18:49:23 +0200119 return name + '(' + ', '.join(arguments) + ')'
120
121 def distribute_arguments(self, name):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200122 """Generate macro calls with each tested argument set.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100123
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200124 If name is a macro without arguments, just yield "name".
125 If name is a macro with arguments, yield a series of
126 "name(arg1,...,argN)" where each argument takes each possible
127 value at least once.
128 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200129 try:
130 if name not in self.argspecs:
131 yield name
132 return
133 argspec = self.argspecs[name]
134 if argspec == []:
135 yield name + '()'
136 return
137 argument_lists = [self.arguments_for[arg] for arg in argspec]
138 arguments = [values[0] for values in argument_lists]
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200139 yield self._format_arguments(name, arguments)
Gilles Peskine54f54452019-05-27 18:31:59 +0200140 # Dear Pylint, enumerate won't work here since we're modifying
141 # the array.
142 # pylint: disable=consider-using-enumerate
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200143 for i in range(len(arguments)):
144 for value in argument_lists[i][1:]:
145 arguments[i] = value
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200146 yield self._format_arguments(name, arguments)
Gilles Peskinef96ed662018-10-19 11:29:56 +0200147 arguments[i] = argument_lists[0][0]
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200148 except BaseException as e:
149 raise Exception('distribute_arguments({})'.format(name)) from e
Gilles Peskine24827022018-09-25 18:49:23 +0200150
Gilles Peskine5a994c12019-11-21 16:46:51 +0100151 def generate_expressions(self, names):
152 return itertools.chain(*map(self.distribute_arguments, names))
153
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200154 _argument_split_re = re.compile(r' *, *')
155 @classmethod
156 def _argument_split(cls, arguments):
157 return re.split(cls._argument_split_re, arguments)
158
Gilles Peskine24827022018-09-25 18:49:23 +0200159 # Regex for interesting header lines.
160 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200161 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200162 re.compile(r'#define +' +
163 r'(PSA_((?:KEY_)?[A-Z]+)_\w+)' +
164 r'(?:\(([^\n()]*)\))?')
165 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200166 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200167 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200168 _excluded_names = set([
169 # Macros that provide an alternative way to build the same
170 # algorithm as another macro.
171 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH',
172 'PSA_ALG_FULL_LENGTH_MAC',
173 # Auxiliary macro whose name doesn't fit the usual patterns for
174 # auxiliary macros.
175 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH_CASE',
176 # PSA_ALG_ECDH and PSA_ALG_FFDH are excluded for now as the script
177 # currently doesn't support them.
178 'PSA_ALG_ECDH',
179 'PSA_ALG_FFDH',
180 # Deprecated aliases.
181 'PSA_ERROR_UNKNOWN_ERROR',
182 'PSA_ERROR_OCCUPIED_SLOT',
183 'PSA_ERROR_EMPTY_SLOT',
184 'PSA_ERROR_INSUFFICIENT_CAPACITY',
Gilles Peskine19835122019-05-17 12:06:55 +0200185 'PSA_ERROR_TAMPERING_DETECTED',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200186 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200187 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200188 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200189 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200190 if not m:
191 return
192 name = m.group(1)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200193 if re.search(self._excluded_name_re, name) or \
194 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200195 return
196 dest = self.table_by_prefix.get(m.group(2))
197 if dest is None:
198 return
199 dest.add(name)
200 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200201 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200202
203 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200204 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200205 with read_file_lines(filename) as lines:
206 for line in lines:
Gilles Peskine24827022018-09-25 18:49:23 +0200207 self.parse_header_line(line)
208
209 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200210 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine24827022018-09-25 18:49:23 +0200211 if function.endswith('_algorithm'):
Darryl Greenb8fe0682019-02-06 13:21:31 +0000212 # As above, ECDH and FFDH algorithms are excluded for now.
213 # Support for them will be added in the future.
Darryl Greenec079502019-01-29 15:48:00 +0000214 if 'ECDH' in argument or 'FFDH' in argument:
215 return
Gilles Peskine24827022018-09-25 18:49:23 +0200216 self.algorithms.add(argument)
217 if function == 'hash_algorithm':
218 self.hash_algorithms.add(argument)
Gilles Peskine434899f2018-10-19 11:30:26 +0200219 elif function in ['mac_algorithm', 'hmac_algorithm']:
220 self.mac_algorithms.add(argument)
221 elif function == 'aead_algorithm':
222 self.aead_algorithms.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200223 elif function == 'key_type':
224 self.key_types.add(argument)
225 elif function == 'ecc_key_types':
226 self.ecc_curves.add(argument)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200227 elif function == 'dh_key_types':
228 self.dh_groups.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200229
230 # Regex matching a *.data line containing a test function call and
231 # its arguments. The actual definition is partly positional, but this
232 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200233 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200234 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200235 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200236 with read_file_lines(filename) as lines:
237 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200238 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200239 if m:
240 self.add_test_case_line(m.group(1), m.group(2))
241
242def gather_inputs(headers, test_suites):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200243 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine24827022018-09-25 18:49:23 +0200244 inputs = Inputs()
245 for header in headers:
246 inputs.parse_header(header)
247 for test_cases in test_suites:
248 inputs.parse_test_cases(test_cases)
249 inputs.gather_arguments()
250 return inputs
251
252def remove_file_if_exists(filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200253 """Remove the specified file, ignoring errors."""
Gilles Peskine24827022018-09-25 18:49:23 +0200254 if not filename:
255 return
256 try:
257 os.remove(filename)
Gilles Peskine54f54452019-05-27 18:31:59 +0200258 except OSError:
Gilles Peskine24827022018-09-25 18:49:23 +0200259 pass
260
Gilles Peskine5a994c12019-11-21 16:46:51 +0100261def run_c(options, type_word, expressions):
262 """Generate and run a program to print out numerical values for expressions."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200263 if type_word == 'status':
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100264 cast_to = 'long'
265 printf_format = '%ld'
266 else:
267 cast_to = 'unsigned long'
268 printf_format = '0x%08lx'
Gilles Peskine24827022018-09-25 18:49:23 +0200269 c_name = None
270 exe_name = None
271 try:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200272 c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(type_word),
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100273 suffix='.c',
Gilles Peskine24827022018-09-25 18:49:23 +0200274 dir='programs/psa')
275 exe_suffix = '.exe' if platform.system() == 'Windows' else ''
276 exe_name = c_name[:-2] + exe_suffix
277 remove_file_if_exists(exe_name)
278 c_file = os.fdopen(c_fd, 'w', encoding='ascii')
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100279 c_file.write('/* Generated by test_psa_constant_names.py for {} values */'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200280 .format(type_word))
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100281 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200282#include <stdio.h>
283#include <psa/crypto.h>
284int main(void)
285{
286''')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100287 for expr in expressions:
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100288 c_file.write(' printf("{}\\n", ({}) {});\n'
Gilles Peskine5a994c12019-11-21 16:46:51 +0100289 .format(printf_format, cast_to, expr))
Gilles Peskine24827022018-09-25 18:49:23 +0200290 c_file.write(''' return 0;
291}
292''')
293 c_file.close()
294 cc = os.getenv('CC', 'cc')
295 subprocess.check_call([cc] +
296 ['-I' + dir for dir in options.include] +
297 ['-o', exe_name, c_name])
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200298 if options.keep_c:
299 sys.stderr.write('List of {} tests kept at {}\n'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200300 .format(type_word, c_name))
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200301 else:
302 os.remove(c_name)
Gilles Peskine24827022018-09-25 18:49:23 +0200303 output = subprocess.check_output([exe_name])
304 return output.decode('ascii').strip().split('\n')
305 finally:
306 remove_file_if_exists(exe_name)
307
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200308NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200309def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200310 """Normalize the C expression so as not to care about trivial differences.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100311
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200312 Currently "trivial differences" means whitespace.
313 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100314 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200315
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200316def do_test(options, inputs, type_word, names):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200317 """Test psa_constant_names for the specified type.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100318
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200319 Run program on names.
320 Use inputs to figure out what arguments to pass to macros that
321 take arguments.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100322
323 Return ``(count, errors)`` where ``count`` is the number of expressions
324 that have been tested and ``errors`` is the list of errors that were
325 encountered.
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200326 """
Gilles Peskine5a994c12019-11-21 16:46:51 +0100327 expressions = sorted(inputs.generate_expressions(names))
328 values = run_c(options, type_word, expressions)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200329 output = subprocess.check_output([options.program, type_word] + values)
Gilles Peskine24827022018-09-25 18:49:23 +0200330 outputs = output.decode('ascii').strip().split('\n')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100331 errors = [(type_word, expr, value, output)
332 for (expr, value, output) in zip(expressions, values, outputs)
333 if normalize(expr) != normalize(output)]
334 return len(expressions), errors
Gilles Peskine24827022018-09-25 18:49:23 +0200335
336def report_errors(errors):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200337 """Describe each case where the output is not as expected."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200338 for type_word, name, value, output in errors:
Gilles Peskine24827022018-09-25 18:49:23 +0200339 print('For {} "{}", got "{}" (value: {})'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200340 .format(type_word, name, output, value))
Gilles Peskine24827022018-09-25 18:49:23 +0200341
342def run_tests(options, inputs):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200343 """Run psa_constant_names on all the gathered inputs.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100344
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200345 Return a tuple (count, errors) where count is the total number of inputs
346 that were tested and errors is the list of cases where the output was
347 not as expected.
348 """
Gilles Peskine24827022018-09-25 18:49:23 +0200349 count = 0
350 errors = []
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200351 for type_word, names in [('status', inputs.statuses),
352 ('algorithm', inputs.algorithms),
353 ('ecc_curve', inputs.ecc_curves),
354 ('dh_group', inputs.dh_groups),
355 ('key_type', inputs.key_types),
356 ('key_usage', inputs.key_usage_flags)]:
357 c, e = do_test(options, inputs, type_word, names)
Gilles Peskine24827022018-09-25 18:49:23 +0200358 count += c
359 errors += e
360 return count, errors
361
Gilles Peskine69f93b52019-11-21 16:49:50 +0100362HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
363TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
364
Gilles Peskine54f54452019-05-27 18:31:59 +0200365def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200366 parser = argparse.ArgumentParser(description=globals()['__doc__'])
367 parser.add_argument('--include', '-I',
368 action='append', default=['include'],
369 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200370 parser.add_argument('--keep-c',
371 action='store_true', dest='keep_c', default=False,
372 help='Keep the intermediate C file')
373 parser.add_argument('--no-keep-c',
374 action='store_false', dest='keep_c',
375 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100376 parser.add_argument('--program',
377 default='programs/psa/psa_constant_names',
378 help='Program to test')
Gilles Peskine24827022018-09-25 18:49:23 +0200379 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100380 headers = [os.path.join(options.include[0], h) for h in HEADERS]
381 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24827022018-09-25 18:49:23 +0200382 count, errors = run_tests(options, inputs)
383 report_errors(errors)
384 if errors == []:
385 print('{} test cases PASS'.format(count))
386 else:
387 print('{} test cases, {} FAIL'.format(count, len(errors)))
388 exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200389
390if __name__ == '__main__':
391 main()