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