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