1a8e1175bSopenharmony_ci#!/usr/bin/env python3
2a8e1175bSopenharmony_ci
3a8e1175bSopenharmony_ci"""Test helper for the Mbed TLS configuration file tool
4a8e1175bSopenharmony_ci
5a8e1175bSopenharmony_ciRun config.py with various parameters and write the results to files.
6a8e1175bSopenharmony_ci
7a8e1175bSopenharmony_ciThis is a harness to help regression testing, not a functional tester.
8a8e1175bSopenharmony_ciSample usage:
9a8e1175bSopenharmony_ci
10a8e1175bSopenharmony_ci    test_config_script.py -d old
11a8e1175bSopenharmony_ci    ## Modify config.py and/or mbedtls_config.h ##
12a8e1175bSopenharmony_ci    test_config_script.py -d new
13a8e1175bSopenharmony_ci    diff -ru old new
14a8e1175bSopenharmony_ci"""
15a8e1175bSopenharmony_ci
16a8e1175bSopenharmony_ci## Copyright The Mbed TLS Contributors
17a8e1175bSopenharmony_ci## SPDX-License-Identifier: Apache-2.0
18a8e1175bSopenharmony_ci##
19a8e1175bSopenharmony_ci## Licensed under the Apache License, Version 2.0 (the "License"); you may
20a8e1175bSopenharmony_ci## not use this file except in compliance with the License.
21a8e1175bSopenharmony_ci## You may obtain a copy of the License at
22a8e1175bSopenharmony_ci##
23a8e1175bSopenharmony_ci## http://www.apache.org/licenses/LICENSE-2.0
24a8e1175bSopenharmony_ci##
25a8e1175bSopenharmony_ci## Unless required by applicable law or agreed to in writing, software
26a8e1175bSopenharmony_ci## distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
27a8e1175bSopenharmony_ci## WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28a8e1175bSopenharmony_ci## See the License for the specific language governing permissions and
29a8e1175bSopenharmony_ci## limitations under the License.
30a8e1175bSopenharmony_ci
31a8e1175bSopenharmony_ciimport argparse
32a8e1175bSopenharmony_ciimport glob
33a8e1175bSopenharmony_ciimport os
34a8e1175bSopenharmony_ciimport re
35a8e1175bSopenharmony_ciimport shutil
36a8e1175bSopenharmony_ciimport subprocess
37a8e1175bSopenharmony_ci
38a8e1175bSopenharmony_ciOUTPUT_FILE_PREFIX = 'config-'
39a8e1175bSopenharmony_ci
40a8e1175bSopenharmony_cidef output_file_name(directory, stem, extension):
41a8e1175bSopenharmony_ci    return os.path.join(directory,
42a8e1175bSopenharmony_ci                        '{}{}.{}'.format(OUTPUT_FILE_PREFIX,
43a8e1175bSopenharmony_ci                                         stem, extension))
44a8e1175bSopenharmony_ci
45a8e1175bSopenharmony_cidef cleanup_directory(directory):
46a8e1175bSopenharmony_ci    """Remove old output files."""
47a8e1175bSopenharmony_ci    for extension in []:
48a8e1175bSopenharmony_ci        pattern = output_file_name(directory, '*', extension)
49a8e1175bSopenharmony_ci        filenames = glob.glob(pattern)
50a8e1175bSopenharmony_ci        for filename in filenames:
51a8e1175bSopenharmony_ci            os.remove(filename)
52a8e1175bSopenharmony_ci
53a8e1175bSopenharmony_cidef prepare_directory(directory):
54a8e1175bSopenharmony_ci    """Create the output directory if it doesn't exist yet.
55a8e1175bSopenharmony_ci
56a8e1175bSopenharmony_ci    If there are old output files, remove them.
57a8e1175bSopenharmony_ci    """
58a8e1175bSopenharmony_ci    if os.path.exists(directory):
59a8e1175bSopenharmony_ci        cleanup_directory(directory)
60a8e1175bSopenharmony_ci    else:
61a8e1175bSopenharmony_ci        os.makedirs(directory)
62a8e1175bSopenharmony_ci
63a8e1175bSopenharmony_cidef guess_presets_from_help(help_text):
64a8e1175bSopenharmony_ci    """Figure out what presets the script supports.
65a8e1175bSopenharmony_ci
66a8e1175bSopenharmony_ci    help_text should be the output from running the script with --help.
67a8e1175bSopenharmony_ci    """
68a8e1175bSopenharmony_ci    # Try the output format from config.py
69a8e1175bSopenharmony_ci    hits = re.findall(r'\{([-\w,]+)\}', help_text)
70a8e1175bSopenharmony_ci    for hit in hits:
71a8e1175bSopenharmony_ci        words = set(hit.split(','))
72a8e1175bSopenharmony_ci        if 'get' in words and 'set' in words and 'unset' in words:
73a8e1175bSopenharmony_ci            words.remove('get')
74a8e1175bSopenharmony_ci            words.remove('set')
75a8e1175bSopenharmony_ci            words.remove('unset')
76a8e1175bSopenharmony_ci            return words
77a8e1175bSopenharmony_ci    # Try the output format from config.pl
78a8e1175bSopenharmony_ci    hits = re.findall(r'\n +([-\w]+) +- ', help_text)
79a8e1175bSopenharmony_ci    if hits:
80a8e1175bSopenharmony_ci        return hits
81a8e1175bSopenharmony_ci    raise Exception("Unable to figure out supported presets. Pass the '-p' option.")
82a8e1175bSopenharmony_ci
83a8e1175bSopenharmony_cidef list_presets(options):
84a8e1175bSopenharmony_ci    """Return the list of presets to test.
85a8e1175bSopenharmony_ci
86a8e1175bSopenharmony_ci    The list is taken from the command line if present, otherwise it is
87a8e1175bSopenharmony_ci    extracted from running the config script with --help.
88a8e1175bSopenharmony_ci    """
89a8e1175bSopenharmony_ci    if options.presets:
90a8e1175bSopenharmony_ci        return re.split(r'[ ,]+', options.presets)
91a8e1175bSopenharmony_ci    else:
92a8e1175bSopenharmony_ci        help_text = subprocess.run([options.script, '--help'],
93a8e1175bSopenharmony_ci                                   check=False, # config.pl --help returns 255
94a8e1175bSopenharmony_ci                                   stdout=subprocess.PIPE,
95a8e1175bSopenharmony_ci                                   stderr=subprocess.STDOUT).stdout
96a8e1175bSopenharmony_ci        return guess_presets_from_help(help_text.decode('ascii'))
97a8e1175bSopenharmony_ci
98a8e1175bSopenharmony_cidef run_one(options, args, stem_prefix='', input_file=None):
99a8e1175bSopenharmony_ci    """Run the config script with the given arguments.
100a8e1175bSopenharmony_ci
101a8e1175bSopenharmony_ci    Take the original content from input_file if specified, defaulting
102a8e1175bSopenharmony_ci    to options.input_file if input_file is None.
103a8e1175bSopenharmony_ci
104a8e1175bSopenharmony_ci    Write the following files, where xxx contains stem_prefix followed by
105a8e1175bSopenharmony_ci    a filename-friendly encoding of args:
106a8e1175bSopenharmony_ci    * config-xxx.h: modified file.
107a8e1175bSopenharmony_ci    * config-xxx.out: standard output.
108a8e1175bSopenharmony_ci    * config-xxx.err: standard output.
109a8e1175bSopenharmony_ci    * config-xxx.status: exit code.
110a8e1175bSopenharmony_ci
111a8e1175bSopenharmony_ci    Return ("xxx+", "path/to/config-xxx.h") which can be used as
112a8e1175bSopenharmony_ci    stem_prefix and input_file to call this function again with new args.
113a8e1175bSopenharmony_ci    """
114a8e1175bSopenharmony_ci    if input_file is None:
115a8e1175bSopenharmony_ci        input_file = options.input_file
116a8e1175bSopenharmony_ci    stem = stem_prefix + '-'.join(args)
117a8e1175bSopenharmony_ci    data_filename = output_file_name(options.output_directory, stem, 'h')
118a8e1175bSopenharmony_ci    stdout_filename = output_file_name(options.output_directory, stem, 'out')
119a8e1175bSopenharmony_ci    stderr_filename = output_file_name(options.output_directory, stem, 'err')
120a8e1175bSopenharmony_ci    status_filename = output_file_name(options.output_directory, stem, 'status')
121a8e1175bSopenharmony_ci    shutil.copy(input_file, data_filename)
122a8e1175bSopenharmony_ci    # Pass only the file basename, not the full path, to avoid getting the
123a8e1175bSopenharmony_ci    # directory name in error messages, which would make comparisons
124a8e1175bSopenharmony_ci    # between output directories more difficult.
125a8e1175bSopenharmony_ci    cmd = [os.path.abspath(options.script),
126a8e1175bSopenharmony_ci           '-f', os.path.basename(data_filename)]
127a8e1175bSopenharmony_ci    with open(stdout_filename, 'wb') as out:
128a8e1175bSopenharmony_ci        with open(stderr_filename, 'wb') as err:
129a8e1175bSopenharmony_ci            status = subprocess.call(cmd + args,
130a8e1175bSopenharmony_ci                                     cwd=options.output_directory,
131a8e1175bSopenharmony_ci                                     stdin=subprocess.DEVNULL,
132a8e1175bSopenharmony_ci                                     stdout=out, stderr=err)
133a8e1175bSopenharmony_ci    with open(status_filename, 'w') as status_file:
134a8e1175bSopenharmony_ci        status_file.write('{}\n'.format(status))
135a8e1175bSopenharmony_ci    return stem + "+", data_filename
136a8e1175bSopenharmony_ci
137a8e1175bSopenharmony_ci### A list of symbols to test with.
138a8e1175bSopenharmony_ci### This script currently tests what happens when you change a symbol from
139a8e1175bSopenharmony_ci### having a value to not having a value or vice versa. This is not
140a8e1175bSopenharmony_ci### necessarily useful behavior, and we may not consider it a bug if
141a8e1175bSopenharmony_ci### config.py stops handling that case correctly.
142a8e1175bSopenharmony_ciTEST_SYMBOLS = [
143a8e1175bSopenharmony_ci    'CUSTOM_SYMBOL', # does not exist
144a8e1175bSopenharmony_ci    'MBEDTLS_AES_C', # set, no value
145a8e1175bSopenharmony_ci    'MBEDTLS_MPI_MAX_SIZE', # unset, has a value
146a8e1175bSopenharmony_ci    'MBEDTLS_NO_UDBL_DIVISION', # unset, in "System support"
147a8e1175bSopenharmony_ci    'MBEDTLS_PLATFORM_ZEROIZE_ALT', # unset, in "Customisation configuration options"
148a8e1175bSopenharmony_ci]
149a8e1175bSopenharmony_ci
150a8e1175bSopenharmony_cidef run_all(options):
151a8e1175bSopenharmony_ci    """Run all the command lines to test."""
152a8e1175bSopenharmony_ci    presets = list_presets(options)
153a8e1175bSopenharmony_ci    for preset in presets:
154a8e1175bSopenharmony_ci        run_one(options, [preset])
155a8e1175bSopenharmony_ci    for symbol in TEST_SYMBOLS:
156a8e1175bSopenharmony_ci        run_one(options, ['get', symbol])
157a8e1175bSopenharmony_ci        (stem, filename) = run_one(options, ['set', symbol])
158a8e1175bSopenharmony_ci        run_one(options, ['get', symbol], stem_prefix=stem, input_file=filename)
159a8e1175bSopenharmony_ci        run_one(options, ['--force', 'set', symbol])
160a8e1175bSopenharmony_ci        (stem, filename) = run_one(options, ['set', symbol, 'value'])
161a8e1175bSopenharmony_ci        run_one(options, ['get', symbol], stem_prefix=stem, input_file=filename)
162a8e1175bSopenharmony_ci        run_one(options, ['--force', 'set', symbol, 'value'])
163a8e1175bSopenharmony_ci        run_one(options, ['unset', symbol])
164a8e1175bSopenharmony_ci
165a8e1175bSopenharmony_cidef main():
166a8e1175bSopenharmony_ci    """Command line entry point."""
167a8e1175bSopenharmony_ci    parser = argparse.ArgumentParser(description=__doc__,
168a8e1175bSopenharmony_ci                                     formatter_class=argparse.RawDescriptionHelpFormatter)
169a8e1175bSopenharmony_ci    parser.add_argument('-d', metavar='DIR',
170a8e1175bSopenharmony_ci                        dest='output_directory', required=True,
171a8e1175bSopenharmony_ci                        help="""Output directory.""")
172a8e1175bSopenharmony_ci    parser.add_argument('-f', metavar='FILE',
173a8e1175bSopenharmony_ci                        dest='input_file', default='include/mbedtls/mbedtls_config.h',
174a8e1175bSopenharmony_ci                        help="""Config file (default: %(default)s).""")
175a8e1175bSopenharmony_ci    parser.add_argument('-p', metavar='PRESET,...',
176a8e1175bSopenharmony_ci                        dest='presets',
177a8e1175bSopenharmony_ci                        help="""Presets to test (default: guessed from --help).""")
178a8e1175bSopenharmony_ci    parser.add_argument('-s', metavar='FILE',
179a8e1175bSopenharmony_ci                        dest='script', default='scripts/config.py',
180a8e1175bSopenharmony_ci                        help="""Configuration script (default: %(default)s).""")
181a8e1175bSopenharmony_ci    options = parser.parse_args()
182a8e1175bSopenharmony_ci    prepare_directory(options.output_directory)
183a8e1175bSopenharmony_ci    run_all(options)
184a8e1175bSopenharmony_ci
185a8e1175bSopenharmony_ciif __name__ == '__main__':
186a8e1175bSopenharmony_ci    main()
187