Skip to content

Commit 1925d39

Browse files
committed
[nrf fromtree] twister: Account for board & SoC extensions
Problem ------- Board & SoC extensions are used to define out-of-tree board variants or SoC qualifiers. When a board is extended, it has multiple directories associated with it (each with its own `board.yml`), where twister should be able to find additional platform files to support these qualifiers. Currently, this doesn't work, because twister only traverses the primary BOARD_DIR and ignores the rest. The fix would've been trivial in the case of "legacy" platform files, i.e. those of the form `<normalized_board_target>.yaml`, but it's less straightforward for the newly introduced `twister.yaml` format. A `twister.yaml` file contains platform configuration that can be shared by multiple board targets and tweaked for specific targets by using the top-level `variants` key. Normally, there is at most one `twister.yaml` per board, but the file isn't necessarily unique to one board. Instead, it's unique to one directory, which may define multiple boards (as is the case with e.g. `boards/qemu/x86/`). With extensions in the picture, the goal is to initialize platforms when given multiple `twister.yaml` per board. The OOT files are expected to only provide information about OOT board targets, without being able to override in-tree targets (same principle as in the Zephyr build system). Solution -------- The `twister.yaml` handling is broken up into multiple passes - first loading all the files, then splitting the `variants` keys apart from the shared configuration, before constructing the Platform instances. The purpose of the split is to treat the variant information as global, instead of making unnecessary or faulty assumptions about locality. Remember that the build system can derive board target names not only from `board.yml`, but from `soc.yml` too. Considering that any board may end up using an OOT-extended SoC (and hence multiple `soc.yml` files), not every board target can be said to belong to some board dir. Unlike the variant data, the remaining top-level config is still rooted to the primary BOARD_DIR and inherited by the extension dirs from there. This is quite intuitive in most imagined cases, but there is a caveat: if a `twister.yaml` resides in an extension dir, then it is allowed to have a top-level config of its own, but it will be silently ignored. This is to support corner cases where, much like how a single board dir can define multiple boards, a single board dir can also extend multiple boards, or even do both. In those cases, the primary BOARD_DIR rule should make it unambiguous which config belongs to which board, even if it may seem counter-intuitive at first. For concrete examples of what this means, please see the newly added platform unit tests. As part of these functional changes, a good chunk of logic is moved out of `TestPlan.add_configurations()` into a new function in `platform.py`. This is because recombining the top-level and variant configs requires direct manipulation of the loaded YAML contents, which would be improper to do outside of the module responsible for encapsulating this data. Signed-off-by: Grzegorz Swiderski <[email protected]> (cherry picked from commit bb8b059)
1 parent 915a4b1 commit 1925d39

File tree

4 files changed

+506
-262
lines changed

4 files changed

+506
-262
lines changed

scripts/pylib/twister/twisterlib/platform.py

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import logging
1010
import os
1111
import shutil
12+
from argparse import Namespace
13+
from itertools import groupby
1214

15+
import list_boards
1316
import scl
1417
from twisterlib.constants import SUPPORTED_SIMS
1518
from twisterlib.environment import ZEPHYR_BASE
@@ -83,28 +86,18 @@ def __init__(self):
8386
self.filter_data = dict()
8487
self.uart = ""
8588
self.resc = ""
86-
self.qualifier = None
8789

88-
def load(self, board, target, aliases, data):
90+
def load(self, board, target, aliases, data, variant_data):
8991
"""Load the platform data from the board data and target data
9092
board: the board object as per the zephyr build system
9193
target: the target name of the board as per the zephyr build system
9294
aliases: list of aliases for the target
93-
data: the data from the twister.yaml file for the target
95+
data: the default data from the twister.yaml file for the board
96+
variant_data: the target-specific data to replace the default data
9497
"""
9598
self.name = target
9699
self.aliases = aliases
97100

98-
# Get data for various targets and use the main board data as a
99-
# defauly. Individual variant information will replace the default data
100-
# provded in the main twister configuration for this board.
101-
variants = data.get("variants", {})
102-
variant_data = {}
103-
for alias in aliases:
104-
variant_data = variants.get(alias, {})
105-
if variant_data:
106-
break
107-
108101
self.normalized_name = self.name.replace("/", "_")
109102
self.sysbuild = variant_data.get("sysbuild", data.get("sysbuild", self.sysbuild))
110103
self.twister = variant_data.get("twister", data.get("twister", self.twister))
@@ -184,3 +177,116 @@ def simulator_by_name(self, sim_name: str | None) -> Simulator | None:
184177

