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            ret = subprocess.call(command)
203            if ret != 0:
204                if command[0] not in ['make', options.make_command]:
205                    log_line('*** [{}] Error {}'.format(' '.join(command), ret))
206                if not options.keep_going or not built:
207                    return False
208                success = False
209            built = True
210        return success
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_PK_PARSE_EC_EXTENDED',
228                      'MBEDTLS_PK_PARSE_EC_COMPRESSED',
229                      'MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
230                      'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED',
231                      'MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED',
232                      'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
233                      'MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED',
234                      'MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED',
235                      'MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL_ENABLED',
236                      'MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_EPHEMERAL_ENABLED'],
237    'MBEDTLS_ECP_DP_SECP256R1_ENABLED': ['MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED'],
238    'MBEDTLS_PKCS1_V21': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
239    'MBEDTLS_PKCS1_V15': ['MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
240                          'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
241                          'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
242                          'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
243    'MBEDTLS_RSA_C': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT',
244                      'MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
245                      'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
246                      'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
247                      'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED',
248                      'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED'],
249    'MBEDTLS_SHA256_C': ['MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED',
250                         'MBEDTLS_ENTROPY_FORCE_SHA256',
251                         'MBEDTLS_SHA256_USE_A64_CRYPTO_IF_PRESENT',
252                         'MBEDTLS_SHA256_USE_A64_CRYPTO_ONLY',
253                         'MBEDTLS_LMS_C',
254                         'MBEDTLS_LMS_PRIVATE'],
255    'MBEDTLS_SHA512_C': ['MBEDTLS_SHA512_USE_A64_CRYPTO_IF_PRESENT',
256                         'MBEDTLS_SHA512_USE_A64_CRYPTO_ONLY'],
257    'MBEDTLS_SHA224_C': ['MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED',
258                         'MBEDTLS_ENTROPY_FORCE_SHA256',
259                         'MBEDTLS_SHA256_USE_A64_CRYPTO_IF_PRESENT',
260                         'MBEDTLS_SHA256_USE_A64_CRYPTO_ONLY'],
261    'MBEDTLS_X509_RSASSA_PSS_SUPPORT': []
262}
263
264# If an option is tested in an exclusive test, alter the following defines.
265# These are not necessarily dependencies, but just minimal required changes
266# if a given define is the only one enabled from an exclusive group.
267EXCLUSIVE_GROUPS = {
268    'MBEDTLS_SHA512_C': ['-MBEDTLS_SSL_COOKIE_C',
269                         '-MBEDTLS_SSL_TLS_C'],
270    'MBEDTLS_ECP_DP_CURVE448_ENABLED': ['-MBEDTLS_ECDSA_C',
271                                        '-MBEDTLS_ECDSA_DETERMINISTIC',
272                                        '-MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED',
273                                        '-MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
274                                        '-MBEDTLS_ECJPAKE_C',
275                                        '-MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED'],
276    'MBEDTLS_ECP_DP_CURVE25519_ENABLED': ['-MBEDTLS_ECDSA_C',
277                                          '-MBEDTLS_ECDSA_DETERMINISTIC',
278                                          '-MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED',
279                                          '-MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
280                                          '-MBEDTLS_ECJPAKE_C',
281                                          '-MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED'],
282    'MBEDTLS_ARIA_C': ['-MBEDTLS_CMAC_C'],
283    'MBEDTLS_CAMELLIA_C': ['-MBEDTLS_CMAC_C'],
284    'MBEDTLS_CHACHA20_C': ['-MBEDTLS_CMAC_C', '-MBEDTLS_CCM_C', '-MBEDTLS_GCM_C'],
285    'MBEDTLS_DES_C': ['-MBEDTLS_CCM_C',
286                      '-MBEDTLS_GCM_C',
287                      '-MBEDTLS_SSL_TICKET_C',
288                      '-MBEDTLS_SSL_CONTEXT_SERIALIZATION'],
289}
290def handle_exclusive_groups(config_settings, symbol):
291    """For every symbol tested in an exclusive group check if there are other
292defines to be altered. """
293    for dep in EXCLUSIVE_GROUPS.get(symbol, []):
294        unset = dep.startswith('-')
295        dep = dep[1:]
296        config_settings[dep] = not unset
297
298def turn_off_dependencies(config_settings):
299    """For every option turned off config_settings, also turn off what depends on it.
300An option O is turned off if config_settings[O] is False."""
301    for key, value in sorted(config_settings.items()):
302        if value is not False:
303            continue
304        for dep in REVERSE_DEPENDENCIES.get(key, []):
305            config_settings[dep] = False
306
307class BaseDomain: # pylint: disable=too-few-public-methods, unused-argument
308    """A base class for all domains."""
309    def __init__(self, symbols, commands, exclude):
310        """Initialize the jobs container"""
311        self.jobs = []
312
313class ExclusiveDomain(BaseDomain): # pylint: disable=too-few-public-methods
314    """A domain consisting of a set of conceptually-equivalent settings.
315Establish a list of configuration symbols. For each symbol, run a test job
316with this symbol set and the others unset."""
317    def __init__(self, symbols, commands, exclude=None):
318        """Build a domain for the specified list of configuration symbols.
319The domain contains a set of jobs that enable one of the elements
320of symbols and disable the others.
321Each job runs the specified commands.
322If exclude is a regular expression, skip generated jobs whose description
323would match this regular expression."""
324        super().__init__(symbols, commands, exclude)
325        base_config_settings = {}
326        for symbol in symbols:
327            base_config_settings[symbol] = False
328        for symbol in symbols:
329            description = symbol
330            if exclude and re.match(exclude, description):
331                continue
332            config_settings = base_config_settings.copy()
333            config_settings[symbol] = True
334            handle_exclusive_groups(config_settings, symbol)
335            turn_off_dependencies(config_settings)
336            job = Job(description, config_settings, commands)
337            self.jobs.append(job)
338
339class ComplementaryDomain(BaseDomain): # pylint: disable=too-few-public-methods
340    """A domain consisting of a set of loosely-related settings.
341Establish a list of configuration symbols. For each symbol, run a test job
342with this symbol unset.
343If exclude is a regular expression, skip generated jobs whose description
344would match this regular expression."""
345    def __init__(self, symbols, commands, exclude=None):
346        """Build a domain for the specified list of configuration symbols.
347Each job in the domain disables one of the specified symbols.
348Each job runs the specified commands."""
349        super().__init__(symbols, commands, exclude)
350        for symbol in symbols:
351            description = '!' + symbol
352            if exclude and re.match(exclude, description):
353                continue
354            config_settings = {symbol: False}
355            turn_off_dependencies(config_settings)
356            job = Job(description, config_settings, commands)
357            self.jobs.append(job)
358
359class DualDomain(ExclusiveDomain, ComplementaryDomain): # pylint: disable=too-few-public-methods
360    """A domain that contains both the ExclusiveDomain and BaseDomain tests.
361Both parent class __init__ calls are performed in any order and
362each call adds respective jobs. The job array initialization is done once in
363BaseDomain, before the parent __init__ calls."""
364
365class CipherInfo: # pylint: disable=too-few-public-methods
366    """Collect data about cipher.h."""
367    def __init__(self):
368        self.base_symbols = set()
369        with open('include/mbedtls/cipher.h', encoding="utf-8") as fh:
370            for line in fh:
371                m = re.match(r' *MBEDTLS_CIPHER_ID_(\w+),', line)
372                if m and m.group(1) not in ['NONE', 'NULL', '3DES']:
373                    self.base_symbols.add('MBEDTLS_' + m.group(1) + '_C')
374
375class DomainData:
376    """A container for domains and jobs, used to structurize testing."""
377    def config_symbols_matching(self, regexp):
378        """List the mbedtls_config.h settings matching regexp."""
379        return [symbol for symbol in self.all_config_symbols
380                if re.match(regexp, symbol)]
381
382    def __init__(self, options, conf):
383        """Gather data about the library and establish a list of domains to test."""
384        build_command = [options.make_command, 'CFLAGS=-Werror']
385        build_and_test = [build_command, [options.make_command, 'test']]
386        self.all_config_symbols = set(conf.settings.keys())
387        # Find hash modules by name.
388        hash_symbols = self.config_symbols_matching(r'MBEDTLS_(MD|RIPEMD|SHA)[0-9]+_C\Z')
389        # Find elliptic curve enabling macros by name.
390        curve_symbols = self.config_symbols_matching(r'MBEDTLS_ECP_DP_\w+_ENABLED\Z')
391        # Find key exchange enabling macros by name.
392        key_exchange_symbols = self.config_symbols_matching(r'MBEDTLS_KEY_EXCHANGE_\w+_ENABLED\Z')
393        # Find cipher IDs (block permutations and stream ciphers --- chaining
394        # and padding modes are exercised separately) information by parsing
395        # cipher.h, as the information is not readily available in mbedtls_config.h.
396        cipher_info = CipherInfo()
397        # Find block cipher chaining and padding mode enabling macros by name.
398        cipher_chaining_symbols = self.config_symbols_matching(r'MBEDTLS_CIPHER_MODE_\w+\Z')
399        cipher_padding_symbols = self.config_symbols_matching(r'MBEDTLS_CIPHER_PADDING_\w+\Z')
400        self.domains = {
401            # Cipher IDs, chaining modes and padding modes. Run the test suites.
402            'cipher_id': ExclusiveDomain(cipher_info.base_symbols,
403                                         build_and_test),
404            'cipher_chaining': ExclusiveDomain(cipher_chaining_symbols,
405                                               build_and_test),
406            'cipher_padding': ExclusiveDomain(cipher_padding_symbols,
407                                              build_and_test),
408            # Elliptic curves. Run the test suites.
409            'curves': ExclusiveDomain(curve_symbols, build_and_test),
410            # Hash algorithms. Excluding exclusive domains of MD, RIPEMD, SHA1,
411            # SHA224 and SHA384 because MBEDTLS_ENTROPY_C is extensively used
412            # across various modules, but it depends on either SHA256 or SHA512.
413            # As a consequence an "exclusive" test of anything other than SHA256
414            # or SHA512 with MBEDTLS_ENTROPY_C enabled is not possible.
415            'hashes': DualDomain(hash_symbols, build_and_test,
416                                 exclude=r'MBEDTLS_(MD|RIPEMD|SHA1_)' \
417                                          '|MBEDTLS_SHA224_' \
418                                          '|MBEDTLS_SHA384_' \
419                                          '|MBEDTLS_SHA3_'),
420            # Key exchange types.
421            'kex': ExclusiveDomain(key_exchange_symbols, build_and_test),
422            'pkalgs': ComplementaryDomain(['MBEDTLS_ECDSA_C',
423                                           'MBEDTLS_ECP_C',
424                                           'MBEDTLS_PKCS1_V21',
425                                           'MBEDTLS_PKCS1_V15',
426                                           'MBEDTLS_RSA_C',
427                                           'MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
428                                          build_and_test),
429        }
430        self.jobs = {}
431        for domain in self.domains.values():
432            for job in domain.jobs:
433                self.jobs[job.name] = job
434
435    def get_jobs(self, name):
436        """Return the list of jobs identified by the given name.
437A name can either be the name of a domain or the name of one specific job."""
438        if name in self.domains:
439            return sorted(self.domains[name].jobs, key=lambda job: job.name)
440        else:
441            return [self.jobs[name]]
442
443def run(options, job, conf, colors=NO_COLORS):
444    """Run the specified job (a Job instance)."""
445    subprocess.check_call([options.make_command, 'clean'])
446    job.announce(colors, None)
447    if not job.configure(conf, options, colors):
448        job.announce(colors, False)
449        return False
450    conf.write()
451    success = job.test(options)
452    job.announce(colors, success)
453    return success
454
455def run_tests(options, domain_data, conf):
456    """Run the desired jobs.
457domain_data should be a DomainData instance that describes the available
458domains and jobs.
459Run the jobs listed in options.tasks."""
460    if not hasattr(options, 'config_backup'):
461        options.config_backup = options.config + '.bak'
462    colors = Colors(options)
463    jobs = []
464    failures = []
465    successes = []
466    for name in options.tasks:
467        jobs += domain_data.get_jobs(name)
468    backup_config(options)
469    try:
470        for job in jobs:
471            success = run(options, job, conf, colors=colors)
472            if not success:
473                if options.keep_going:
474                    failures.append(job.name)
475                else:
476                    return False
477            else:
478                successes.append(job.name)
479        restore_config(options)
480    except:
481        # Restore the configuration, except in stop-on-error mode if there
482        # was an error, where we leave the failing configuration up for
483        # developer convenience.
484        if options.keep_going:
485            restore_config(options)
486        raise
487    if successes:
488        log_line('{} passed'.format(' '.join(successes)), color=colors.bold_green)
489    if failures:
490        log_line('{} FAILED'.format(' '.join(failures)), color=colors.bold_red)
491        return False
492    else:
493        return True
494
495def main():
496    try:
497        parser = argparse.ArgumentParser(
498            formatter_class=argparse.RawDescriptionHelpFormatter,
499            description=
500            "Test Mbed TLS with a subset of algorithms.\n\n"
501            "Example usage:\n"
502            r"./tests/scripts/depends.py \!MBEDTLS_SHA1_C MBEDTLS_SHA256_C""\n"
503            "./tests/scripts/depends.py MBEDTLS_AES_C hashes\n"
504            "./tests/scripts/depends.py cipher_id cipher_chaining\n")
505        parser.add_argument('--color', metavar='WHEN',
506                            help='Colorize the output (always/auto/never)',
507                            choices=['always', 'auto', 'never'], default='auto')
508        parser.add_argument('-c', '--config', metavar='FILE',
509                            help='Configuration file to modify',
510                            default='include/mbedtls/mbedtls_config.h')
511        parser.add_argument('-C', '--directory', metavar='DIR',
512                            help='Change to this directory before anything else',
513                            default='.')
514        parser.add_argument('-k', '--keep-going',
515                            help='Try all configurations even if some fail (default)',
516                            action='store_true', dest='keep_going', default=True)
517        parser.add_argument('-e', '--no-keep-going',
518                            help='Stop as soon as a configuration fails',
519                            action='store_false', dest='keep_going')
520        parser.add_argument('--list-jobs',
521                            help='List supported jobs and exit',
522                            action='append_const', dest='list', const='jobs')
523        parser.add_argument('--list-domains',
524                            help='List supported domains and exit',
525                            action='append_const', dest='list', const='domains')
526        parser.add_argument('--make-command', metavar='CMD',
527                            help='Command to run instead of make (e.g. gmake)',
528                            action='store', default='make')
529        parser.add_argument('--unset-use-psa',
530                            help='Unset MBEDTLS_USE_PSA_CRYPTO before any test',
531                            action='store_true', dest='unset_use_psa')
532        parser.add_argument('tasks', metavar='TASKS', nargs='*',
533                            help='The domain(s) or job(s) to test (default: all).',
534                            default=True)
535        options = parser.parse_args()
536        os.chdir(options.directory)
537        conf = config.ConfigFile(options.config)
538        domain_data = DomainData(options, conf)
539
540        if options.tasks is True:
541            options.tasks = sorted(domain_data.domains.keys())
542        if options.list:
543            for arg in options.list:
544                for domain_name in sorted(getattr(domain_data, arg).keys()):
545                    print(domain_name)
546            sys.exit(0)
547        else:
548            sys.exit(0 if run_tests(options, domain_data, conf) else 1)
549    except Exception: # pylint: disable=broad-except
550        traceback.print_exc()
551        sys.exit(3)
552
553if __name__ == '__main__':
554    main()
555