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