blob: 433f352757079ce6ec0bbddc715e3347f3a4d651 [file] [log] [blame]
Gilles Peskineb39e3ec2019-01-29 08:50:20 +01001#!/usr/bin/env python3
2
3# Copyright (c) 2018, Arm Limited, All Rights Reserved.
4# SPDX-License-Identifier: Apache-2.0
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18# This file is part of Mbed TLS (https://tls.mbed.org)
19
20"""Test Mbed TLS with a subset of algorithms.
21"""
22
23import argparse
24import os
25import re
26import shutil
27import subprocess
28import sys
29import traceback
30
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +010031class Colors:
32 """Minimalistic support for colored output.
33Each field of an object of this class is either None if colored output
34is not possible or not desired, or a pair of strings (start, stop) such
35that outputting start switches the text color to the desired color and
36stop switches the text color back to the default."""
37 red = None
38 green = None
39 bold_red = None
40 bold_green = None
41 def __init__(self, options=None):
42 if not options or options.color in ['no', 'never']:
43 want_color = False
44 elif options.color in ['yes', 'always']:
45 want_color = True
46 else:
47 want_color = sys.stderr.isatty()
48 if want_color:
49 # Assume ANSI compatible terminal
50 normal = '\033[0m'
51 self.red = ('\033[31m', normal)
52 self.green = ('\033[32m', normal)
53 self.bold_red = ('\033[1;31m', normal)
54 self.bold_green = ('\033[1;32m', normal)
55NO_COLORS = Colors(None)
56
57def log_line(text, prefix='depends.py:', suffix='', color=None):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010058 """Print a status message."""
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +010059 if color != None:
60 prefix = color[0] + prefix
61 suffix = suffix + color[1]
62 sys.stderr.write(prefix + ' ' + text + suffix + '\n')
Gilles Peskine46c82562019-01-29 18:42:55 +010063 sys.stderr.flush()
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010064
Gilles Peskine54aa5c62019-01-29 18:46:34 +010065def log_command(cmd):
66 """Print a trace of the specified command.
67cmd is a list of strings: a command name and its arguments."""
68 log_line(' '.join(cmd), prefix='+')
69
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010070def backup_config(options):
71 """Back up the library configuration file (config.h)."""
72 shutil.copy(options.config, options.config_backup)
73
74def restore_config(options, done=False):
75 """Restore the library configuration file (config.h).
76If done is true, remove the backup file."""
77 if done:
78 shutil.move(options.config_backup, options.config)
79 else:
80 shutil.copy(options.config_backup, options.config)
Gilles Peskine54aa5c62019-01-29 18:46:34 +010081def run_config_pl(options, args):
82 """Run scripts/config.pl with the specified arguments."""
83 cmd = ['scripts/config.pl']
84 if options.config != 'include/mbedtls/config.h':
85 cmd += ['--file', options.config]
86 cmd += args
87 log_command(cmd)
88 subprocess.check_call(cmd)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010089
90class Job:
91 """A job builds the library in a specific configuration and runs some tests."""
92 def __init__(self, name, config_settings, commands):
93 """Build a job object.
94The job uses the configuration described by config_settings. This is a
95dictionary where the keys are preprocessor symbols and the values are
96booleans or strings. A boolean indicates whether or not to #define the
97symbol. With a string, the symbol is #define'd to that value.
98After setting the configuration, the job runs the programs specified by
99commands. This is a list of lists of strings; each list of string is a
100command name and its arguments and is passed to subprocess.call with
101shell=False."""
102 self.name = name
103 self.config_settings = config_settings
104 self.commands = commands
105
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100106 def announce(self, colors, what):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100107 '''Announce the start or completion of a job.
108If what is None, announce the start of the job.
109If what is True, announce that the job has passed.
110If what is False, announce that the job has failed.'''
111 if what is True:
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100112 log_line(self.name + ' PASSED', color=colors.green)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100113 elif what is False:
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100114 log_line(self.name + ' FAILED', color=colors.red)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100115 else:
116 log_line('starting ' + self.name)
117
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100118
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100119 def configure(self, options):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100120 '''Set library configuration options as required for the job.
121config_file_name indicates which file to modify.'''
122 for key, value in sorted(self.config_settings.items()):
123 if value is True:
124 args = ['set', key]
125 elif value is False:
126 args = ['unset', key]
127 else:
128 args = ['set', key, value]
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100129 run_config_pl(options, args)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100130
131 def test(self, options):
132 '''Run the job's build and test commands.
133Return True if all the commands succeed and False otherwise.
134If options.keep_going is false, stop as soon as one command fails. Otherwise
135run all the commands, except that if the first command fails, none of the
136other commands are run (typically, the first command is a build command
137and subsequent commands are tests that cannot run if the build failed).'''
138 built = False
139 success = True
140 for command in self.commands:
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100141 log_command(command)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100142 ret = subprocess.call(command)
143 if ret != 0:
144 if command[0] not in ['make', options.make_command]:
145 log_line('*** [{}] Error {}'.format(' '.join(command), ret))
146 if not options.keep_going or not built:
147 return False
148 success = False
149 built = True
150 return success
151
152# SSL/TLS versions up to 1.1 and corresponding options. These require
153# both MD5 and SHA-1.
154ssl_pre_1_2_dependencies = ['MBEDTLS_SSL_CBC_RECORD_SPLITTING',
155 'MBEDTLS_SSL_PROTO_SSL3',
156 'MBEDTLS_SSL_PROTO_TLS1',
157 'MBEDTLS_SSL_PROTO_TLS1_1']
158
159# If the configuration option A requires B, make sure that
160# B in reverse_dependencies[A].
161reverse_dependencies = {
162 'MBEDTLS_ECDSA_C': ['MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'],
163 'MBEDTLS_ECP_C': ['MBEDTLS_ECDSA_C',
164 'MBEDTLS_ECDH_C',
165 'MBEDTLS_ECJPAKE_C',
166 'MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
167 'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED',
168 'MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED',
169 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
170 'MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'],
171 'MBEDTLS_MD5_C': ssl_pre_1_2_dependencies,
172 'MBEDTLS_PKCS1_V21': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
173 'MBEDTLS_PKCS1_V15': ['MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
174 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
175 'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
176 'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
177 'MBEDTLS_RSA_C': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT',
178 'MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
179 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
180 'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
181 'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
182 'MBEDTLS_SHA1_C': ssl_pre_1_2_dependencies,
183 'MBEDTLS_X509_RSASSA_PSS_SUPPORT': [],
184}
185
186def turn_off_dependencies(config_settings):
187 """For every option turned off config_settings, also turn off what depends on it.
188An option O is turned off if config_settings[O] is False."""
189 for key, value in sorted(config_settings.items()):
190 if value is not False:
191 continue
192 for dep in reverse_dependencies.get(key, []):
193 config_settings[dep] = False
194
195class Domain:
196 """A domain is a set of jobs that all relate to a particular configuration aspect."""
197 pass
198
199class ExclusiveDomain(Domain):
200 """A domain consisting of a set of conceptually-equivalent settings.
201Establish a list of configuration symbols. For each symbol, run a test job
202with this symbol set and the others unset, and a test job with this symbol
203unset and the others set."""
204 def __init__(self, symbols, commands):
205 self.jobs = []
206 for invert in [False, True]:
207 base_config_settings = {}
208 for symbol in symbols:
209 base_config_settings[symbol] = invert
210 for symbol in symbols:
211 description = '!' + symbol if invert else symbol
212 config_settings = base_config_settings.copy()
213 config_settings[symbol] = not invert
214 turn_off_dependencies(config_settings)
215 job = Job(description, config_settings, commands)
216 self.jobs.append(job)
217
218class ComplementaryDomain:
219 """A domain consisting of a set of loosely-related settings.
220Establish a list of configuration symbols. For each symbol, run a test job
221with this symbol unset."""
222 def __init__(self, symbols, commands):
223 self.jobs = []
224 for symbol in symbols:
225 description = '!' + symbol
226 config_settings = {symbol: False}
227 turn_off_dependencies(config_settings)
228 job = Job(description, config_settings, commands)
229 self.jobs.append(job)
230
231class DomainData:
232 """Collect data about the library."""
233 def collect_config_symbols(self, options):
234 """Read the list of settings from config.h.
235Return them in a generator."""
236 with open(options.config) as config_file:
237 rx = re.compile(r'\s*(?://\s*)?#define\s+(\w+)\s*(?:$|/[/*])')
238 for line in config_file:
239 m = re.match(rx, line)
240 if m:
241 yield m.group(1)
242
243 def config_symbols_matching(self, regexp):
244 """List the config.h settings matching regexp."""
245 return [symbol for symbol in self.all_config_symbols
246 if re.match(regexp, symbol)]
247
248 def __init__(self, options):
249 """Gather data about the library and establish a list of domains to test."""
250 build_command = [options.make_command, 'CFLAGS=-Werror']
251 build_and_test = [build_command, [options.make_command, 'test']]
252 self.all_config_symbols = set(self.collect_config_symbols(options))
253 # Find hash modules by name.
254 hash_symbols = self.config_symbols_matching(r'MBEDTLS_(MD|RIPEMD|SHA)[0-9]+_C\Z')
255 # Find elliptic curve enabling macros by name.
256 curve_symbols = self.config_symbols_matching(r'MBEDTLS_ECP_DP_\w+_ENABLED\Z')
257 # Find key exchange enabling macros by name.
258 key_exchange_symbols = self.config_symbols_matching(r'MBEDTLS_KEY_EXCHANGE_\w+_ENABLED\Z')
259 self.domains = {
260 # Elliptic curves. Run the test suites.
261 'curves': ExclusiveDomain(curve_symbols, build_and_test),
262 # Hash algorithms. Exclude configurations with only one
263 # hash which is obsolete. Run the test suites.
264 'hashes': ExclusiveDomain(hash_symbols, build_and_test),
265 # Key exchange types. Just check the build.
266 'kex': ExclusiveDomain(key_exchange_symbols, [build_command]),
267 # Public-key algorithms. Run the test suites.
268 'pkalgs': ComplementaryDomain(['MBEDTLS_ECDSA_C',
269 'MBEDTLS_ECP_C',
270 'MBEDTLS_PKCS1_V21',
271 'MBEDTLS_PKCS1_V15',
272 'MBEDTLS_RSA_C',
273 'MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
274 build_and_test),
275 }
276 self.jobs = {}
277 for domain in self.domains.values():
278 for job in domain.jobs:
279 self.jobs[job.name] = job
280
281 def get_jobs(self, name):
282 """Return the list of jobs identified by the given name.
283A name can either be the name of a domain or the name of one specific job."""
284 if name in self.domains:
285 return sorted(self.domains[name].jobs, key=lambda job: job.name)
286 else:
287 return [self.jobs[name]]
288
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100289def run(options, job, colors=NO_COLORS):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100290 """Run the specified job (a Job instance)."""
291 subprocess.check_call([options.make_command, 'clean'])
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100292 job.announce(colors, None)
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100293 job.configure(options)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100294 success = job.test(options)
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100295 job.announce(colors, success)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100296 return success
297
298def main(options, domain_data):
299 """Run the desired jobs.
300domain_data should be a DomainData instance that describes the available
301domains and jobs.
302Run the jobs listed in options.domains."""
303 if not hasattr(options, 'config_backup'):
304 options.config_backup = options.config + '.bak'
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100305 colors = Colors(options)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100306 jobs = []
307 failures = []
308 successes = []
309 for name in options.domains:
310 jobs += domain_data.get_jobs(name)
311 backup_config(options)
312 try:
313 for job in jobs:
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100314 success = run(options, job, colors=colors)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100315 if not success:
316 if options.keep_going:
317 failures.append(job.name)
318 else:
319 return False
320 else:
321 successes.append(job.name)
322 restore_config(options)
323 finally:
324 if options.keep_going:
325 restore_config(options, True)
326 if failures:
327 if successes:
328 log_line('{} passed; {} FAILED'.format(' '.join(successes),
329 ' '.join(failures)))
330 else:
331 log_line('{} FAILED'.format(' '.join(failures)))
332 return False
333 else:
334 log_line('{} passed'.format(' '.join(successes)))
335 return True
336
337
338if __name__ == '__main__':
339 try:
340 parser = argparse.ArgumentParser(description=__doc__)
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100341 parser.add_argument('--color', metavar='WHEN',
342 help='Colorize the output (always/auto/never)',
343 choices=['always', 'auto', 'never'], default='auto')
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100344 parser.add_argument('-c', '--config', metavar='FILE',
345 help='Configuration file to modify',
346 default='include/mbedtls/config.h')
347 parser.add_argument('-C', '--directory', metavar='DIR',
348 help='Change to this directory before anything else',
349 default='.')
350 parser.add_argument('-k', '--keep-going',
351 help='Try all configurations even if some fail (default)',
352 action='store_true', dest='keep_going', default=True)
353 parser.add_argument('-e', '--no-keep-going',
354 help='Stop as soon as a configuration fails',
355 action='store_false', dest='keep_going')
356 parser.add_argument('--list-jobs',
357 help='List supported jobs and exit',
358 action='append_const', dest='list', const='jobs')
359 parser.add_argument('--list-domains',
360 help='List supported domains and exit',
361 action='append_const', dest='list', const='domains')
362 parser.add_argument('--make-command', metavar='CMD',
363 help='Command to run instead of make (e.g. gmake)',
364 action='store', default='make')
365 parser.add_argument('domains', metavar='DOMAIN', nargs='*',
366 help='The domain(s) to test (default: all)',
367 default=True)
368 options = parser.parse_args()
369 os.chdir(options.directory)
370 domain_data = DomainData(options)
371 if options.domains == True:
372 options.domains = sorted(domain_data.domains.keys())
373 if options.list:
374 for what in options.list:
375 for key in sorted(getattr(domain_data, what).keys()):
376 print(key)
377 exit(0)
378 else:
379 sys.exit(0 if main(options, domain_data) else 1)
380 except SystemExit:
381 raise
382 except:
383 traceback.print_exc()
384 exit(3)