Skip to content

Commit 7cc70f0

Browse files
ithinuelkartben
authored andcommitted
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 <[email protected]>
1 parent f0646d3 commit 7cc70f0

File tree

14 files changed

+193
-89
lines changed

14 files changed

+193
-89
lines changed

doc/develop/test/twister.rst

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,23 @@ name:
151151
type:
152152
Type of the board or configuration, currently we support 2 types: mcu, qemu
153153
simulation:
154-
Simulator used to simulate the platform, e.g. qemu.
154+
Simulator(s) used to simulate the platform, e.g. qemu.
155+
156+
.. code-block:: yaml
157+
158+
simulation:
159+
- name: qemu
160+
- name: armfvp
161+
exec: FVP_Some_Platform
162+
- name: custom
163+
exec: AnotherBinary
164+
165+
By default, tests will be executed using the first entry in the simulation array. Another
166+
simulation can be selected with ``--simulation <simulation_name>``.
167+
The ``exec`` attribute is optional. If it is set but the required simulator is not available, the
168+
tests will be built only.
169+
If it is not set and the required simulator is not available the tests will fail to run.
170+
The simulation name must match one of the element of ``SUPPORTED_EMU_PLATFORMS``.
155171
arch:
156172
Architecture of the board
157173
toolchain:
@@ -919,8 +935,9 @@ To use this type of simulation, add the following properties to
919935

920936
.. code-block:: yaml
921937
922-
simulation: custom
923-
simulation_exec: <name_of_emu_binary>
938+
simulation:
939+
- name: custom
940+
exec: <name_of_emu_binary>
924941
925942
This tells Twister that the board is using a custom emulator called ``<name_of_emu_binary>``,
926943
make sure this binary exists in the PATH.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved.
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
SUPPORTED_SIMS = [
8+
"mdb-nsim",
9+
"nsim",
10+
"renode",
11+
"qemu",
12+
"tsim",
13+
"armfvp",
14+
"xt-sim",
15+
"native",
16+
"custom",
17+
"simics",
18+
]
19+
SUPPORTED_SIMS_IN_PYTEST = ['native', 'qemu']
20+
SUPPORTED_SIMS_WITH_EXEC = ['nsim', 'mdb-nsim', 'renode', 'tsim', 'native', 'simics', 'custom']

scripts/pylib/twister/twisterlib/environment.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from pathlib import Path
2121
from typing import Generator, List
2222

23+
from twisterlib.constants import SUPPORTED_SIMS
2324
from twisterlib.coverage import supported_coverage_formats
2425

2526
logger = logging.getLogger('twister')
@@ -71,7 +72,7 @@ def norm_path(astring):
7172
return newstring
7273

7374

