scripts: twisterlib: Enable multiple simulator support in twister

This change introduces the ability in twister to select which
emulation/simulation tool to use on the command line.

If none is specified, it will select the first in the list.

Signed-off-by: Wilfried Chauveau <wilfried.chauveau@arm.com>
This commit is contained in:
Wilfried Chauveau 2024-10-16 17:35:10 +01:00 committed by Benjamin Cabé
commit 7cc70f01f1
14 changed files with 193 additions and 89 deletions

View file

@ -151,7 +151,23 @@ name:
type:
Type of the board or configuration, currently we support 2 types: mcu, qemu
simulation:
Simulator used to simulate the platform, e.g. qemu.
Simulator(s) used to simulate the platform, e.g. qemu.
.. code-block:: yaml
simulation:
- name: qemu
- name: armfvp
exec: FVP_Some_Platform
- name: custom
exec: AnotherBinary
By default, tests will be executed using the first entry in the simulation array. Another
simulation can be selected with ``--simulation <simulation_name>``.
The ``exec`` attribute is optional. If it is set but the required simulator is not available, the
tests will be built only.
If it is not set and the required simulator is not available the tests will fail to run.
The simulation name must match one of the element of ``SUPPORTED_EMU_PLATFORMS``.
arch:
Architecture of the board
toolchain:
@ -919,8 +935,9 @@ To use this type of simulation, add the following properties to
.. code-block:: yaml
simulation: custom
simulation_exec: <name_of_emu_binary>
simulation:
- name: custom
exec: <name_of_emu_binary>
This tells Twister that the board is using a custom emulator called ``<name_of_emu_binary>``,
make sure this binary exists in the PATH.

View file

@ -0,0 +1,20 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
SUPPORTED_SIMS = [
"mdb-nsim",
"nsim",
"renode",
"qemu",
"tsim",
"armfvp",
"xt-sim",
"native",
"custom",
"simics",
]
SUPPORTED_SIMS_IN_PYTEST = ['native', 'qemu']
SUPPORTED_SIMS_WITH_EXEC = ['nsim', 'mdb-nsim', 'renode', 'tsim', 'native', 'simics', 'custom']

View file

@ -20,6 +20,7 @@ from importlib import metadata
from pathlib import Path
from typing import Generator, List
from twisterlib.constants import SUPPORTED_SIMS
from twisterlib.coverage import supported_coverage_formats
logger = logging.getLogger('twister')
@ -71,7 +72,7 @@ def norm_path(astring):
return newstring
def add_parse_arguments(parser = None):
def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
if parser is None:
parser = argparse.ArgumentParser(
description=__doc__,
@ -180,6 +181,13 @@ Artificially long but functional example:
--device-testing
""")
run_group_option.add_argument(
"--simulation", dest="sim_name", choices=SUPPORTED_SIMS,
help="Selects which simulation to use. Must match one of the names defined in the board's "
"manifest. If multiple simulator are specified in the selected board and this "
"argument is not passed, then the first simulator is selected.")
device.add_argument("--device-serial",
help="""Serial device for accessing the board
(e.g., /dev/ttyACM0)
@ -811,7 +819,7 @@ structure in the main Zephyr tree: boards/<vendor>/<board_name>/""")
return parser
def parse_arguments(parser, args, options = None, on_init=True):
def parse_arguments(parser: argparse.ArgumentParser, args, options = None, on_init=True) -> argparse.Namespace:
if options is None:
options = parser.parse_args(args)
@ -958,7 +966,7 @@ def strip_ansi_sequences(s: str) -> str:
class TwisterEnv:
def __init__(self, options, default_options=None) -> None:
def __init__(self, options : argparse.Namespace, default_options=None) -> None:
self.version = "Unknown"
self.toolchain = None
self.commit_date = "Unknown"

View file

@ -49,9 +49,6 @@ except ImportError as capture_error:
logger = logging.getLogger('twister')
logger.setLevel(logging.DEBUG)
SUPPORTED_SIMS = ["mdb-nsim", "nsim", "renode", "qemu", "tsim", "armfvp", "xt-sim", "native", "custom", "simics"]
SUPPORTED_SIMS_IN_PYTEST = ['native', 'qemu']
def terminate_process(proc):
"""
@ -242,6 +239,7 @@ class BinaryHandler(Handler):
self.terminate(proc)
def _create_command(self, robot_test):
if robot_test:
keywords = os.path.join(self.options.coverage_basedir, 'tests/robot/common.robot')
elf = os.path.join(self.build_dir, "zephyr/zephyr.elf")
@ -263,8 +261,14 @@ class BinaryHandler(Handler):
"--variable", "RESC:@" + resc,
"--variable", "UART:" + uart]
elif self.call_make_run:
command = [self.generator_cmd, "-C", self.get_default_domain_build_dir(), "run"]
if self.options.sim_name:
target = f"run_{self.options.sim_name}"
else:
target = "run"
command = [self.generator_cmd, "-C", self.get_default_domain_build_dir(), target]
elif self.instance.testsuite.type == "unit":
assert self.binary, "Missing binary in unit testsuite."
command = [self.binary]
else:
binary = os.path.join(self.get_default_domain_build_dir(), "zephyr", "zephyr.exe")

