diff --git a/scripts/pylib/twister/twisterlib/config_parser.py b/scripts/pylib/twister/twisterlib/config_parser.py index c822e9dfc77..f431e2d57c2 100644 --- a/scripts/pylib/twister/twisterlib/config_parser.py +++ b/scripts/pylib/twister/twisterlib/config_parser.py @@ -48,6 +48,7 @@ class TwisterConfigParser: "extra_conf_files": {"type": "list", "default": []}, "extra_overlay_confs" : {"type": "list", "default": []}, "extra_dtc_overlay_files": {"type": "list", "default": []}, + "required_snippets": {"type": "list"}, "build_only": {"type": "bool", "default": False}, "build_on_all": {"type": "bool", "default": False}, "skip": {"type": "bool", "default": False}, diff --git a/scripts/pylib/twister/twisterlib/runner.py b/scripts/pylib/twister/twisterlib/runner.py index ad93145db38..bbbdb18d05a 100644 --- a/scripts/pylib/twister/twisterlib/runner.py +++ b/scripts/pylib/twister/twisterlib/runner.py @@ -359,6 +359,10 @@ class CMake: cmake_opts = ['-DBOARD={}'.format(self.platform.name)] cmake_args.extend(cmake_opts) + if self.instance.testsuite.required_snippets: + cmake_opts = ['-DSNIPPET={}'.format(';'.join(self.instance.testsuite.required_snippets))] + cmake_args.extend(cmake_opts) + cmake = shutil.which('cmake') cmd = [cmake] + cmake_args diff --git a/scripts/pylib/twister/twisterlib/testplan.py b/scripts/pylib/twister/twisterlib/testplan.py index 85340885dbb..1d9c625b28a 100755 --- a/scripts/pylib/twister/twisterlib/testplan.py +++ b/scripts/pylib/twister/twisterlib/testplan.py @@ -16,6 +16,8 @@ import logging import copy import shutil import random +import snippets +from pathlib import Path logger = logging.getLogger('twister') logger.setLevel(logging.DEBUG) @@ -819,6 +821,46 @@ class TestPlan: if plat.only_tags and not set(plat.only_tags) & ts.tags: instance.add_filter("Excluded tags per platform (only_tags)", Filters.PLATFORM) + if ts.required_snippets: + missing_snippet = False + snippet_args = {"snippets": ts.required_snippets} + found_snippets = snippets.find_snippets_in_roots(snippet_args, [Path(ZEPHYR_BASE), Path(ts.source_dir)]) + + # Search and check that all required snippet files are found + for this_snippet in snippet_args['snippets']: + if this_snippet not in found_snippets: + logger.error(f"Can't find snippet '%s' for test '%s'", this_snippet, ts.name) + instance.status = "error" + instance.reason = f"Snippet {this_snippet} not found" + missing_snippet = True + break + + if not missing_snippet: + # Look for required snippets and check that they are applicable for these + # platforms/boards + for this_snippet in found_snippets: + matched_snippet_board = False + + # If the "appends" key is present with at least one entry then this + # snippet applies to all boards and further platform-specific checks + # are not required + if found_snippets[this_snippet].appends: + continue + + for this_board in found_snippets[this_snippet].board2appends: + if this_board.startswith('/'): + match = re.search(this_board[1:-1], plat.name) + if match is not None: + matched_snippet_board = True + break + elif this_board == plat.name: + matched_snippet_board = True + break + + if matched_snippet_board is False: + instance.add_filter("Snippet not supported", Filters.PLATFORM) + break + # platform_key is a list of unique platform attributes that form a unique key a test # will match against to determine if it should be scheduled to run. A key containing a # field name that the platform does not have will filter the platform. diff --git a/scripts/schemas/twister/testsuite-schema.yaml b/scripts/schemas/twister/testsuite-schema.yaml index 4d67fe71706..116c1a43379 100644 --- a/scripts/schemas/twister/testsuite-schema.yaml +++ b/scripts/schemas/twister/testsuite-schema.yaml @@ -145,6 +145,11 @@ mapping: matching: "all" sequence: - type: str + "required_snippets": + type: seq + required: false + sequence: + - type: str "tags": type: any required: false @@ -243,6 +248,11 @@ mapping: "extra_sections": type: any required: false + "required_snippets": + type: seq + required: false + sequence: + - type: str "filter": type: str required: false diff --git a/scripts/snippets.py b/scripts/snippets.py index 9662f3edcec..78ab896e85b 100644 --- a/scripts/snippets.py +++ b/scripts/snippets.py @@ -238,6 +238,22 @@ def process_snippets(args: argparse.Namespace) -> Snippets: return snippets +def find_snippets_in_roots(requested_snippets, snippet_roots) -> Snippets: + '''Process snippet.yml files under each *snippet_root* + by recursive search. Return a Snippets object describing + the results of the search. + ''' + # This will contain information about all the snippets + # we discover in each snippet_root element. + snippets = Snippets(requested=requested_snippets) + + # Process each path in snippet_root in order, adjusting + # snippets as needed for each one. + for root in snippet_roots: + process_snippets_in(root, snippets) + + return snippets + def process_snippets_in(root_dir: Path, snippets: Snippets) -> None: '''Process snippet.yml files in *root_dir*, updating *snippets* as needed.''' diff --git a/scripts/twister b/scripts/twister index 0f5e622011f..f41349871ed 100755 --- a/scripts/twister +++ b/scripts/twister @@ -44,6 +44,9 @@ pairs: Extra configuration options to be merged with a master prj.conf when building or running the test case. + required_snippets: + Snippets that must be applied for the test case to run. + sysbuild: (default False) If true, build the sample using the sysbuild infrastructure. Filtering will only be enabled for the main project, and is not supported for diff --git a/scripts/west_commands/build.py b/scripts/west_commands/build.py index 65dbe963cd4..bcc5106f8ca 100644 --- a/scripts/west_commands/build.py +++ b/scripts/west_commands/build.py @@ -293,6 +293,7 @@ class Build(Forceable): extra_dtc_overlay_files = [] extra_overlay_confs = [] extra_conf_files = [] + required_snippets = [] for section in [common, item]: if not section: continue @@ -302,7 +303,8 @@ class Build(Forceable): 'extra_configs', 'extra_conf_files', 'extra_overlay_confs', - 'extra_dtc_overlay_files' + 'extra_dtc_overlay_files', + 'required_snippets' ]: extra = section.get(data) if not extra: @@ -325,6 +327,9 @@ class Build(Forceable): elif data == 'extra_dtc_overlay_files': extra_dtc_overlay_files.extend(arg_list) continue + elif data == 'required_snippets': + required_snippets.extend(arg_list) + continue if self.args.cmake_opts: self.args.cmake_opts.extend(args) @@ -343,6 +348,10 @@ class Build(Forceable): if extra_overlay_confs: args.append(f"OVERLAY_CONFIG=\"{';'.join(extra_overlay_confs)}\"") + + if required_snippets: + args.append(f"SNIPPET=\"{';'.join(required_snippets)}\"") + # Build the final argument list args_expanded = ["-D{}".format(a.replace('"', '')) for a in args]