Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # Copyright (c) 2018, 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 | """Test Mbed TLS with a subset of algorithms. |
| 21 | """ |
| 22 | |
| 23 | import argparse |
| 24 | import os |
| 25 | import re |
| 26 | import shutil |
| 27 | import subprocess |
| 28 | import sys |
| 29 | import traceback |
| 30 | |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 31 | class Colors: |
| 32 | """Minimalistic support for colored output. |
| 33 | Each field of an object of this class is either None if colored output |
| 34 | is not possible or not desired, or a pair of strings (start, stop) such |
| 35 | that outputting start switches the text color to the desired color and |
| 36 | stop switches the text color back to the default.""" |
| 37 | red = None |
| 38 | green = None |
| 39 | bold_red = None |
| 40 | bold_green = None |
| 41 | def __init__(self, options=None): |
| 42 | if not options or options.color in ['no', 'never']: |
| 43 | want_color = False |
| 44 | elif options.color in ['yes', 'always']: |
| 45 | want_color = True |
| 46 | else: |
| 47 | want_color = sys.stderr.isatty() |
| 48 | if want_color: |
| 49 | # Assume ANSI compatible terminal |
| 50 | normal = '\033[0m' |
| 51 | self.red = ('\033[31m', normal) |
| 52 | self.green = ('\033[32m', normal) |
| 53 | self.bold_red = ('\033[1;31m', normal) |
| 54 | self.bold_green = ('\033[1;32m', normal) |
| 55 | NO_COLORS = Colors(None) |
| 56 | |
| 57 | def log_line(text, prefix='depends.py:', suffix='', color=None): |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 58 | """Print a status message.""" |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 59 | if color != None: |
| 60 | prefix = color[0] + prefix |
| 61 | suffix = suffix + color[1] |
| 62 | sys.stderr.write(prefix + ' ' + text + suffix + '\n') |
Gilles Peskine | 46c8256 | 2019-01-29 18:42:55 +0100 | [diff] [blame] | 63 | sys.stderr.flush() |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 64 | |
Gilles Peskine | 54aa5c6 | 2019-01-29 18:46:34 +0100 | [diff] [blame] | 65 | def log_command(cmd): |
| 66 | """Print a trace of the specified command. |
| 67 | cmd is a list of strings: a command name and its arguments.""" |
| 68 | log_line(' '.join(cmd), prefix='+') |
| 69 | |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 70 | def backup_config(options): |
Gilles Peskine | bf7537d | 2019-01-29 18:52:16 +0100 | [diff] [blame^] | 71 | """Back up the library configuration file (config.h). |
| 72 | If the backup file already exists, it is presumed to be the desired backup, |
| 73 | so don't make another backup.""" |
| 74 | if os.path.exists(options.config_backup): |
| 75 | options.own_backup = False |
| 76 | else: |
| 77 | options.own_backup = True |
| 78 | shutil.copy(options.config, options.config_backup) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 79 | |
Gilles Peskine | bf7537d | 2019-01-29 18:52:16 +0100 | [diff] [blame^] | 80 | def restore_config(options): |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 81 | """Restore the library configuration file (config.h). |
Gilles Peskine | bf7537d | 2019-01-29 18:52:16 +0100 | [diff] [blame^] | 82 | Remove the backup file if it was saved earlier.""" |
| 83 | if options.own_backup: |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 84 | shutil.move(options.config_backup, options.config) |
| 85 | else: |
| 86 | shutil.copy(options.config_backup, options.config) |
Gilles Peskine | bf7537d | 2019-01-29 18:52:16 +0100 | [diff] [blame^] | 87 | |
Gilles Peskine | 54aa5c6 | 2019-01-29 18:46:34 +0100 | [diff] [blame] | 88 | def run_config_pl(options, args): |
| 89 | """Run scripts/config.pl with the specified arguments.""" |
| 90 | cmd = ['scripts/config.pl'] |
| 91 | if options.config != 'include/mbedtls/config.h': |
| 92 | cmd += ['--file', options.config] |
| 93 | cmd += args |
| 94 | log_command(cmd) |
| 95 | subprocess.check_call(cmd) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 96 | |
| 97 | class Job: |
| 98 | """A job builds the library in a specific configuration and runs some tests.""" |
| 99 | def __init__(self, name, config_settings, commands): |
| 100 | """Build a job object. |
| 101 | The job uses the configuration described by config_settings. This is a |
| 102 | dictionary where the keys are preprocessor symbols and the values are |
| 103 | booleans or strings. A boolean indicates whether or not to #define the |
| 104 | symbol. With a string, the symbol is #define'd to that value. |
| 105 | After setting the configuration, the job runs the programs specified by |
| 106 | commands. This is a list of lists of strings; each list of string is a |
| 107 | command name and its arguments and is passed to subprocess.call with |
| 108 | shell=False.""" |
| 109 | self.name = name |
| 110 | self.config_settings = config_settings |
| 111 | self.commands = commands |
| 112 | |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 113 | def announce(self, colors, what): |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 114 | '''Announce the start or completion of a job. |
| 115 | If what is None, announce the start of the job. |
| 116 | If what is True, announce that the job has passed. |
| 117 | If what is False, announce that the job has failed.''' |
| 118 | if what is True: |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 119 | log_line(self.name + ' PASSED', color=colors.green) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 120 | elif what is False: |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 121 | log_line(self.name + ' FAILED', color=colors.red) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 122 | else: |
| 123 | log_line('starting ' + self.name) |
| 124 | |
Gilles Peskine | bf7537d | 2019-01-29 18:52:16 +0100 | [diff] [blame^] | 125 | def set_reference_config(self, options): |
| 126 | """Change the library configuration file (config.h) to the reference state. |
| 127 | The reference state is the one from which the tested configurations are |
| 128 | derived.""" |
| 129 | # Turn off memory management options that are not relevant to |
| 130 | # the tests and slow them down. |
| 131 | run_config_pl(options, ['full']) |
| 132 | run_config_pl(options, ['unset', 'MBEDTLS_MEMORY_BACKTRACE']) |
| 133 | run_config_pl(options, ['unset', 'MBEDTLS_MEMORY_BUFFER_ALLOC_C']) |
| 134 | run_config_pl(options, ['unset', 'MBEDTLS_MEMORY_DEBUG']) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 135 | |
Gilles Peskine | 54aa5c6 | 2019-01-29 18:46:34 +0100 | [diff] [blame] | 136 | def configure(self, options): |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 137 | '''Set library configuration options as required for the job. |
| 138 | config_file_name indicates which file to modify.''' |
Gilles Peskine | bf7537d | 2019-01-29 18:52:16 +0100 | [diff] [blame^] | 139 | self.set_reference_config(options) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 140 | for key, value in sorted(self.config_settings.items()): |
| 141 | if value is True: |
| 142 | args = ['set', key] |
| 143 | elif value is False: |
| 144 | args = ['unset', key] |
| 145 | else: |
| 146 | args = ['set', key, value] |
Gilles Peskine | 54aa5c6 | 2019-01-29 18:46:34 +0100 | [diff] [blame] | 147 | run_config_pl(options, args) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 148 | |
| 149 | def test(self, options): |
| 150 | '''Run the job's build and test commands. |
| 151 | Return True if all the commands succeed and False otherwise. |
| 152 | If options.keep_going is false, stop as soon as one command fails. Otherwise |
| 153 | run all the commands, except that if the first command fails, none of the |
| 154 | other commands are run (typically, the first command is a build command |
| 155 | and subsequent commands are tests that cannot run if the build failed).''' |
| 156 | built = False |
| 157 | success = True |
| 158 | for command in self.commands: |
Gilles Peskine | 54aa5c6 | 2019-01-29 18:46:34 +0100 | [diff] [blame] | 159 | log_command(command) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 160 | ret = subprocess.call(command) |
| 161 | if ret != 0: |
| 162 | if command[0] not in ['make', options.make_command]: |
| 163 | log_line('*** [{}] Error {}'.format(' '.join(command), ret)) |
| 164 | if not options.keep_going or not built: |
| 165 | return False |
| 166 | success = False |
| 167 | built = True |
| 168 | return success |
| 169 | |
| 170 | # SSL/TLS versions up to 1.1 and corresponding options. These require |
| 171 | # both MD5 and SHA-1. |
| 172 | ssl_pre_1_2_dependencies = ['MBEDTLS_SSL_CBC_RECORD_SPLITTING', |
| 173 | 'MBEDTLS_SSL_PROTO_SSL3', |
| 174 | 'MBEDTLS_SSL_PROTO_TLS1', |
| 175 | 'MBEDTLS_SSL_PROTO_TLS1_1'] |
| 176 | |
| 177 | # If the configuration option A requires B, make sure that |
| 178 | # B in reverse_dependencies[A]. |
| 179 | reverse_dependencies = { |
| 180 | 'MBEDTLS_ECDSA_C': ['MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'], |
| 181 | 'MBEDTLS_ECP_C': ['MBEDTLS_ECDSA_C', |
| 182 | 'MBEDTLS_ECDH_C', |
| 183 | 'MBEDTLS_ECJPAKE_C', |
| 184 | 'MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED', |
| 185 | 'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED', |
| 186 | 'MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED', |
| 187 | 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED', |
| 188 | 'MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'], |
| 189 | 'MBEDTLS_MD5_C': ssl_pre_1_2_dependencies, |
| 190 | 'MBEDTLS_PKCS1_V21': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT'], |
| 191 | 'MBEDTLS_PKCS1_V15': ['MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED', |
| 192 | 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED', |
| 193 | 'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED', |
| 194 | 'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'], |
| 195 | 'MBEDTLS_RSA_C': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT', |
| 196 | 'MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED', |
| 197 | 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED', |
| 198 | 'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED', |
| 199 | 'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'], |
| 200 | 'MBEDTLS_SHA1_C': ssl_pre_1_2_dependencies, |
| 201 | 'MBEDTLS_X509_RSASSA_PSS_SUPPORT': [], |
| 202 | } |
| 203 | |
| 204 | def turn_off_dependencies(config_settings): |
| 205 | """For every option turned off config_settings, also turn off what depends on it. |
| 206 | An option O is turned off if config_settings[O] is False.""" |
| 207 | for key, value in sorted(config_settings.items()): |
| 208 | if value is not False: |
| 209 | continue |
| 210 | for dep in reverse_dependencies.get(key, []): |
| 211 | config_settings[dep] = False |
| 212 | |
| 213 | class Domain: |
| 214 | """A domain is a set of jobs that all relate to a particular configuration aspect.""" |
| 215 | pass |
| 216 | |
| 217 | class ExclusiveDomain(Domain): |
| 218 | """A domain consisting of a set of conceptually-equivalent settings. |
| 219 | Establish a list of configuration symbols. For each symbol, run a test job |
| 220 | with this symbol set and the others unset, and a test job with this symbol |
| 221 | unset and the others set.""" |
| 222 | def __init__(self, symbols, commands): |
| 223 | self.jobs = [] |
| 224 | for invert in [False, True]: |
| 225 | base_config_settings = {} |
| 226 | for symbol in symbols: |
| 227 | base_config_settings[symbol] = invert |
| 228 | for symbol in symbols: |
| 229 | description = '!' + symbol if invert else symbol |
| 230 | config_settings = base_config_settings.copy() |
| 231 | config_settings[symbol] = not invert |
| 232 | turn_off_dependencies(config_settings) |
| 233 | job = Job(description, config_settings, commands) |
| 234 | self.jobs.append(job) |
| 235 | |
| 236 | class ComplementaryDomain: |
| 237 | """A domain consisting of a set of loosely-related settings. |
| 238 | Establish a list of configuration symbols. For each symbol, run a test job |
| 239 | with this symbol unset.""" |
| 240 | def __init__(self, symbols, commands): |
| 241 | self.jobs = [] |
| 242 | for symbol in symbols: |
| 243 | description = '!' + symbol |
| 244 | config_settings = {symbol: False} |
| 245 | turn_off_dependencies(config_settings) |
| 246 | job = Job(description, config_settings, commands) |
| 247 | self.jobs.append(job) |
| 248 | |
| 249 | class DomainData: |
| 250 | """Collect data about the library.""" |
| 251 | def collect_config_symbols(self, options): |
| 252 | """Read the list of settings from config.h. |
| 253 | Return them in a generator.""" |
| 254 | with open(options.config) as config_file: |
| 255 | rx = re.compile(r'\s*(?://\s*)?#define\s+(\w+)\s*(?:$|/[/*])') |
| 256 | for line in config_file: |
| 257 | m = re.match(rx, line) |
| 258 | if m: |
| 259 | yield m.group(1) |
| 260 | |
| 261 | def config_symbols_matching(self, regexp): |
| 262 | """List the config.h settings matching regexp.""" |
| 263 | return [symbol for symbol in self.all_config_symbols |
| 264 | if re.match(regexp, symbol)] |
| 265 | |
| 266 | def __init__(self, options): |
| 267 | """Gather data about the library and establish a list of domains to test.""" |
| 268 | build_command = [options.make_command, 'CFLAGS=-Werror'] |
| 269 | build_and_test = [build_command, [options.make_command, 'test']] |
| 270 | self.all_config_symbols = set(self.collect_config_symbols(options)) |
| 271 | # Find hash modules by name. |
| 272 | hash_symbols = self.config_symbols_matching(r'MBEDTLS_(MD|RIPEMD|SHA)[0-9]+_C\Z') |
| 273 | # Find elliptic curve enabling macros by name. |
| 274 | curve_symbols = self.config_symbols_matching(r'MBEDTLS_ECP_DP_\w+_ENABLED\Z') |
| 275 | # Find key exchange enabling macros by name. |
| 276 | key_exchange_symbols = self.config_symbols_matching(r'MBEDTLS_KEY_EXCHANGE_\w+_ENABLED\Z') |
| 277 | self.domains = { |
| 278 | # Elliptic curves. Run the test suites. |
| 279 | 'curves': ExclusiveDomain(curve_symbols, build_and_test), |
| 280 | # Hash algorithms. Exclude configurations with only one |
| 281 | # hash which is obsolete. Run the test suites. |
| 282 | 'hashes': ExclusiveDomain(hash_symbols, build_and_test), |
| 283 | # Key exchange types. Just check the build. |
| 284 | 'kex': ExclusiveDomain(key_exchange_symbols, [build_command]), |
| 285 | # Public-key algorithms. Run the test suites. |
| 286 | 'pkalgs': ComplementaryDomain(['MBEDTLS_ECDSA_C', |
| 287 | 'MBEDTLS_ECP_C', |
| 288 | 'MBEDTLS_PKCS1_V21', |
| 289 | 'MBEDTLS_PKCS1_V15', |
| 290 | 'MBEDTLS_RSA_C', |
| 291 | 'MBEDTLS_X509_RSASSA_PSS_SUPPORT'], |
| 292 | build_and_test), |
| 293 | } |
| 294 | self.jobs = {} |
| 295 | for domain in self.domains.values(): |
| 296 | for job in domain.jobs: |
| 297 | self.jobs[job.name] = job |
| 298 | |
| 299 | def get_jobs(self, name): |
| 300 | """Return the list of jobs identified by the given name. |
| 301 | A name can either be the name of a domain or the name of one specific job.""" |
| 302 | if name in self.domains: |
| 303 | return sorted(self.domains[name].jobs, key=lambda job: job.name) |
| 304 | else: |
| 305 | return [self.jobs[name]] |
| 306 | |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 307 | def run(options, job, colors=NO_COLORS): |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 308 | """Run the specified job (a Job instance).""" |
| 309 | subprocess.check_call([options.make_command, 'clean']) |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 310 | job.announce(colors, None) |
Gilles Peskine | 54aa5c6 | 2019-01-29 18:46:34 +0100 | [diff] [blame] | 311 | job.configure(options) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 312 | success = job.test(options) |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 313 | job.announce(colors, success) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 314 | return success |
| 315 | |
| 316 | def main(options, domain_data): |
| 317 | """Run the desired jobs. |
| 318 | domain_data should be a DomainData instance that describes the available |
| 319 | domains and jobs. |
| 320 | Run the jobs listed in options.domains.""" |
| 321 | if not hasattr(options, 'config_backup'): |
| 322 | options.config_backup = options.config + '.bak' |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 323 | colors = Colors(options) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 324 | jobs = [] |
| 325 | failures = [] |
| 326 | successes = [] |
| 327 | for name in options.domains: |
| 328 | jobs += domain_data.get_jobs(name) |
| 329 | backup_config(options) |
| 330 | try: |
| 331 | for job in jobs: |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 332 | success = run(options, job, colors=colors) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 333 | if not success: |
| 334 | if options.keep_going: |
| 335 | failures.append(job.name) |
| 336 | else: |
| 337 | return False |
| 338 | else: |
| 339 | successes.append(job.name) |
Gilles Peskine | bf7537d | 2019-01-29 18:52:16 +0100 | [diff] [blame^] | 340 | restore_config(options) |
| 341 | except: |
| 342 | # Restore the configuration, except in stop-on-error mode if there |
| 343 | # was an error, where we leave the failing configuration up for |
| 344 | # developer convenience. |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 345 | if options.keep_going: |
Gilles Peskine | bf7537d | 2019-01-29 18:52:16 +0100 | [diff] [blame^] | 346 | restore_config(options) |
| 347 | raise |
Gilles Peskine | e85163b | 2019-01-29 18:50:03 +0100 | [diff] [blame] | 348 | if successes: |
| 349 | log_line('{} passed'.format(' '.join(successes)), color=colors.bold_green) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 350 | if failures: |
Gilles Peskine | e85163b | 2019-01-29 18:50:03 +0100 | [diff] [blame] | 351 | log_line('{} FAILED'.format(' '.join(failures)), color=colors.bold_red) |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 352 | return False |
| 353 | else: |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 354 | return True |
| 355 | |
| 356 | |
| 357 | if __name__ == '__main__': |
| 358 | try: |
| 359 | parser = argparse.ArgumentParser(description=__doc__) |
Gilles Peskine | 0fa7cbe | 2019-01-29 18:48:48 +0100 | [diff] [blame] | 360 | parser.add_argument('--color', metavar='WHEN', |
| 361 | help='Colorize the output (always/auto/never)', |
| 362 | choices=['always', 'auto', 'never'], default='auto') |
Gilles Peskine | b39e3ec | 2019-01-29 08:50:20 +0100 | [diff] [blame] | 363 | parser.add_argument('-c', '--config', metavar='FILE', |
| 364 | help='Configuration file to modify', |
| 365 | default='include/mbedtls/config.h') |
| 366 | parser.add_argument('-C', '--directory', metavar='DIR', |
| 367 | help='Change to this directory before anything else', |
| 368 | default='.') |
| 369 | parser.add_argument('-k', '--keep-going', |
| 370 | help='Try all configurations even if some fail (default)', |
| 371 | action='store_true', dest='keep_going', default=True) |
| 372 | parser.add_argument('-e', '--no-keep-going', |
| 373 | help='Stop as soon as a configuration fails', |
| 374 | action='store_false', dest='keep_going') |
| 375 | parser.add_argument('--list-jobs', |
| 376 | help='List supported jobs and exit', |
| 377 | action='append_const', dest='list', const='jobs') |
| 378 | parser.add_argument('--list-domains', |
| 379 | help='List supported domains and exit', |
| 380 | action='append_const', dest='list', const='domains') |
| 381 | parser.add_argument('--make-command', metavar='CMD', |
| 382 | help='Command to run instead of make (e.g. gmake)', |
| 383 | action='store', default='make') |
| 384 | parser.add_argument('domains', metavar='DOMAIN', nargs='*', |
| 385 | help='The domain(s) to test (default: all)', |
| 386 | default=True) |
| 387 | options = parser.parse_args() |
| 388 | os.chdir(options.directory) |
| 389 | domain_data = DomainData(options) |
| 390 | if options.domains == True: |
| 391 | options.domains = sorted(domain_data.domains.keys()) |
| 392 | if options.list: |
| 393 | for what in options.list: |
| 394 | for key in sorted(getattr(domain_data, what).keys()): |
| 395 | print(key) |
| 396 | exit(0) |
| 397 | else: |
| 398 | sys.exit(0 if main(options, domain_data) else 1) |
| 399 | except SystemExit: |
| 400 | raise |
| 401 | except: |
| 402 | traceback.print_exc() |
| 403 | exit(3) |