74-
def add_parse_arguments(parser = None):
75+
def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
7576
if parser is None:
7677
parser = argparse.ArgumentParser(
7778
description=__doc__,
@@ -180,6 +181,13 @@ def add_parse_arguments(parser = None):
180181
--device-testing
181182
""")
182183

184+
run_group_option.add_argument(
185+
"--simulation", dest="sim_name", choices=SUPPORTED_SIMS,
186+
help="Selects which simulation to use. Must match one of the names defined in the board's "
187+
"manifest. If multiple simulator are specified in the selected board and this "
188+
"argument is not passed, then the first simulator is selected.")
189+
190+
183191
device.add_argument("--device-serial",
184192
help="""Serial device for accessing the board
185193
(e.g., /dev/ttyACM0)
@@ -811,7 +819,7 @@ def add_parse_arguments(parser = None):
811819
return parser
812820

813821

814-
def parse_arguments(parser, args, options = None, on_init=True):
822+
def parse_arguments(parser: argparse.ArgumentParser, args, options = None, on_init=True) -> argparse.Namespace:
815823
if options is None:
816824
options = parser.parse_args(args)
817825

@@ -958,7 +966,7 @@ def strip_ansi_sequences(s: str) -> str:
958966

959967
class TwisterEnv:
960968

961-
def __init__(self, options, default_options=None) -> None:
969+
def __init__(self, options : argparse.Namespace, default_options=None) -> None:
962970
self.version = "Unknown"
963971
self.toolchain = None
964972
self.commit_date = "Unknown"

scripts/pylib/twister/twisterlib/handlers.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@
4949
logger = logging.getLogger('twister')
5050
logger.setLevel(logging.DEBUG)
5151

52-
SUPPORTED_SIMS = ["mdb-nsim", "nsim", "renode", "qemu", "tsim", "armfvp", "xt-sim", "native", "custom", "simics"]
53-
SUPPORTED_SIMS_IN_PYTEST = ['native', 'qemu']
54-
5552

5653
def terminate_process(proc):
5754
"""
@@ -242,6 +239,7 @@ def _output_handler(self, proc, harness):
242239
self.terminate(proc)
243240

244241
def _create_command(self, robot_test):
242+
245243
if robot_test:
246244
keywords = os.path.join(self.options.coverage_basedir, 'tests/robot/common.robot')
247245
elf = os.path.join(self.build_dir, "zephyr/zephyr.elf")
@@ -263,8 +261,14 @@ def _create_command(self, robot_test):
263261
"--variable", "RESC:@" + resc,
264262
"--variable", "UART:" + uart]
265263
elif self.call_make_run:
266-
command = [self.generator_cmd, "-C", self.get_default_domain_build_dir(), "run"]
264+
if self.options.sim_name:
265+
target = f"run_{self.options.sim_name}"
266+
else:
267+
target = "run"
268+
269+
command = [self.generator_cmd, "-C", self.get_default_domain_build_dir(), target]
267270
elif self.instance.testsuite.type == "unit":
271+
assert self.binary, "Missing binary in unit testsuite."
268272
command = [self.binary]
269273
else:
270274
binary = os.path.join(self.get_default_domain_build_dir(), "zephyr", "zephyr.exe")

scripts/pylib/twister/twisterlib/harness.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
from twisterlib.reports import ReportStatus
2121
from twisterlib.error import ConfigurationError, StatusAttributeError
2222
from twisterlib.environment import ZEPHYR_BASE, PYTEST_PLUGIN_INSTALLED
23-
from twisterlib.handlers import Handler, terminate_process, SUPPORTED_SIMS_IN_PYTEST
23+
from twisterlib.handlers import Handler, terminate_process
2424
from twisterlib.statuses import TwisterStatus
2525
from twisterlib.testinstance import TestInstance
26-
26+
from twisterlib.constants import SUPPORTED_SIMS_IN_PYTEST
2727

2828
logger = logging.getLogger('twister')
2929
logger.setLevel(logging.DEBUG)

scripts/pylib/twister/twisterlib/platform.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,43 @@
22
# vim: set syntax=python ts=4 :
33
#
44
# Copyright (c) 2018-2022 Intel Corporation
5+
# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved.
6+
#
57
# SPDX-License-Identifier: Apache-2.0
68

79
import os
10+
import shutil
811
import scl
912
from twisterlib.environment import ZEPHYR_BASE
13+
from twisterlib.constants import SUPPORTED_SIMS
1014
import logging
1115

1216
logger = logging.getLogger('twister')
1317
logger.setLevel(logging.DEBUG)
1418

19+
20+
class Simulator:
21+
"""Class representing a simulator"""
22+
23+
def __init__(self, data: dict[str, str]):
24+
assert "name" in data
25+
assert data["name"] in SUPPORTED_SIMS
26+
self.name = data["name"]
27+
self.exec = data.get("exec")
28+
29+
def is_runnable(self) -> bool:
30+
return not bool(self.exec) or bool(shutil.which(self.exec))
31+
32+
def __str__(self):
33+
return f"Simulator(name: {self.name}, exec: {self.exec})"
34+
35+
def __eq__(self, other):
36+
if isinstance(other, Simulator):
37+
return self.name == other.name and self.exec == other.exec
38+
else:
39+
return False
40+
41+
1542
class Platform:
1643
"""Class representing metadata for a particular platform
1744
@@ -46,8 +73,8 @@ def __init__(self):
4673
self.vendor = ""
4774
self.tier = -1
4875
self.type = "na"
49-
self.simulation = "na"
50-
self.simulation_exec = None
76+
self.simulators: list[Simulator] = []
77+
self.simulation: str = "na"
5178
self.supported_toolchains = []
5279
self.env = []
5380
self.env_satisfied = True
@@ -103,8 +130,12 @@ def load(self, board, target, aliases, data):
103130
self.vendor = board.vendor
104131
self.tier = variant_data.get("tier", data.get("tier", self.tier))
105132
self.type = variant_data.get('type', data.get('type', self.type))
106-
self.simulation = variant_data.get('simulation', data.get('simulation', self.simulation))
107-
self.simulation_exec = variant_data.get('simulation_exec', data.get('simulation_exec', self.simulation_exec))
133+
134+
self.simulators = [Simulator(data) for data in variant_data.get('simulation', data.get('simulation', self.simulators))]
135+
default_sim = self.simulator_by_name(None)
136+
if default_sim:
137+
self.simulation = default_sim.name
138+
108139
self.supported_toolchains = variant_data.get("toolchain", data.get("toolchain", []))
109140
if self.supported_toolchains is None:
110141
self.supported_toolchains = []
@@ -138,5 +169,11 @@ def load(self, board, target, aliases, data):
138169
if not os.environ.get(env, None):
139170
self.env_satisfied = False
140171

172+
def simulator_by_name(self, sim_name: str | None) -> Simulator | None:
173+
if sim_name:
174+
return next(filter(lambda s: s.name == sim_name, iter(self.simulators)), None)
175+
else:
176+
return next(iter(self.simulators), None)
177+
141178
def __repr__(self):
142179
return "<%s on %s>" % (self.name, self.arch)

scripts/pylib/twister/twisterlib/quarantine.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Copyright (c) 2022 Nordic Semiconductor ASA
2+
# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved.
3+
#
24
# SPDX-License-Identifier: Apache-2.0
35

46
from __future__ import annotations
@@ -31,8 +33,8 @@ def __init__(self, quarantine_list=[]) -> None:
3133
for quarantine_file in quarantine_list:
3234
self.quarantine.extend(QuarantineData.load_data_from_yaml(quarantine_file))
3335

34-
def get_matched_quarantine(self, testname, platform, architecture, simulation):
35-
qelem = self.quarantine.get_matched_quarantine(testname, platform, architecture, simulation)
36+
def get_matched_quarantine(self, testname, platform, architecture, simulator):
37+
qelem = self.quarantine.get_matched_quarantine(testname, platform, architecture, simulator)
3638
if qelem:
3739
logger.debug('%s quarantined with reason: %s' % (testname, qelem.comment))
3840
return qelem.comment
@@ -111,7 +113,7 @@ def get_matched_quarantine(self,
111113
scenario: str,
112114
platform: str,
113115
architecture: str,
114-
simulation: str) -> QuarantineElement | None:
116+
simulator_name: str) -> QuarantineElement | None:
115117
"""Return quarantine element if test is matched to quarantine rules"""
116118
for qelem in self.qlist:
117119
matched: bool = False
@@ -125,7 +127,7 @@ def get_matched_quarantine(self,
125127
and (matched := _is_element_matched(architecture, qelem.re_architectures)) is False):
126128
continue
127129
if (qelem.simulations
128-
and (matched := _is_element_matched(simulation, qelem.re_simulations)) is False):
130+
and (matched := _is_element_matched(simulator_name, qelem.re_simulations)) is False):
129131
continue
130132

