Skip to content

Conversation

@ronald-cron-arm
Copy link
Contributor

@ronald-cron-arm ronald-cron-arm commented Dec 1, 2025

Description

Add support for driver dispatch testing in TF-PSA-Crypto

PR checklist

Update test drivers for TF-PSA-Crypto test driver
based on a copy of `drivers/builtin`.

We mainly keep using what is already in place for
libtestdriver1 in Mbed TLS by still defining
MBEDTLS_TEST_LIBTESTDRIVER1.

As in TF-PSA-Crypto, the test driver is a derivative
of a clone of the built-in driver, not of the whole
library, the paths to the driver internal headers
are different. The paths are relative to
`drivers/libtestdriver1/include`.

Otherwise, `psa_key_attributes_t` is prefixed with
`libtestdriver1_` in Mbed TLS test driver but not
in TF-PSA-Crypto one. Thus in TF-PSA-Crypto case
define `libtestdriver1_psa_key_attributes_t` as
`psa_key_attributes_t`.

Signed-off-by: Ronald Cron <ronald.cron@arm.com>
Signed-off-by: Ronald Cron <ronald.cron@arm.com>
Signed-off-by: Ronald Cron <ronald.cron@arm.com>
@ronald-cron-arm ronald-cron-arm added enhancement New feature or request needs-ci Needs to pass CI tests priority-high High priority - will be reviewed soon labels Dec 1, 2025
Add get_identifiers_with_prefixes method that returns
the list of identifiers that potentially need to be
prefixed in the test driver code.

Signed-off-by: Ronald Cron <ronald.cron@arm.com>
Signed-off-by: Ronald Cron <ronald.cron@arm.com>
@ronald-cron-arm ronald-cron-arm added needs-review Every commit must be reviewed by at least two team members, needs-reviewer This PR needs someone to pick it up for review and removed needs-ci Needs to pass CI tests labels Dec 2, 2025
@ronald-cron-arm ronald-cron-arm removed the needs-reviewer This PR needs someone to pick it up for review label Dec 2, 2025
Copy link
Contributor

@gilles-peskine-arm gilles-peskine-arm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fully reviewed 61f15a6, as well as the Python code in Mbed-TLS/TF-PSA-Crypto#586.

I like the methodology, but the Python code sometimes feels hard to follow compared to what it does.


#if defined(MBEDTLS_TEST_LIBTESTDRIVER1)
#if defined(TF_PSA_CRYPTO_TEST_LIBTESTDRIVER1)
typedef psa_key_attributes_t libtestdriver1_psa_key_attributes_t;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is repeated, it feels like it should be in a header somewhere, probably tests/include/test/drivers/test_driver_common.h.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @gilles-peskine-arm

What the code is effectively trying to achieve is rewrite the include path and include the appropriate header depending on the variant of the libtestdriver.

That logic alongside the typedef could easily reside in a separate header and exposed thought a getter macro e.g LIBTESTDRIVER1_INCLUDE(psa_crypto_hash)

#if defined(MBEDTLS_TEST_LIBTESTDRIVER1)
#if defined(TF_PSA_CRYPTO_TEST_LIBTESTDRIVER1)
typedef psa_key_attributes_t libtestdriver1_psa_key_attributes_t;
#include "../../libtestdriver1/src/psa_crypto_aead.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a while to understand this path. It's weird to go up two levels. My first thought was that the include path was relative to the file that this directive is in, but that didn't work, and a path relative to the framework would be fragile since it could be different in in-tree vs out-of-tree builds. Then I noticed the commit message

The paths are relative to drivers/libtestdriver1/include.

Ok, that makes sense, but then why not ../src/psa_crypto_aead.h? But that wouldn't necessarily work since it could pick up the one under /drivers/builtin. Ok, then it does have to be what you wrote, although I would prefer

#include <../../libtestdriver1/src/psa_crypto_aead.h>

since we know this isn't relative to the location of the file, it comes from the include path.

And I would like this explained in a comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. When people will need to touch the code in the future they likely wont consider looking at the comment history using git log --follow. If its not intuitive it should be a comment