185178
def __repr__(self):
186179
return f"<{self.name} on {self.arch}>"
180+
181+
182+
def generate_platforms(board_roots, soc_roots, arch_roots):
183+
"""Initialize and yield all Platform instances.
184+
185+
Using the provided board/soc/arch roots, determine the available
186+
platform names and load the test platform description files.
187+
188+
An exception is raised if not all platform files are valid YAML,
189+
or if not all platform names are unique.
190+
"""
191+
alias2target = {}
192+
target2board = {}
193+
target2data = {}
194+
dir2data = {}
195+
legacy_files = []
196+
197+
lb_args = Namespace(board_roots=board_roots, soc_roots=soc_roots, arch_roots=arch_roots,
198+
board=None, board_dir=None)
199+
200+
for board in list_boards.find_v2_boards(lb_args).values():
201+
for board_dir in board.directories:
202+
if board_dir in dir2data:
203+
# don't load the same board data twice
204+
continue
205+
file = board_dir / "twister.yaml"
206+
if file.is_file():
207+
data = scl.yaml_load_verify(file, Platform.platform_schema)
208+
else:
209+
data = None
210+
dir2data[board_dir] = data
211+
212+
legacy_files.extend(
213+
file for file in board_dir.glob("*.yaml") if file.name != "twister.yaml"
214+
)
215+
216+
for qual in list_boards.board_v2_qualifiers(board):
217+
if board.revisions:
218+
for rev in board.revisions:
219+
if rev.name:
220+
target = f"{board.name}@{rev.name}/{qual}"
221+
alias2target[target] = target
222+
if rev.name == board.revision_default:
223+
alias2target[f"{board.name}/{qual}"] = target
224+
if '/' not in qual and len(board.socs) == 1:
225+
if rev.name == board.revision_default:
226+
alias2target[f"{board.name}"] = target
227+
alias2target[f"{board.name}@{rev.name}"] = target
228+
else:
229+
target = f"{board.name}/{qual}"
230+
alias2target[target] = target
231+
if '/' not in qual and len(board.socs) == 1 \
232+
and rev.name == board.revision_default:
233+
alias2target[f"{board.name}"] = target
234+
235+
target2board[target] = board
236+
else:
237+
target = f"{board.name}/{qual}"
238+
alias2target[target] = target
239+
if '/' not in qual and len(board.socs) == 1:
240+
alias2target[board.name] = target
241+
target2board[target] = board
242+
243+
for board_dir, data in dir2data.items():
244+
if data is None:
245+
continue
246+
# Separate the default and variant information in the loaded board data.
247+
# The default (top-level) data can be shared by multiple board targets;
248+
# it will be overlaid by the variant data (if present) for each target.
249+
variant_data = data.pop("variants", {})
250+
for variant in variant_data:
251+
target = alias2target.get(variant)
252+
if target is None:
253+
continue
254+
if target in target2data:
255+
logger.error(f"Duplicate platform {target} in {board_dir}")
256+
raise Exception(f"Duplicate platform identifier {target} found")
257+
target2data[target] = variant_data[variant]
258+
259+
# note: this inverse mapping will only be used for loading legacy files
260+
target2aliases = {}
261+
262+
for target, aliases in groupby(alias2target, alias2target.get):
263+
aliases = list(aliases)
264+
board = target2board[target]
265+
266+
# Default board data always comes from the primary 'board.dir'.
267+
# Other 'board.directories' can only supply variant data.
268+
data = dir2data[board.dir]
269+
if data is not None:
270+
variant_data = target2data.get(target, {})
271+
272+
platform = Platform()
273+
platform.load(board, target, aliases, data, variant_data)
274+
yield platform
275+
276+
target2aliases[target] = aliases
277+
278+
for file in legacy_files:
279+
data = scl.yaml_load_verify(file, Platform.platform_schema)
280+
target = alias2target.get(data.get("identifier"))
281+
if target is None:
282+
continue
283+
284+
board = target2board[target]
285+
if dir2data[board.dir] is not None:
286+
# all targets are already loaded for this board
287+
logger.error(f"Duplicate platform {target} in {file.parent}")
288+
raise Exception(f"Duplicate platform identifier {target} found")
289+
290+
platform = Platform()
291+
platform.load(board, target, target2aliases[target], data, variant_data={})
292+
yield platform

scripts/pylib/twister/twisterlib/testplan.py

Lines changed: 5 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
# SPDX-License-Identifier: Apache-2.0
88
import collections
99
import copy
10-
import glob
1110
import itertools
1211
import json
1312
import logging
@@ -28,11 +27,10 @@
2827
except ImportError:
2928
print("Install the anytree module to use the --test-tree option")
3029

