1#!/usr/bin/env python3
2
3# Copyright The Mbed TLS Contributors
4# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
5
6"""
7Test Mbed TLS with a subset of algorithms.
8
9This script can be divided into several steps:
10
11First, include/mbedtls/mbedtls_config.h or a different config file passed
12in the arguments is parsed to extract any configuration options (using config.py).
13
14Then, test domains (groups of jobs, tests) are built based on predefined data
15collected in the DomainData class. Here, each domain has five major traits:
16- domain name, can be used to run only specific tests via command-line;
17- configuration building method, described in detail below;
18- list of symbols passed to the configuration building method;
19- commands to be run on each job (only build, build and test, or any other custom);
20- optional list of symbols to be excluded from testing.
21
22The configuration building method can be one of the three following:
23
24- ComplementaryDomain - build a job for each passed symbol by disabling a single
25  symbol and its reverse dependencies (defined in REVERSE_DEPENDENCIES);
26
27- ExclusiveDomain - build a job where, for each passed symbol, only this particular
28  one is defined and other symbols from the list are unset. For each job look for
29  any non-standard symbols to set/unset in EXCLUSIVE_GROUPS. These are usually not
30  direct dependencies, but rather non-trivial results of other configs missing. Then
31  look for any unset symbols and handle their reverse dependencies.
32  Examples of EXCLUSIVE_GROUPS usage:
33  - MBEDTLS_SHA512_C job turns off all hashes except SHA512. MBEDTLS_SSL_COOKIE_C
34    requires either SHA256 or SHA384 to work, so it also has to be disabled.
35    This is not a dependency on SHA512_C, but a result of an exclusive domain
36    config building method. Relevant field:
37    'MBEDTLS_SHA512_C': ['-MBEDTLS_SSL_COOKIE_C'],
38
39- DualDomain - combination of the two above - both complementary and exclusive domain
40  job generation code will be run. Currently only used for hashes.
41
42Lastly, the collected jobs are executed and (optionally) tested, with
43error reporting and coloring as configured in options. Each test starts with
44a full config without a couple of slowing down or unnecessary options
45(see set_reference_config), then the specific job config is derived.
46"""
47import argparse
48import os
49import re
50import shutil
51import subprocess
52import sys
53import traceback
54from typing import Union
55
56# Add the Mbed TLS Python library directory to the module search path
57import scripts_path # pylint: disable=unused-import
58import config
59
60class Colors: # pylint: disable=too-few-public-methods
61    """Minimalistic support for colored output.
62Each field of an object of this class is either None if colored output
63is not possible or not desired, or a pair of strings (start, stop) such
64that outputting start switches the text color to the desired color and
65stop switches the text color back to the default."""
66    red = None
67    green = None
68    cyan = None
69    bold_red = None
70    bold_green = None
71    def __init__(self, options=None):
72        """Initialize color profile according to passed options."""
73        if not options or options.color in ['no', 'never']:
74            want_color = False
75        elif options.color in ['yes', 'always']:
76            want_color = True
77        else:
78            want_color = sys.stderr.isatty()
79        if want_color:
80            # Assume ANSI compatible terminal
81            normal = '\033[0m'
82            self.red = ('\033[31m', normal)
83            self.green = ('\033[32m', normal)
84            self.cyan = ('\033[36m', normal)
85            self.bold_red = ('\033[1;31m', normal)
86            self.bold_green = ('\033[1;32m', normal)
87NO_COLORS = Colors(None)
88
89def log_line(text, prefix='depends.py:', suffix='', color=None):
90    """Print a status message."""
91    if color is not None:
92        prefix = color[0] + prefix
93        suffix = suffix + color[1]
94    sys.stderr.write(prefix + ' ' + text + suffix + '\n')
95    sys.stderr.flush()
96
97def log_command(cmd):
98    """Print a trace of the specified command.
99cmd is a list of strings: a command name and its arguments."""
100    log_line(' '.join(cmd), prefix='+')
101
102def backup_config(options):
103    """Back up the library configuration file (mbedtls_config.h).
104If the backup file already exists, it is presumed to be the desired backup,
105so don't make another backup."""
106    if os.path.exists(options.config_backup):
107        options.own_backup = False
108    else:
109        options.own_backup = True
110        shutil.copy(options.config, options.config_backup)
111
112def restore_config(options):
113    """Restore the library configuration file (mbedtls_config.h).
114Remove the backup file if it was saved earlier."""
115    if options.own_backup:
116        shutil.move(options.config_backup, options.config)
117    else:
118        shutil.copy(options.config_backup, options.config)
119
120def option_exists(conf, option):
121    return option in conf.settings
122
123def set_config_option_value(conf, option, colors, value: Union[bool, str]):
124    """Set/unset a configuration option, optionally specifying a value.
125value can be either True/False (set/unset config option), or a string,
126which will make a symbol defined with a certain value."""
127    if not option_exists(conf, option):
128        log_line('Symbol {} was not found in {}'.format(option, conf.filename), color=colors.red)
129        return False
130
131    if value is False:
132        log_command(['config.py', 'unset', option])
133        conf.unset(option)
134    elif value is True:
135        log_command(['config.py', 'set', option])
136        conf.set(option)
137    else:
138        log_command(['config.py', 'set', option, value])
139        conf.set(option, value)
140    return True
141
142def set_reference_config(conf, options, colors):
143    """Change the library configuration file (mbedtls_config.h) to the reference state.
144The reference state is the one from which the tested configurations are
145derived."""
146    # Turn off options that are not relevant to the tests and slow them down.
147    log_command(['config.py', 'full'])
148    conf.adapt(config.full_adapter)
149    set_config_option_value(conf, 'MBEDTLS_TEST_HOOKS', colors, False)
150    set_config_option_value(conf, 'MBEDTLS_PSA_CRYPTO_CONFIG', colors, False)
151    if options.unset_use_psa:
152        set_config_option_value(conf, 'MBEDTLS_USE_PSA_CRYPTO', colors, False)
153
154class Job:
155    """A job builds the library in a specific configuration and runs some tests."""
156    def __init__(self, name, config_settings, commands):
157        """Build a job object.
158The job uses the configuration described by config_settings. This is a
159dictionary where the keys are preprocessor symbols and the values are
160booleans or strings. A boolean indicates whether or not to #define the
161symbol. With a string, the symbol is #define'd to that value.
162After setting the configuration, the job runs the programs specified by
163commands. This is a list of lists of strings; each list of string is a
164command name and its arguments and is passed to subprocess.call with
165shell=False."""
166        self.name = name
167        self.config_settings = config_settings
168        self.commands = commands
169
170    def announce(self, colors, what):
171        '''Announce the start or completion of a job.
172If what is None, announce the start of the job.
173If what is True, announce that the job has passed.
174If what is False, announce that the job has failed.'''
175        if what is True:
176            log_line(self.name + ' PASSED', color=colors.green)
177        elif what is False:
178            log_line(self.name + ' FAILED', color=colors.red)
179        else:
180            log_line('starting ' + self.name, color=colors.cyan)
181
182    def configure(self, conf, options, colors):
183        '''Set library configuration options as required for the job.'''
184        set_reference_config(conf, options, colors)
185        for key, value in sorted(self.config_settings.items()):
186            ret = set_config_option_value(conf, key, colors, value)
187            if ret is False:
188                return False
189        return True
190
191    def test(self, options):
192        '''Run the job's build and test commands.
193Return True if all the commands succeed and False otherwise.
194If options.keep_going is false, stop as soon as one command fails. Otherwise
195run all the commands, except that if the first command fails, none of the
196other commands are run (typically, the first command is a build command
197and subsequent commands are tests that cannot run if the build failed).'''
198        built = False
199        success = True
200        for command in self.commands:
201            log_command(command)
202            env = os.environ.copy()
203            if 'MBEDTLS_TEST_CONFIGURATION' in env:
204                env['MBEDTLS_TEST_CONFIGURATION'] += '-' + self.name
205            ret = subprocess.call(command, env=env)
206            if ret != 0:
207                if command[0] not in ['make', options.make_command]:
208                    log_line('*** [{}] Error {}'.format(' '.join(command), ret))
209                if not options.keep_going or not built:
210                    return False
211                success = False
212            built = True
213        return success
214
215# If the configuration option A requires B, make sure that
216# B in REVERSE_DEPENDENCIES[A].
217# All the information here should be contained in check_config.h. This
218# file includes a copy because it changes rarely and it would be a pain
219# to extract automatically.
220REVERSE_DEPENDENCIES = {
221    'MBEDTLS_AES_C': ['MBEDTLS_CTR_DRBG_C',
222                      'MBEDTLS_NIST_KW_C'],
223    'MBEDTLS_CHACHA20_C': ['MBEDTLS_CHACHAPOLY_C'],
224    'MBEDTLS_ECDSA_C': ['MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED',
225                        'MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED'],
226    'MBEDTLS_ECP_C': ['MBEDTLS_ECDSA_C',
227                      'MBEDTLS_ECDH_C',
228                      'MBEDTLS_ECJPAKE_C',
229                      'MBEDTLS_ECP_RESTARTABLE',
230                      'MBEDTLS_PK_PARSE_EC_EXTENDED',
231                      'MBEDTLS_PK_PARSE_EC_COMPRESSED',
232                      'MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
233                      'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED',
234                      'MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED',
235                      'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
236                      'MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED',
237                      'MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED',
238                      'MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL_ENABLED',
239                      'MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_EPHEMERAL_ENABLED'],
240    'MBEDTLS_ECP_DP_SECP256R1_ENABLED': ['MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED'],
241    'MBEDTLS_PKCS1_V21': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
242    'MBEDTLS_PKCS1_V15': ['MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
243                          'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
244                          'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
245                          'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
246    'MBEDTLS_RSA_C': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT',
247                      'MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
248                      'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
249                      'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
250                      'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED',
251                      'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED'],
252    'MBEDTLS_SHA256_C': ['MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED',
253                         'MBEDTLS_ENTROPY_FORCE_SHA256',
254                         'MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_IF_PRESENT',
255                         'MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_ONLY',
256                         'MBEDTLS_LMS_C',
257                         'MBEDTLS_LMS_PRIVATE'],
258    'MBEDTLS_SHA512_C': ['MBEDTLS_SHA512_USE_A64_CRYPTO_IF_PRESENT',
259                         'MBEDTLS_SHA512_USE_A64_CRYPTO_ONLY'],
260    'MBEDTLS_SHA224_C': ['MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED',
261                         'MBEDTLS_ENTROPY_FORCE_SHA256',
262                         'MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_IF_PRESENT',
263                         'MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_ONLY'],
264    'MBEDTLS_X509_RSASSA_PSS_SUPPORT': []
265}
266
267# If an option is tested in an exclusive test, alter the following defines.
268# These are not necessarily dependencies, but just minimal required changes
269# if a given define is the only one enabled from an exclusive group.
270EXCLUSIVE_GROUPS = {
271    'MBEDTLS_SHA512_C': ['-MBEDTLS_SSL_COOKIE_C',
272                         '-MBEDTLS_SSL_TLS_C'],
273    'MBEDTLS_ECP_DP_CURVE448_ENABLED': ['-MBEDTLS_ECDSA_C',
274                                        '-MBEDTLS_ECDSA_DETERMINISTIC',
275                                        '-MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED',
276                                        '-MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
277                                        '-MBEDTLS_ECJPAKE_C',
278                                        '-MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED'],
279    'MBEDTLS_ECP_DP_CURVE25519_ENABLED': ['-MBEDTLS_ECDSA_C',
280                                          '-MBEDTLS_ECDSA_DETERMINISTIC',
281                                          '-MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED',
282                                          '-MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
283                                          '-MBEDTLS_ECJPAKE_C',
284                                          '-MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED'],
285    'MBEDTLS_ARIA_C': ['-MBEDTLS_CMAC_C'],
286    'MBEDTLS_CAMELLIA_C': ['-MBEDTLS_CMAC_C'],
287    'MBEDTLS_CHACHA20_C': ['-MBEDTLS_CMAC_C', '-MBEDTLS_CCM_C', '-MBEDTLS_GCM_C'],
288    'MBEDTLS_DES_C': ['-MBEDTLS_CCM_C',
289                      '-MBEDTLS_GCM_C',
290                      '-MBEDTLS_SSL_TICKET_C',
291                      '-MBEDTLS_SSL_CONTEXT_SERIALIZATION'],
292}
293def handle_exclusive_groups(config_settings, symbol):
294    """For every symbol tested in an exclusive group check if there are other
295defines to be altered. """
296    for dep in EXCLUSIVE_GROUPS.get(symbol, []):
297        unset = dep.startswith('-')
298        dep = dep[1:]
299        config_settings[dep] = not unset
300
301def turn_off_dependencies(config_settings):
302    """For every option turned off config_settings, also turn off what depends on it.
303An option O is turned off if config_settings[O] is False."""
304    for key, value in sorted(config_settings.items()):
305        if value is not False:
306            continue
307        for dep in REVERSE_DEPENDENCIES.get(key, []):
308            config_settings[dep] = False
309
310class BaseDomain: # pylint: disable=too-few-public-methods, unused-argument
311    """A base class for all domains."""
312    def __init__(self, symbols, commands, exclude):
313        """Initialize the jobs container"""
314        self.jobs = []
315
316class ExclusiveDomain(BaseDomain): # pylint: disable=too-few-public-methods
317    """A domain consisting of a set of conceptually-equivalent settings.
318Establish a list of configuration symbols. For each symbol, run a test job
319with this symbol set and the others unset."""
320    def __init__(self, symbols, commands, exclude=None):
321        """Build a domain for the specified list of configuration symbols.
322The domain contains a set of jobs that enable one of the elements
323of symbols and disable the others.
324Each job runs the specified commands.
325If exclude is a regular expression, skip generated jobs whose description
326would match this regular expression."""
327        super().__init__(symbols, commands, exclude)
328        base_config_settings = {}
329        for symbol in symbols:
330            base_config_settings[symbol] = False
331        for symbol in symbols:
332            description = symbol
333            if exclude and re.match(exclude, description):
334                continue
335            config_settings = base_config_settings.copy()
336            config_settings[symbol] = True
337            handle_exclusive_groups(config_settings, symbol)
338            turn_off_dependencies(config_settings)
339            job = Job(description, config_settings, commands)
340            self.jobs.append(job)
341
342class ComplementaryDomain(BaseDomain): # pylint: disable=too-few-public-methods
343    """A domain consisting of a set of loosely-related settings.
344Establish a list of configuration symbols. For each symbol, run a test job
345with this symbol unset.
346If exclude is a regular expression, skip generated jobs whose description
347would match this regular expression."""
348    def __init__(self, symbols, commands, exclude=None):
349        """Build a domain for the specified list of configuration symbols.
350Each job in the domain disables one of the specified symbols.
351Each job runs the specified commands."""
352        super().__init__(symbols, commands, exclude)
353        for symbol in symbols:
354            description = '!' + symbol
355            if exclude and re.match(exclude, description):
356                continue
357            config_settings = {symbol: False}
358            turn_off_dependencies(config_settings)
359            job = Job(description, config_settings, commands)
360            self.jobs.append(job)
361
362class DualDomain(ExclusiveDomain, ComplementaryDomain): # pylint: disable=too-few-public-methods
363    """A domain that contains both the ExclusiveDomain and BaseDomain tests.
364Both parent class __init__ calls are performed in any order and
365each call adds respective jobs. The job array initialization is done once in
366BaseDomain, before the parent __init__ calls."""
367
368class CipherInfo: # pylint: disable=too-few-public-methods
369    """Collect data about cipher.h."""
370    def __init__(self):
371        self.base_symbols = set()
372        with open('include/mbedtls/cipher.h', encoding="utf-8") as fh:
373            for line in fh:
374                m = re.match(r' *MBEDTLS_CIPHER_ID_(\w+),', line)
375                if m and m.group(1) not in ['NONE', 'NULL', '3DES']:
376                    self.base_symbols.add('MBEDTLS_' + m.group(1) + '_C')
377
378class DomainData:
379    """A container for domains and jobs, used to structurize testing."""
380    def config_symbols_matching(self, regexp):
381        """List the mbedtls_config.h settings matching regexp."""
382        return [symbol for symbol in self.all_config_symbols
383                if re.match(regexp, symbol)]
384
385    def __init__(self, options, conf):
386        """Gather data about the library and establish a list of domains to test."""
387        build_command = [options.make_command, 'CFLAGS=-Werror -O2']
388        build_and_test = [build_command, [options.make_command, 'test']]
389        self.all_config_symbols = set(conf.settings.keys())
390        # Find hash modules by name.
391        hash_symbols = self.config_symbols_matching(r'MBEDTLS_(MD|RIPEMD|SHA)[0-9]+_C\Z')
392        # Find elliptic curve enabling macros by name.
393        curve_symbols = self.config_symbols_matching(r'MBEDTLS_ECP_DP_\w+_ENABLED\Z')
394        # Find key exchange enabling macros by name.
395        key_exchange_symbols = self.config_symbols_matching(r'MBEDTLS_KEY_EXCHANGE_\w+_ENABLED\Z')
396        # Find cipher IDs (block permutations and stream ciphers --- chaining
397        # and padding modes are exercised separately) information by parsing
398        # cipher.h, as the information is not readily available in mbedtls_config.h.
399        cipher_info = CipherInfo()
400        # Find block cipher chaining and padding mode enabling macros by name.
401        cipher_chaining_symbols = self.config_symbols_matching(r'MBEDTLS_CIPHER_MODE_\w+\Z')
402        cipher_padding_symbols = self.config_symbols_matching(r'MBEDTLS_CIPHER_PADDING_\w+\Z')
403        self.domains = {
404            # Cipher IDs, chaining modes and padding modes. Run the test suites.
405            'cipher_id': ExclusiveDomain(cipher_info.base_symbols,
406                                         build_and_test),
407            'cipher_chaining': ExclusiveDomain(cipher_chaining_symbols,
408                                               build_and_test),
409            'cipher_padding': ExclusiveDomain(cipher_padding_symbols,
410                                              build_and_test),
411            # Elliptic curves. Run the test suites.
412            'curves': ExclusiveDomain(curve_symbols, build_and_test),
413            # Hash algorithms. Excluding exclusive domains of MD, RIPEMD, SHA1,
414            # SHA224 and SHA384 because MBEDTLS_ENTROPY_C is extensively used
415            # across various modules, but it depends on either SHA256 or SHA512.
416            # As a consequence an "exclusive" test of anything other than SHA256
417            # or SHA512 with MBEDTLS_ENTROPY_C enabled is not possible.
418            'hashes': DualDomain(hash_symbols, build_and_test,
419                                 exclude=r'MBEDTLS_(MD|RIPEMD|SHA1_)' \
420                                          '|MBEDTLS_SHA224_' \
421                                          '|MBEDTLS_SHA384_' \
422                                          '|MBEDTLS_SHA3_'),
423            # Key exchange types.
424            'kex': ExclusiveDomain(key_exchange_symbols, build_and_test),
425            'pkalgs': ComplementaryDomain(['MBEDTLS_ECDSA_C',
426                                           'MBEDTLS_ECP_C',
427                                           'MBEDTLS_PKCS1_V21',
428                                           'MBEDTLS_PKCS1_V15',
429                                           'MBEDTLS_RSA_C',
430                                           'MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
431                                          build_and_test),
432        }
433        self.jobs = {}
434        for domain in self.domains.values():
435            for job in domain.jobs:
436                self.jobs[job.name] = job
437
438    def get_jobs(self, name):
439        """Return the list of jobs identified by the given name.
440A name can either be the name of a domain or the name of one specific job."""
441        if name in self.domains:
442            return sorted(self.domains[name].jobs, key=lambda job: job.name)
443        else:
444            return [self.jobs[name]]
445
446def run(options, job, conf, colors=NO_COLORS):
447    """Run the specified job (a Job instance)."""
448    subprocess.check_call([options.make_command, 'clean'])
449    job.announce(colors, None)
450    if not job.configure(conf, options, colors):
451        job.announce(colors, False)
452        return False
453    conf.write()
454    success = job.test(options)
455    job.announce(colors, success)
456    return success
457
458def run_tests(options, domain_data, conf):
459    """Run the desired jobs.
460domain_data should be a DomainData instance that describes the available
461domains and jobs.
462Run the jobs listed in options.tasks."""
463    if not hasattr(options, 'config_backup'):
464        options.config_backup = options.config + '.bak'
465    colors = Colors(options)
466    jobs = []
467    failures = []
468    successes = []
469    for name in options.tasks:
470        jobs += domain_data.get_jobs(name)
471    backup_config(options)
472    try:
473        for job in jobs:
474            success = run(options, job, conf, colors=colors)
475            if not success:
476                if options.keep_going:
477                    failures.append(job.name)
478                else:
479                    return False
480            else:
481                successes.append(job.name)
482        restore_config(options)
483    except:
484        # Restore the configuration, except in stop-on-error mode if there
485        # was an error, where we leave the failing configuration up for
486        # developer convenience.
487        if options.keep_going:
488            restore_config(options)
489        raise
490    if successes:
491        log_line('{} passed'.format(' '.join(successes)), color=colors.bold_green)
492    if failures:
493        log_line('{} FAILED'.format(' '.join(failures)), color=colors.bold_red)
494        return False
495    else:
496        return True
497
498def main():
499    try:
500        parser = argparse.ArgumentParser(
501            formatter_class=argparse.RawDescriptionHelpFormatter,
502            description=
503            "Test Mbed TLS with a subset of algorithms.\n\n"
504            "Example usage:\n"
505            r"./tests/scripts/depends.py \!MBEDTLS_SHA1_C MBEDTLS_SHA256_C""\n"
506            "./tests/scripts/depends.py MBEDTLS_AES_C hashes\n"
507            "./tests/scripts/depends.py cipher_id cipher_chaining\n")
508        parser.add_argument('--color', metavar='WHEN',
509                            help='Colorize the output (always/auto/never)',
510                            choices=['always', 'auto', 'never'], default='auto')
511        parser.add_argument('-c', '--config', metavar='FILE',
512                            help='Configuration file to modify',
513                            default='include/mbedtls/mbedtls_config.h')
514        parser.add_argument('-C', '--directory', metavar='DIR',
515                            help='Change to this directory before anything else',
516                            default='.')
517        parser.add_argument('-k', '--keep-going',
518                            help='Try all configurations even if some fail (default)',
519                            action='store_true', dest='keep_going', default=True)
520        parser.add_argument('-e', '--no-keep-going',
521                            help='Stop as soon as a configuration fails',
522                            action='store_false', dest='keep_going')
523        parser.add_argument('--list-jobs',
524                            help='List supported jobs and exit',
525                            action='append_const', dest='list', const='jobs')
526        parser.add_argument('--list-domains',
527                            help='List supported domains and exit',
528                            action='append_const', dest='list', const='domains')
529        parser.add_argument('--make-command', metavar='CMD',
530                            help='Command to run instead of make (e.g. gmake)',
531                            action='store', default='make')
532        parser.add_argument('--unset-use-psa',
533                            help='Unset MBEDTLS_USE_PSA_CRYPTO before any test',
534                            action='store_true', dest='unset_use_psa')
535        parser.add_argument('tasks', metavar='TASKS', nargs='*',
536                            help='The domain(s) or job(s) to test (default: all).',
537                            default=True)
538        options = parser.parse_args()
539        os.chdir(options.directory)
540        conf = config.ConfigFile(options.config)
541        domain_data = DomainData(options, conf)
542
543        if options.tasks is True:
544            options.tasks = sorted(domain_data.domains.keys())
545        if options.list:
546            for arg in options.list:
547                for domain_name in sorted(getattr(domain_data, arg).keys()):
548                    print(domain_name)
549            sys.exit(0)
550        else:
551            sys.exit(0 if run_tests(options, domain_data, conf) else 1)
552    except Exception: # pylint: disable=broad-except
553        traceback.print_exc()
554        sys.exit(3)
555
556if __name__ == '__main__':
557    main()
558