View file

@ -20,10 +20,10 @@ from pytest import ExitCode
from twisterlib.reports import ReportStatus
from twisterlib.error import ConfigurationError, StatusAttributeError
from twisterlib.environment import ZEPHYR_BASE, PYTEST_PLUGIN_INSTALLED
from twisterlib.handlers import Handler, terminate_process, SUPPORTED_SIMS_IN_PYTEST
from twisterlib.handlers import Handler, terminate_process
from twisterlib.statuses import TwisterStatus
from twisterlib.testinstance import TestInstance
from twisterlib.constants import SUPPORTED_SIMS_IN_PYTEST
logger = logging.getLogger('twister')
logger.setLevel(logging.DEBUG)

View file

@ -2,16 +2,43 @@
# vim: set syntax=python ts=4 :
#
# Copyright (c) 2018-2022 Intel Corporation
# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
import os
import shutil
import scl
from twisterlib.environment import ZEPHYR_BASE
from twisterlib.constants import SUPPORTED_SIMS
import logging
logger = logging.getLogger('twister')
logger.setLevel(logging.DEBUG)
class Simulator:
"""Class representing a simulator"""
def __init__(self, data: dict[str, str]):
assert "name" in data
assert data["name"] in SUPPORTED_SIMS
self.name = data["name"]
self.exec = data.get("exec")
def is_runnable(self) -> bool:
return not bool(self.exec) or bool(shutil.which(self.exec))
def __str__(self):
return f"Simulator(name: {self.name}, exec: {self.exec})"
def __eq__(self, other):
if isinstance(other, Simulator):
return self.name == other.name and self.exec == other.exec
else:
return False
class Platform:
"""Class representing metadata for a particular platform
@ -46,8 +73,8 @@ class Platform:
self.vendor = ""
self.tier = -1
self.type = "na"
self.simulation = "na"
self.simulation_exec = None
self.simulators: list[Simulator] = []
self.simulation: str = "na"
self.supported_toolchains = []
self.env = []
self.env_satisfied = True
@ -103,8 +130,12 @@ class Platform:
self.vendor = board.vendor
self.tier = variant_data.get("tier", data.get("tier", self.tier))
self.type = variant_data.get('type', data.get('type', self.type))
self.simulation = variant_data.get('simulation', data.get('simulation', self.simulation))
self.simulation_exec = variant_data.get('simulation_exec', data.get('simulation_exec', self.simulation_exec))
self.simulators = [Simulator(data) for data in variant_data.get('simulation', data.get('simulation', self.simulators))]
default_sim = self.simulator_by_name(None)
if default_sim:
self.simulation = default_sim.name
self.supported_toolchains = variant_data.get("toolchain", data.get("toolchain", []))
if self.supported_toolchains is None:
self.supported_toolchains = []
@ -138,5 +169,11 @@ class Platform:
if not os.environ.get(env, None):
self.env_satisfied = False
def simulator_by_name(self, sim_name: str | None) -> Simulator | None:
if sim_name:
return next(filter(lambda s: s.name == sim_name, iter(self.simulators)), None)
else:
return next(iter(self.simulators), None)
def __repr__(self):
return "<%s on %s>" % (self.name, self.arch)

View file

@ -1,4 +1,6 @@
# Copyright (c) 2022 Nordic Semiconductor ASA
# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
@ -31,8 +33,8 @@ class Quarantine:
for quarantine_file in quarantine_list:
self.quarantine.extend(QuarantineData.load_data_from_yaml(quarantine_file))
def get_matched_quarantine(self, testname, platform, architecture, simulation):
qelem = self.quarantine.get_matched_quarantine(testname, platform, architecture, simulation)
def get_matched_quarantine(self, testname, platform, architecture, simulator):
qelem = self.quarantine.get_matched_quarantine(testname, platform, architecture, simulator)
if qelem:
logger.debug('%s quarantined with reason: %s' % (testname, qelem.comment))
return qelem.comment
@ -111,7 +113,7 @@ class QuarantineData:
scenario: str,
platform: str,
architecture: str,
simulation: str) -> QuarantineElement | None:
simulator_name: str) -> QuarantineElement | None:
"""Return quarantine element if test is matched to quarantine rules"""
for qelem in self.qlist:
matched: bool = False
@ -125,7 +127,7 @@ class QuarantineData:
and (matched := _is_element_matched(architecture, qelem.re_architectures)) is False):
continue
if (qelem.simulations
and (matched := _is_element_matched(simulation, qelem.re_simulations)) is False):
and (matched := _is_element_matched(simulator_name, qelem.re_simulations)) is False):
continue
if matched:

View file

@ -11,7 +11,6 @@ import os
import hashlib
import random
import logging
import shutil
import glob
import csv
@ -28,8 +27,11 @@ from twisterlib.handlers import (
QEMUHandler,
QEMUWinHandler,
DeviceHandler,
)
from twisterlib.constants import (
SUPPORTED_SIMS,
SUPPORTED_SIMS_IN_PYTEST,
SUPPORTED_SIMS_WITH_EXEC,
)
logger = logging.getLogger('twister')
@ -211,12 +213,13 @@ class TestInstance:
options = env.options
common_args = (options, env.generator_cmd, not options.disable_suite_name_check)
simulator = self.platform.simulator_by_name(options.sim_name)
if options.device_testing:
handler = DeviceHandler(self, "device", *common_args)
handler.call_make_run = False
handler.ready = True
elif self.platform.simulation != "na":
if self.platform.simulation == "qemu":
elif simulator:
if simulator.name == "qemu":
if os.name != "nt":
handler = QEMUHandler(self, "qemu", *common_args)
else:
@ -224,10 +227,9 @@ class TestInstance:
handler.args.append(f"QEMU_PIPE={handler.get_fifo()}")
handler.ready = True
else:
handler = SimulationHandler(self, self.platform.simulation, *common_args)
handler = SimulationHandler(self, simulator.name, *common_args)
handler.ready = simulator.is_runnable()
if self.platform.simulation_exec and shutil.which(self.platform.simulation_exec):
handler.ready = True
elif self.testsuite.type == "unit":
handler = BinaryHandler(self, "unit", *common_args)
handler.binary = os.path.join(self.build_dir, "testbinary")
@ -242,21 +244,23 @@ class TestInstance:
# Global testsuite parameters
def check_runnable(self,
options,
hardware_map=None):
options: TwisterEnv,
hardware_map=None):
enable_slow = options.enable_slow
filter = options.filter
fixtures = options.fixture
device_testing = options.device_testing
simulation = options.sim_name
simulator = self.platform.simulator_by_name(simulation)
if os.name == 'nt':
# running on simulators is currently supported only for QEMU on Windows
if self.platform.simulation not in ('na', 'qemu'):
if (not simulator) or simulator.name not in ('na', 'qemu'):
return False
# check presence of QEMU on Windows
if self.platform.simulation == 'qemu' and 'QEMU_BIN_PATH' not in os.environ:
if simulator.name == 'qemu' and 'QEMU_BIN_PATH' not in os.environ:
return False
# we asked for build-only on the command line
@ -269,20 +273,20 @@ class TestInstance:
return False
target_ready = bool(self.testsuite.type == "unit" or \
self.platform.type == "native" or \
(self.platform.simulation in SUPPORTED_SIMS and \
self.platform.simulation not in self.testsuite.simulation_exclude) or device_testing)
self.platform.type == "native" or \
(simulator and simulator.name in SUPPORTED_SIMS and \
simulator.name not in self.testsuite.simulation_exclude) or \
device_testing)
# check if test is runnable in pytest
if self.testsuite.harness == 'pytest':
target_ready = bool(filter == 'runnable' or self.platform.simulation in SUPPORTED_SIMS_IN_PYTEST)
target_ready = bool(filter == 'runnable' or simulator and simulator.name in SUPPORTED_SIMS_IN_PYTEST)
SUPPORTED_SIMS_WITH_EXEC = ['nsim', 'mdb-nsim', 'renode', 'tsim', 'native', 'simics', 'custom']
if filter != 'runnable' and \
self.platform.simulation in SUPPORTED_SIMS_WITH_EXEC and \
self.platform.simulation_exec:
if not shutil.which(self.platform.simulation_exec):
target_ready = False
simulator and \
simulator.name in SUPPORTED_SIMS_WITH_EXEC and \
not simulator.is_runnable():
target_ready = False
testsuite_runnable = self.testsuite_runnable(self.testsuite, fixtures)

View file

@ -2,6 +2,8 @@
# vim: set syntax=python ts=4 :
#
# Copyright (c) 2018 Intel Corporation
# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
import os
import sys
@ -14,7 +16,6 @@ from collections import OrderedDict
from itertools import islice
import logging
import copy
import shutil
import random
import snippets
from pathlib import Path
@ -95,7 +96,7 @@ class TestPlan:
SAMPLE_FILENAME = 'sample.yaml'
TESTSUITE_FILENAME = 'testcase.yaml'
def __init__(self, env=None):
def __init__(self, env: Namespace):
self.options = env.options
self.env = env
@ -123,6 +124,7 @@ class TestPlan:
self.levels = []
self.test_config = {}
self.name = "unnamed"
def get_level(self, name):
level = next((l for l in self.levels if l.name == name), None)
@ -157,8 +159,9 @@ class TestPlan:
if inherit:
for inherted_level in inherit:
_inherited = self.get_level(inherted_level)
assert _inherited, "Unknown inherited level {inherted_level}"
_inherited_scenarios = _inherited.scenarios
level_scenarios = _level.scenarios
level_scenarios = _level.scenarios if _level else []
level_scenarios.extend(_inherited_scenarios)
def find_subtests(self):
@ -627,8 +630,9 @@ class TestPlan:
def handle_quarantined_tests(self, instance: TestInstance, plat: Platform):
if self.quarantine:
simulator = plat.simulator_by_name(self.options)
matched_quarantine = self.quarantine.get_matched_quarantine(
instance.testsuite.id, plat.name, plat.arch, plat.simulation
instance.testsuite.id, plat.name, plat.arch, simulator.name if simulator is not None else 'na'
)
if matched_quarantine and not self.options.quarantine_verify:
instance.add_filter("Quarantine: " + matched_quarantine, Filters.QUARANTINE)
@ -773,7 +777,7 @@ class TestPlan:
platform_filter = _platforms
platforms = list(filter(lambda p: p.name in platform_filter, self.platforms))
elif emu_filter:
platforms = list(filter(lambda p: p.simulation != 'na', self.platforms))
platforms = list(filter(lambda p: bool(p.simulator_by_name(self.options.sim_name)), self.platforms))
elif vendor_filter:
platforms = list(filter(lambda p: p.vendor in vendor_filter, self.platforms))
logger.info(f"Selecting platforms by vendors: {','.join(vendor_filter)}")
@ -786,10 +790,8 @@ class TestPlan:
# the default platforms list. Default platforms should always be
# runnable.
for p in _platforms:
if p.simulation and p.simulation_exec:
if shutil.which(p.simulation_exec):
platforms.append(p)
else:
sim = p.simulator_by_name(self.options.sim_name)
if (not sim) or sim.is_runnable():
platforms.append(p)
else:
platforms = self.platforms
@ -931,7 +933,8 @@ class TestPlan:
instance.add_filter("Not enough RAM", Filters.PLATFORM)
if ts.harness:
if ts.harness == 'robot' and plat.simulation != 'renode':
sim = plat.simulator_by_name(self.options.sim_name)
if ts.harness == 'robot' and sim and sim.name != 'renode':
instance.add_filter("No robot support for the selected platform", Filters.SKIP)
if ts.depends_on:
@ -999,7 +1002,7 @@ class TestPlan:
# to run a test once per unique (arch, simulation) platform.
if not ignore_platform_key and hasattr(ts, 'platform_key') and len(ts.platform_key) > 0:
key_fields = sorted(set(ts.platform_key))
keys = [getattr(plat, key_field) for key_field in key_fields]
keys = [getattr(plat, key_field, None) for key_field in key_fields]
for key in keys:
if key is None or key == 'na':
instance.add_filter(
@ -1054,7 +1057,8 @@ class TestPlan:
elif emulation_platforms:
self.add_instances(instance_list)
for instance in list(filter(lambda inst: not inst.platform.simulation != 'na', instance_list)):
for instance in list(filter(lambda inst: not
inst.platform.simulator_by_name(self.options.sim_name), instance_list)):
instance.add_filter("Not an emulated platform", Filters.CMD_LINE)
elif vendor_platforms:
self.add_instances(instance_list)

View file

@ -30,22 +30,28 @@ schema;platform-schema:
type: str
enum: ["mcu", "qemu", "sim", "unit", "native"]
"simulation":
type: str
enum:
[
"qemu",
"simics",
"xt-sim",
"renode",
"nsim",
"mdb-nsim",
"tsim",
"armfvp",
"native",
"custom",
]
"simulation_exec":
type: str
type: seq
seq:
- type: map
mapping:
"name":
type: str
required: true
enum:
[
"qemu",
"simics",
"xt-sim",
"renode",
"nsim",
"mdb-nsim",
"tsim",
"armfvp",
"native",
"custom",
]
"exec":
type: str
"arch":
type: str
enum:

View file

@ -443,6 +443,7 @@ def test_binaryhandler_create_command(
options = SimpleNamespace()
options.enable_valgrind = enable_valgrind
options.coverage_basedir = "coverage_basedir"
options.sim_name = None
handler = BinaryHandler(mocked_instance, 'build', options, 'generator', False)
handler.binary = 'bin'
handler.call_make_run = call_make_run

View file

@ -14,7 +14,7 @@ import pytest
ZEPHYR_BASE = os.getenv("ZEPHYR_BASE")
sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/pylib/twister"))
from twisterlib.platform import Platform
from twisterlib.platform import Platform, Simulator
TESTDATA_1 = [
@ -38,8 +38,7 @@ arch: arc
'vendor': '',
'tier': -1,
'type': 'na',
'simulation': 'na',
'simulation_exec': None,
'simulators': [],
'supported_toolchains': [],
'env': [],
'env_satisfied': True
@ -71,8 +70,9 @@ supported:
vendor: vendor1
tier: 1
type: unit
simulation: nsim
simulation_exec: nsimdrv
simulation:
- name: nsim
exec: nsimdrv
toolchain:
- zephyr
- llvm
@ -94,8 +94,7 @@ env:
'vendor': 'vendor1',
'tier': 1,
'type': 'unit',
'simulation': 'nsim',
'simulation_exec': 'nsimdrv',
'simulators': [Simulator({'name': 'nsim', 'exec': 'nsimdrv'})],
'supported_toolchains': ['zephyr', 'llvm', 'cross-compile'],
'env': ['dummynonexistentvar'],
'env_satisfied': False

View file

@ -263,12 +263,12 @@ def test_quarantinedata_get_matched_quarantine(
scenario=scenario,
platform=platform,
architecture=architecture,
simulation=simulation
simulator_name=simulation
) is None
else:
assert quarantine_data.get_matched_quarantine(
scenario=scenario,
platform=platform,
architecture=architecture,
simulation=simulation
simulator_name=simulation
) == qlist[expected_idx]

View file

@ -16,6 +16,7 @@ import mock
ZEPHYR_BASE = os.getenv("ZEPHYR_BASE")
sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/pylib/twister"))
from pylib.twister.twisterlib.platform import Simulator
from twisterlib.statuses import TwisterStatus
from twisterlib.testinstance import TestInstance
from twisterlib.error import BuildError
@ -25,12 +26,12 @@ from expr_parser import reserved
TESTDATA_PART_1 = [
(False, False, "console", "na", "qemu", False, [], (False, True)),
(False, False, "console", None, "qemu", False, [], (False, True)),
(False, False, "console", "native", "qemu", False, [], (False, True)),
(True, False, "console", "native", "nsim", False, [], (True, False)),
(True, True, "console", "native", "renode", False, [], (True, False)),
(False, False, "sensor", "native", "", False, [], (True, False)),
(False, False, "sensor", "na", "", False, [], (True, False)),
(False, False, "sensor", None, "", False, [], (True, False)),
(False, True, "sensor", "native", "", True, [], (True, False)),
]
@pytest.mark.parametrize(
@ -62,7 +63,7 @@ def test_check_build_or_run(
class_testplan.platforms = platforms_list
platform = class_testplan.get_platform("demo_board_2")
platform.type = platform_type
platform.simulation = platform_sim
platform.simulators = [Simulator({"name": platform_sim})] if platform_sim else []
testsuite.harness = harness
testsuite.build_only = build_only
testsuite.slow = slow
@ -73,7 +74,8 @@ def test_check_build_or_run(
device_testing=False,
enable_slow=slow,
fixtures=fixture,
filter=""
filter="",
sim_name=platform_sim
)
)
run = testinstance.check_runnable(env.options)
@ -455,9 +457,9 @@ TESTDATA_4 = [
(True, mock.ANY, mock.ANY, mock.ANY, None, [], False),
(False, True, mock.ANY, mock.ANY, 'device', [], True),
(False, False, 'qemu', mock.ANY, 'qemu', ['QEMU_PIPE=1'], True),
(False, False, 'dummy sim', mock.ANY, 'dummy sim', [], True),
(False, False, 'na', 'unit', 'unit', ['COVERAGE=1'], True),
(False, False, 'na', 'dummy type', '', [], False),
(False, False, 'armfvp', mock.ANY, 'armfvp', [], True),
(False, False, None, 'unit', 'unit', ['COVERAGE=1'], True),
(False, False, None, 'dummy type', '', [], False),
]
@pytest.mark.parametrize(
@ -479,13 +481,13 @@ def test_testinstance_setup_handler(
expected_handler_ready
):
testinstance.handler = mock.Mock() if preexisting_handler else None
testinstance.platform.simulation = platform_sim
testinstance.platform.simulation_exec = 'dummy exec'
testinstance.platform.simulators = [Simulator({"name": platform_sim, "exec": 'dummy exec'})] if platform_sim else []
testinstance.testsuite.type = testsuite_type
env = mock.Mock(
options=mock.Mock(
device_testing=device_testing,
enable_coverage=True
enable_coverage=True,
sim_name=platform_sim,
)
)
@ -546,8 +548,7 @@ def test_testinstance_check_runnable(
hardware_map,
expected
):
testinstance.platform.simulation = platform_sim
testinstance.platform.simulation_exec = platform_sim_exec
testinstance.platform.simulators = [Simulator({"name": platform_sim, "exec": platform_sim_exec})]
testinstance.testsuite.build_only = testsuite_build_only
testinstance.testsuite.slow = testsuite_slow
testinstance.testsuite.harness = testsuite_harness
@ -557,7 +558,8 @@ def test_testinstance_check_runnable(
device_testing=False,
enable_slow=enable_slow,
fixtures=fixtures,
filter=filter
filter=filter,
sim_name=platform_sim
)
)
with mock.patch('os.name', os_name), \