Comment on lines +19 to +21
parser = argparse.ArgumentParser(description= \
"Clone partially builtin tree, rewrite header inclusions and prefix"
"exposed C identifiers.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string contains prefixexposed without a space. Use triple-quotes for such long strings.

Suggested change
parser = argparse.ArgumentParser(description= \
"Clone partially builtin tree, rewrite header inclusions and prefix"
"exposed C identifiers.")
parser = argparse.ArgumentParser(description="""\
Partially clone builtin tree, rewrite header inclusions and prefix exposed C
identifiers.
""")

(I also fixed the grammar, since AFAICT this isn't about cloning trees that are partially built-in, but about partially cloning the builtin tree.)

Comment on lines +24 to +26
help="Destination directory.\n"
" - If absolute, used as-is.\n"
" - If relative, interpreted relative to the repository root.\n")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argparse doesn't preserve line breaks in help text, so you can't format with bullet points. I think it could be shorter anyway.

Suggested change
help="Destination directory.\n"
" - If absolute, used as-is.\n"
" - If relative, interpreted relative to the repository root.\n")
help="Destination directory (relative to repository root)"

for directory in ("include", "src"):
directory_path = root / directory
for ext in (".c", ".h"):
yield from directory_path.rglob(f"*{ext}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rglob doesn't have deterministic ordering, which can make debugging annoying. On the other hand, you take care to emit .c files before .h files, is there a reason for that?

How about something like

return sorted(path
              for directory in ('include', 'src')
              for path in (root / directory).rglob('*.[hc]')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No particular reason to use "for ext in (".c", ".h"):" over "*.[hc]".

#include "libtestdriver1/private/aes.h"
"""
include_line_re = re.compile(
fr'^\s*#\s*include\s*([<"])\s*{src_include_dir}/([^>"]+)\s*([>"])',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't have spaces inside the quotes/brackets.

Suggested change
fr'^\s*#\s*include\s*([<"])\s*{src_include_dir}/([^>"]+)\s*([>"])',
fr'^\s*#\s*include\s*([<"]){src_include_dir}/([^>"]+)([>"])',

return f'#include {m.group(1)}{driver}/{header}{m.group(3)}'
return m.group(0)

new_text = include_line_re.sub(repl, text)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified to something like (untested):

Suggested change
new_text = include_line_re.sub(repl, text)
renamed_headers = '|'.join(re.escape(header) for header in headers)
include_line_re = re.compile(fr'^(\s*#\s*include\s*["<])((?:{renamed_headers})[">])', re.M)
new_text, changed = include_line_re.subn(repl, fr'\1{driver}-\2')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried something similar but this is significantly slower. I guess because of the huge regular expressions.

Comment on lines +204 to +218
c_identifier_re = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b")
text = file.read_text(encoding="utf-8")
prefix_uppercased = prefix.upper()
prefix_lowercased = prefix.lower()

def repl(m: Match) -> str:
identifier = m.group(0)
if identifier in identifiers:
if identifier[0].isupper():
return f"{prefix_uppercased}_{identifier}"
else:
return f"{prefix_lowercased}_{identifier}"
return identifier

new_text = c_identifier_re.sub(repl, text)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make regexps do the work:

Suggested change
c_identifier_re = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b")
text = file.read_text(encoding="utf-8")
prefix_uppercased = prefix.upper()
prefix_lowercased = prefix.lower()
def repl(m: Match) -> str:
identifier = m.group(0)
if identifier in identifiers:
if identifier[0].isupper():
return f"{prefix_uppercased}_{identifier}"
else:
return f"{prefix_lowercased}_{identifier}"
return identifier
new_text = c_identifier_re.sub(repl, text)
lower_identifier_re = re.compile(r'\b(' + '|'.join(ident for ident in identifiers if ident[0].islower()) + r')\b')
upper_identifier_re = re.compile(r'\b(' + '|'.join(ident for ident in identifiers if ident[0].isupper()) + r')\b')
text = file.read_text(encoding="utf-8")
prefix_uppercased = prefix.upper()
prefix_lowercased = prefix.lower()
new_text = lower_identifier_re.sub(prefix.lower() + r'_\1',
upper_identifier_re.sub(prefix.upper() + r'_\1', text))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried something similar but this is significantly slower. I guess because of the huge regular expressions.

Only the `include` and `src` directories from `src_dir` are used to build
the test driver tree, and their directory structure is preserved.
Only "*.h" and "*.c" files are copied. Files whose names match any of the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“file name” either refers to the base name or to the full path (not necessarily absolute), depending on who you ask. Please clarify.

Suggested change
Only "*.h" and "*.c" files are copied. Files whose names match any of the
Only "*.h" and "*.c" files are copied. Files whose basenames match any of the

for ext in (".c", ".h"):
yield from directory_path.rglob(f"*{ext}")

def run_ctags(file: Path) -> Set[str]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“Run ctags” is how the function works, not its purpose. How about something like

Suggested change
def run_ctags(file: Path) -> Set[str]:
def get_c_identifiers(file: Path) -> Set[str]:

Copy link
Contributor

@minosgalanakis minosgalanakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionally works, but there is some work required for the design


#if defined(MBEDTLS_TEST_LIBTESTDRIVER1)
#if defined(TF_PSA_CRYPTO_TEST_LIBTESTDRIVER1)
typedef psa_key_attributes_t libtestdriver1_psa_key_attributes_t;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @gilles-peskine-arm

What the code is effectively trying to achieve is rewrite the include path and include the appropriate header depending on the variant of the libtestdriver.

That logic alongside the typedef could easily reside in a separate header and exposed thought a getter macro e.g LIBTESTDRIVER1_INCLUDE(psa_crypto_hash)

#if defined(MBEDTLS_TEST_LIBTESTDRIVER1)
#if defined(TF_PSA_CRYPTO_TEST_LIBTESTDRIVER1)
typedef psa_key_attributes_t libtestdriver1_psa_key_attributes_t;
#include "../../libtestdriver1/src/psa_crypto_aead.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. When people will need to touch the code in the future they likely wont consider looking at the comment history using git log --follow. If its not intuitive it should be a comment


#if defined(MBEDTLS_TEST_LIBTESTDRIVER1)
#if !defined(TF_PSA_CRYPTO_TEST_LIBTESTDRIVER1) && \
defined(MBEDTLS_TEST_LIBTESTDRIVER1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we end up using a helper header file, this logic and other variants can also be simplified by a macro or symbol defined there.

mbedtls_test_driver_signature_verify_hooks = MBEDTLS_TEST_DRIVER_SIGNATURE_INIT;

psa_status_t sign_hash(
static psa_status_t sign_hash(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a minor bugfix unreleated to the previous changes? Or required because multiple references of the symbol were included?

The source directory `src_dir` is expected to have the following structure:
- an `include` directory
- an `src` directory
- the `include` directory contains exactly one subdirectory
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is needed because of the line:

        (self.dst_dir / "include" / src_include_dir_name).rename( \
         self.test_driver_include_dir)

which will rename the drivers/builtin/include/mbedtls -> drivers/builtin/include/libtestdrive1

if exclude_files is None:
exclude_files = set()

for file in iter_code_files(src_dir):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 Currently the logic required to create this tree is split across two repositories, with tf-psa-crypto/tests/scripts/generate_test_driver.py responsbile to call generator.prefix_identifiers(identifiers). We have historically been having troubles maintaing split logic tools.

The way the data is linked is that build_tree stores data to 'self.dst_dir' which is parsed by get_identifiers_with_prefixes and get_external_identifiers to extract the contents of that to generate_test_driver, allow it to append some identifiers to the list, and call the prefix_identifiers to write them in the file.

I propose we give the TestDriveGeneration information ahead of time, and let it do its thing without having to know how it works from the caller side.

As such a simplistic approach on the api should be:

  1. create the instance: generator = test_driver.TestDriverGenerator(dst_dir, args.driver)
  2. Configure it with the infomation it needs. Provide the EXCLUDE_FILES, extra identifies that need to be added like MBEDTLS_ECP_LIGHT, set the rename rules e.g PSA_WANT_ -> MBEDTLS_PSA_ACCEL_. This will need a new .config method.
  3. Invoke generator.build_tree or something equivalent that will do all the steps, without exposing the logic to the calling script.

continue
dst = self.dst_dir / file.relative_to(src_dir)
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(file, dst)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep and return a list of all the files that have been created

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have "--list-vars-for-cmake" for that I'd say.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request needs-review Every commit must be reviewed by at least two team members, needs-work priority-high High priority - will be reviewed soon

Projects

Development

Successfully merging this pull request may close these issues.

3 participants