31-
import list_boards
3230
import scl
3331
from twisterlib.config_parser import TwisterConfigParser
3432
from twisterlib.error import TwisterRuntimeError
35-
from twisterlib.platform import Platform
33+
from twisterlib.platform import Platform, generate_platforms
3634
from twisterlib.quarantine import Quarantine
3735
from twisterlib.statuses import TwisterStatus
3836
from twisterlib.testinstance import TestInstance
@@ -437,99 +435,21 @@ def info(what):
437435
sys.stdout.write(what + "\n")
438436
sys.stdout.flush()
439437

440-
def find_twister_data(self, board_data_list, board_aliases):
441-
"""Find the twister data for a board in the list of board data based on the aliases"""
442-
for board_data in board_data_list:
443-
if board_data.get('identifier') in board_aliases:
444-
return board_data
445-
446438
def add_configurations(self):
447439
# Create a list of board roots as defined by the build system in general
448440
# Note, internally in twister a board root includes the `boards` folder
449441
# but in Zephyr build system, the board root is without the `boards` in folder path.
450442
board_roots = [Path(os.path.dirname(root)) for root in self.env.board_roots]
451-
lb_args = Namespace(arch_roots=self.env.arch_roots, soc_roots=self.env.soc_roots,
452-
board_roots=board_roots, board=None, board_dir=None)
443+
soc_roots = self.env.soc_roots
444+
arch_roots = self.env.arch_roots
453445

454-
known_boards = list_boards.find_v2_boards(lb_args)
455-
bdirs = {}
456446
platform_config = self.test_config.get('platforms', {})
457447

458-
# helper function to initialize and add platforms
459-
def init_and_add_platforms(data, board, target, qualifier, aliases):
460-
platform = Platform()
461-
if not new_config_found:
462-
data = self.find_twister_data(bdirs[board.dir], aliases)
463-
if not data:
464-
return
465-
platform.load(board, target, aliases, data)
466-
platform.qualifier = qualifier
467-
if platform.name in [p.name for p in self.platforms]:
468-
logger.error(f"Duplicate platform {platform.name} in {board.dir}")
469-
raise Exception(f"Duplicate platform identifier {platform.name} found")
448+
for platform in generate_platforms(board_roots, soc_roots, arch_roots):
470449
if not platform.twister:
471-
return
450+
continue
472451
self.platforms.append(platform)
473452

474-
for board in known_boards.values():
475-
new_config_found = False
476-
# don't load the same board data twice
477-
if not bdirs.get(board.dir):
478-
datas = []
479-
for file in glob.glob(os.path.join(board.dir, "*.yaml")):
480-
if os.path.basename(file) == "twister.yaml":
481-
continue
482-
try:
483-
scp = TwisterConfigParser(file, Platform.platform_schema)
484-
sdata = scp.load()
485-
datas.append(sdata)
486-
except Exception as e:
487-
logger.error(f"Error loading {file}: {e!r}")
488-
self.load_errors += 1
489-
continue
490-
bdirs[board.dir] = datas
491-
data = {}
492-
if os.path.exists(board.dir / 'twister.yaml'):
493-
try:
494-
scp = TwisterConfigParser(board.dir / 'twister.yaml', Platform.platform_schema)
495-
data = scp.load()
496-
except Exception as e:
497-
logger.error(f"Error loading {board.dir / 'twister.yaml'}: {e!r}")
498-
self.load_errors += 1
499-
continue
500-
new_config_found = True
501-
502-
503-
504-
for qual in list_boards.board_v2_qualifiers(board):
505-
506-
if board.revisions:
507-
for rev in board.revisions:
508-
if rev.name:
509-
target = f"{board.name}@{rev.name}/{qual}"
510-
aliases = [target]
511-
if rev.name == board.revision_default:
512-
aliases.append(f"{board.name}/{qual}")
513-
if '/' not in qual and len(board.socs) == 1:
514-
if rev.name == board.revision_default:
515-
aliases.append(f"{board.name}")
516-
aliases.append(f"{board.name}@{rev.name}")
517-
else:
518-
target = f"{board.name}/{qual}"
519-
aliases = [target]
520-
if '/' not in qual and len(board.socs) == 1 \
521-
and rev.name == board.revision_default:
522-
aliases.append(f"{board.name}")
523-
524-
init_and_add_platforms(data, board, target, qual, aliases)
525-
else:
526-
target = f"{board.name}/{qual}"
527-
aliases = [target]
528-
if '/' not in qual and len(board.socs) == 1:
529-
aliases.append(board.name)
530-
init_and_add_platforms(data, board, target, qual, aliases)
531-
532-
for platform in self.platforms:
533453
if not platform_config.get('override_default_platforms', False):
534454
if platform.default:
535455
self.default_platforms.append(platform.name)

0 commit comments

Comments
 (0)