131133
if matched:

scripts/pylib/twister/twisterlib/testinstance.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import hashlib
1212
import random
1313
import logging
14-
import shutil
1514
import glob
1615
import csv
1716

@@ -28,8 +27,11 @@
2827
QEMUHandler,
2928
QEMUWinHandler,
3029
DeviceHandler,
30+
)
31+
from twisterlib.constants import (
3132
SUPPORTED_SIMS,
3233
SUPPORTED_SIMS_IN_PYTEST,
34+
SUPPORTED_SIMS_WITH_EXEC,
3335
)
3436

3537
logger = logging.getLogger('twister')
@@ -211,23 +213,23 @@ def setup_handler(self, env: TwisterEnv):
211213

212214
options = env.options
213215
common_args = (options, env.generator_cmd, not options.disable_suite_name_check)
216+
simulator = self.platform.simulator_by_name(options.sim_name)
214217
if options.device_testing:
215218
handler = DeviceHandler(self, "device", *common_args)
216219
handler.call_make_run = False
217220
handler.ready = True
218-
elif self.platform.simulation != "na":
219-
if self.platform.simulation == "qemu":
221+
elif simulator:
222+
if simulator.name == "qemu":
220223
if os.name != "nt":
221224
handler = QEMUHandler(self, "qemu", *common_args)
222225
else:
223226
handler = QEMUWinHandler(self, "qemu", *common_args)
224227
handler.args.append(f"QEMU_PIPE={handler.get_fifo()}")
225228
handler.ready = True
226229
else:
227-
handler = SimulationHandler(self, self.platform.simulation, *common_args)
230+
handler = SimulationHandler(self, simulator.name, *common_args)
231+
handler.ready = simulator.is_runnable()
228232

