-
Notifications
You must be signed in to change notification settings - Fork 31
check_config.h: unit tests #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
gilles-peskine-arm
merged 2 commits into
Mbed-TLS:main
from
gilles-peskine-arm:test-check-config
Jul 23, 2025
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
"""Test the configuration checks that reject some bad compile-time configs. | ||
|
||
This tests the output of ``generate_config_checks.py``. | ||
This can also let us verify what we enforce in the manually written | ||
checks in ``<PROJECT>_check_config.h``. | ||
""" | ||
|
||
## Copyright The Mbed TLS Contributors | ||
## SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later | ||
|
||
import os | ||
import subprocess | ||
import sys | ||
import unittest | ||
from typing import List, Optional, Pattern, Union | ||
|
||
|
||
class TestConfigChecks(unittest.TestCase): | ||
"""Unit tests for checks performed via ``<PROJECT>_config.c``. | ||
|
||
This can test the code generated by `config_checks_generator`, | ||
as well as manually written checks in `check_config.h`. | ||
""" | ||
|
||
# Set this to the path to the source file containing the config checks. | ||
PROJECT_CONFIG_C = None #type: Optional[str] | ||
|
||
# Project-specific include directories (in addition to /include) | ||
PROJECT_SPECIFIC_INCLUDE_DIRECTORIES = [] #type: List[str] | ||
|
||
# Increase the length of strings that assertion failures are willing to | ||
# print. This is useful for failures where the preprocessor has a lot | ||
# to say. | ||
maxDiff = 9999 | ||
|
||
def setUp(self) -> None: | ||
self.cpp_output = None #type: Optional[str] | ||
|
||
def tearDown(self) -> None: | ||
"""Log the preprocessor output to a file, if available and desired. | ||
|
||
This is intended for debugging. It only happens if the environment | ||
variable UNITTEST_CONFIG_CHECKS_DEBUG is non-empty. | ||
""" | ||
if os.getenv('UNITTEST_CONFIG_CHECKS_DEBUG'): | ||
# We set self.cpp_output to the preprocessor output before | ||
# asserting, and set it to None if all the assertions pass. | ||
if self.cpp_output is not None: | ||
basename = os.path.splitext(os.path.basename(sys.argv[0]))[0] | ||
filename = f'{basename}.{self._testMethodName}.out.txt' | ||
with open(filename, 'w') as out: | ||
out.write(self.cpp_output) | ||
|
||
def user_config_file_name(self, variant: str) -> str: | ||
"""Construct a unique temporary file name for a user config header.""" | ||
name = os.path.splitext(os.path.basename(sys.argv[0]))[0] | ||
pid = str(os.getpid()) | ||
oid = str(id(self)) | ||
return f'tmp-user_config_{variant}-{name}-{pid}-{oid}.h' | ||
|
||
def write_user_config(self, variant: str, content: Optional[str]) -> Optional[str]: | ||
"""Write a user configuration file with the given content. | ||
|
||
If content is None, ensure the file does not exist. | ||
valeriosetti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Return None if content is none, otherwise return the file name. | ||
""" | ||
file_name = self.user_config_file_name(variant) | ||
if content is None: | ||
if os.path.exists(file_name): | ||
os.remove(file_name) | ||
return None | ||
if content and not content.endswith('\n'): | ||
content += '\n' | ||
with open(file_name, 'w', encoding='ascii') as out: | ||
out.write(content) | ||
return file_name | ||
|
||
def run_with_config_files(self, | ||
crypto_user_config_file: Optional[str], | ||
mbedtls_user_config_file: Optional[str], | ||
extra_options: List[str], | ||
) -> subprocess.CompletedProcess: | ||
"""Run cpp with the given user configuration files. | ||
|
||
Return the CompletedProcess object capturing the return code, | ||
stdout and stderr. | ||
""" | ||
cmd = ['cpp'] | ||
if os.getenv('UNITTEST_CONFIG_CHECKS_DEBUG'): | ||
cmd += ['-dD'] | ||
if crypto_user_config_file is not None: | ||
cmd.append(f'-DTF_PSA_CRYPTO_USER_CONFIG_FILE="{crypto_user_config_file}"') | ||
if mbedtls_user_config_file is not None: | ||
cmd.append(f'-DMBEDTLS_USER_CONFIG_FILE="{mbedtls_user_config_file}"') | ||
cmd += extra_options | ||
assert self.PROJECT_CONFIG_C is not None | ||
cmd += ['-I' + dir for dir in self.PROJECT_SPECIFIC_INCLUDE_DIRECTORIES] | ||
cmd += ['-Iinclude', | ||
'-I.', | ||
'-I' + os.path.dirname(self.PROJECT_CONFIG_C)] | ||
cmd.append(self.PROJECT_CONFIG_C) | ||
return subprocess.run(cmd, | ||
check=False, | ||
encoding='ascii', | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.PIPE) | ||
|
||
def run_with_config(self, | ||
crypto_user_config: Optional[str], | ||
mbedtls_user_config: Optional[str] = None, | ||
extra_options: Optional[List[str]] = None, | ||
) -> subprocess.CompletedProcess: | ||
"""Run cpp with the given content for user configuration files. | ||
|
||
Return the CompletedProcess object capturing the return code, | ||
stdout and stderr. | ||
""" | ||
if extra_options is None: | ||
extra_options = [] | ||
crypto_user_config_file = None | ||
mbedtls_user_config_file = None | ||
try: | ||
# Create temporary files without using tempfile because: | ||
# 1. Before Python 3.12, tempfile.NamedTemporaryFile does | ||
# not have good support for allowing an external program | ||
# to access the file on Windows. | ||
# 2. With a tempfile-provided context, it's awkward to not | ||
# create a file optionally (we only do it when xxx_user_config | ||
# is not None). | ||
crypto_user_config_file = \ | ||
self.write_user_config('crypto', crypto_user_config) | ||
mbedtls_user_config_file = \ | ||
self.write_user_config('mbedtls', mbedtls_user_config) | ||
cp = self.run_with_config_files(crypto_user_config_file, | ||
mbedtls_user_config_file, | ||
extra_options) | ||
return cp | ||
finally: | ||
if crypto_user_config_file is not None and \ | ||
os.path.exists(crypto_user_config_file): | ||
os.remove(crypto_user_config_file) | ||
if mbedtls_user_config_file is not None and \ | ||
os.path.exists(mbedtls_user_config_file): | ||
os.remove(mbedtls_user_config_file) | ||
|
||
def good_case(self, | ||
crypto_user_config: Optional[str], | ||
mbedtls_user_config: Optional[str] = None, | ||
extra_options: Optional[List[str]] = None, | ||
) -> None: | ||
"""Run cpp with the given user config(s). Expect no error. | ||
|
||
Pass extra_options on the command line of cpp. | ||
""" | ||
cp = self.run_with_config(crypto_user_config, mbedtls_user_config, | ||
extra_options=extra_options) | ||
# Assert the error text before the status. That way, if it fails, | ||
# we see the unexpected error messages in the test log. | ||
self.cpp_output = cp.stdout | ||
self.assertEqual(cp.stderr, '') | ||
self.assertEqual(cp.returncode, 0) | ||
self.cpp_output = None | ||
|
||
def bad_case(self, | ||
crypto_user_config: Optional[str], | ||
mbedtls_user_config: Optional[str] = None, | ||
error: Optional[Union[str, Pattern]] = None, | ||
extra_options: Optional[List[str]] = None, | ||
) -> None: | ||
"""Run cpp with the given user config(s). Expect errors. | ||
|
||
Pass extra_options on the command line of cpp. | ||
|
||
If error is given, the standard error from cpp must match this regex. | ||
""" | ||
cp = self.run_with_config(crypto_user_config, mbedtls_user_config, | ||
extra_options=extra_options) | ||
self.cpp_output = cp.stdout | ||
if error is not None: | ||
# Assert the error text before the status. That way, if it fails, | ||
# we see the unexpected error messages in the test log. | ||
self.assertRegex(cp.stderr, error) | ||
self.assertGreater(cp.returncode, 0) | ||
self.assertLess(cp.returncode, 126) | ||
self.cpp_output = None | ||
|
||
# Nominal case, run first | ||
def test_01_nominal(self) -> None: | ||
self.good_case(None) | ||
|
||
# Trivial error case, run second | ||
def test_02_error(self) -> None: | ||
self.bad_case('#error "Bad crypto configuration"', | ||
error='"Bad crypto configuration"') |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.