229-
if self.platform.simulation_exec and shutil.which(self.platform.simulation_exec):
230-
handler.ready = True
231233
elif self.testsuite.type == "unit":
232234
handler = BinaryHandler(self, "unit", *common_args)
233235
handler.binary = os.path.join(self.build_dir, "testbinary")
@@ -242,21 +244,23 @@ def setup_handler(self, env: TwisterEnv):
242244

243245
# Global testsuite parameters
244246
def check_runnable(self,
245-
options,
246-
hardware_map=None):
247+
options: TwisterEnv,
248+
hardware_map=None):
247249

248250
enable_slow = options.enable_slow
249251
filter = options.filter
250252
fixtures = options.fixture
251253
device_testing = options.device_testing
254+
simulation = options.sim_name
252255

256+
simulator = self.platform.simulator_by_name(simulation)
253257
if os.name == 'nt':
254258
# running on simulators is currently supported only for QEMU on Windows
255-
if self.platform.simulation not in ('na', 'qemu'):
259+
if (not simulator) or simulator.name not in ('na', 'qemu'):
256260
return False
257261

258262
# check presence of QEMU on Windows
259-
if self.platform.simulation == 'qemu' and 'QEMU_BIN_PATH' not in os.environ:
263+
if simulator.name == 'qemu' and 'QEMU_BIN_PATH' not in os.environ:
260264
return False
261265

262266
# we asked for build-only on the command line
@@ -269,20 +273,20 @@ def check_runnable(self,
269273
return False
270274

271275
target_ready = bool(self.testsuite.type == "unit" or \
272-
self.platform.type == "native" or \
273-
(self.platform.simulation in SUPPORTED_SIMS and \
274-
self.platform.simulation not in self.testsuite.simulation_exclude) or device_testing)
276+
self.platform.type == "native" or \
277+
(simulator and simulator.name in SUPPORTED_SIMS and \
278+
simulator.name not in self.testsuite.simulation_exclude) or \
279+
device_testing)
275280

276281
# check if test is runnable in pytest
277282
if self.testsuite.harness == 'pytest':
278-
target_ready = bool(filter == 'runnable' or self.platform.simulation in SUPPORTED_SIMS_IN_PYTEST)
283+
target_ready = bool(filter == 'runnable' or simulator and simulator.name in SUPPORTED_SIMS_IN_PYTEST)
279284

280-
SUPPORTED_SIMS_WITH_EXEC = ['nsim', 'mdb-nsim', 'renode', 'tsim', 'native', 'simics', 'custom']
281285
if filter != 'runnable' and \
282-
self.platform.simulation in SUPPORTED_SIMS_WITH_EXEC and \
283-
self.platform.simulation_exec:
284-
if not shutil.which(self.platform.simulation_exec):
285-
target_ready = False
286+
simulator and \
287+
simulator.name in SUPPORTED_SIMS_WITH_EXEC and \
288+
not simulator.is_runnable():
289+
target_ready = False
286290

287291
testsuite_runnable = self.testsuite_runnable(self.testsuite, fixtures)
288292

0 commit comments

Comments
 (0)