Skip to content

Commit 58d2834

Browse files
committed
scripts: twister: adjusting twister to run Bsim more efficiently
BSIM tests might be reusing the same binary files, so it makes sense to be able to split tests into build-only, which will only build and copy executable files, and run only, which will run them whenever needed. This requires more sequential pipeline to avoid race conitions. Signed-off-by: Artur Dobrynin <[email protected]>
1 parent 64de10f commit 58d2834

File tree

5 files changed

+137
-47
lines changed

5 files changed

+137
-47
lines changed

scripts/pylib/twister/twisterlib/config_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class TwisterConfigParser:
5757
"extra_dtc_overlay_files": {"type": "list", "default": []},
5858
"required_snippets": {"type": "list"},
5959
"build_only": {"type": "bool", "default": False},
60+
"no_build": {"type": "bool", "default": False},
6061
"build_on_all": {"type": "bool", "default": False},
6162
"skip": {"type": "bool", "default": False},
6263
"slow": {"type": "bool", "default": False},

scripts/pylib/twister/twisterlib/harness.py

Lines changed: 93 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class Harness:
4141
RUN_FAILED = "PROJECT EXECUTION FAILED"
4242
run_id_pattern = r"RunID: (?P<run_id>.*)"
4343

44+
cacheable = False
45+
4446
def __init__(self):
4547
self._status = TwisterStatus.NONE
4648
self.reason = None
@@ -967,37 +969,62 @@ class Bsim(Harness):
967969
DEFAULT_VERBOSITY = 2
968970
DEFAULT_SIM_LENGTH = 60e6
969971

972+
BSIM_READY_TIMEOUT_S = 20
973+
974+
cacheable = True
975+
970976
def __init__(self):
971977
super().__init__()
972978
self._bsim_out_path = os.getenv('BSIM_OUT_PATH', '')
973-
self._exe_path = None
979+
self._exe_paths = []
974980
self._tc_output = []
981+
self._start_time = 0
975982

976-
@property
977-
def exe_path(self):
978-
if self._exe_path:
979-
return self._exe_path
983+
def _set_start_time(self):
984+
self._start_time = time.time()
985+
986+
def _get_exe_path(self, index):
987+
return self._exe_paths[index if index < len(self._exe_paths) else 0]
988+
989+
def configure(self, instance):
990+
super().configure(instance)
980991

981992
if not self._bsim_out_path:
982-
logger.warning('Cannot copy bsim exe - BSIM_OUT_PATH not provided.')
983-
return self._exe_path
993+
raise Exception('Cannot copy bsim exe - BSIM_OUT_PATH not provided.')
984994

985-
new_exe_name: str = self.instance.testsuite.harness_config.get('bsim_exe_name', '')
986-
if new_exe_name:
987-
new_exe_name = f'bs_{self.instance.platform.name}_{new_exe_name}'
988-
else:
989-
new_exe_name = f'bs_{self.instance.name}'
995+
exe_names = []
996+
for exe_name in self.instance.testsuite.harness_config.get('bsim_exe_name', []):
997+
new_exe_name = f'bs_{self.instance.platform.name}_{exe_name}'
998+
exe_names.append(
999+
new_exe_name.replace(os.path.sep, '_').replace('.', '_').replace('@', '_'))
1000+
1001+
if not exe_names:
1002+
exe_names = [f'bs_{self.instance.name}']
9901003

991-
new_exe_name = new_exe_name.replace(os.path.sep, '_').replace('.', '_').replace('@', '_')
992-
self._exe_path = os.path.join(self._bsim_out_path, 'bin', new_exe_name)
993-
return self._exe_path
1004+
self._exe_paths = \
1005+
[os.path.join(self._bsim_out_path, 'bin', exe_name) for exe_name in exe_names]
1006+
1007+
def clean_exes(self):
1008+
for exe_path in [self._get_exe_path(i) for i in range(len(self._exe_paths))]:
1009+
if os.path.exists(exe_path):
1010+
os.remove(exe_path)
1011+
1012+
self._set_start_time()
1013+
1014+
def wait_bsim_ready(self):
1015+
start_time = time.time()
1016+
while time.time() - start_time < Bsim.BSIM_READY_TIMEOUT_S:
1017+
if all([os.path.exists(self._get_exe_path(i)) for i in range(len(self._exe_paths))]):
1018+
return True
1019+
time.sleep(0.1)
1020+
1021+
return False
9941022

9951023
def build(self):
9961024
"""
9971025
Copying the application executable to BabbleSim's bin directory enables
9981026
running multidevice bsim tests after twister has built them.
9991027
"""
1000-
10011028
if self.instance is None:
10021029
return
10031030

@@ -1006,8 +1033,16 @@ def build(self):
10061033
logger.warning('Cannot copy bsim exe - cannot find original executable.')
10071034
return
10081035

1009-
logger.debug(f'Copying executable from {original_exe_path} to {self.exe_path}')
1010-
shutil.copy(original_exe_path, self.exe_path)
1036+
try:
1037+
new_exe_path = self._get_exe_path(0)
1038+
logger.debug(f'Copying executable from {original_exe_path} to {new_exe_path}')
1039+
shutil.copy(original_exe_path, new_exe_path)
1040+
self.status = TwisterStatus.PASS
1041+
except Exception as e:
1042+
logger.error(f'Failed to copy bsim exe: {e}')
1043+
self.status = TwisterStatus.ERROR
1044+
finally:
1045+
self.instance.execution_time = time.time() - self._start_time
10111046

10121047
def _run_cmd(self, cmd, timeout):
10131048
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
@@ -1020,18 +1055,14 @@ def _run_cmd(self, cmd, timeout):
10201055
terminate_process(proc)
10211056
logger.warning('Timeout has occurred. Can be extended in testspec file. '
10221057
f'Currently set to {timeout} seconds.')
1023-
self.status = TwisterStatus.FAIL
1058+
self.status = TwisterStatus.ERROR
10241059
proc.wait(timeout)
10251060
except subprocess.TimeoutExpired:
1026-
self.status = TwisterStatus.FAIL
1061+
self.status = TwisterStatus.ERROR
10271062
proc.kill()
10281063

10291064
if self.status == TwisterStatus.NONE:
10301065
self.status = TwisterStatus.FAIL if proc.returncode != 0 else TwisterStatus.PASS
1031-
else:
1032-
self.status = TwisterStatus.FAIL \
1033-
if proc.returncode != 0 or self.status in [TwisterStatus.ERROR, TwisterStatus.FAIL] \
1034-
else TwisterStatus.PASS
10351066

10361067
def _output_reader(self, proc):
10371068
while proc.stdout.readable() and proc.poll() is None:
@@ -1054,40 +1085,42 @@ def _generate_commands(self):
10541085
if not test_ids:
10551086
logger.error('No test ids specified for bsim test')
10561087
self.status = TwisterStatus.ERROR
1057-
return
1088+
return []
10581089

1059-
cmds = [[self.exe_path, verbosity, suite_id, f'-d={i}', f'-testid={t_id}'] + extra_args
1090+
cmds = [[self._get_exe_path(i), verbosity, suite_id, f'-d={i}', f'-testid={t_id}'] + extra_args
10601091
for i, t_id in enumerate(test_ids)]
10611092
cmds.append([bsim_phy_path, verbosity, suite_id, f'-D={len(test_ids)}', sim_length])
10621093

10631094
return cmds
10641095

10651096
def bsim_run(self, timeout):
10661097
try:
1098+
self._set_start_time()
1099+
10671100
threads = []
1068-
start_time = time.time()
10691101
for cmd in self._generate_commands():
10701102
t = threading.Thread(target=lambda c=cmd: self._run_cmd(c, timeout))
10711103
threads.append(t)
10721104
t.start()
10731105

10741106
for t in threads:
10751107
t.join(timeout=timeout)
1076-
1077-
self.instance.execution_time = time.time() - start_time
1108+
except Exception as e:
1109+
logger.error(f'BSIM test failed: {e}')
1110+
self.status = TwisterStatus.ERROR
10781111
finally:
10791112
self._update_test_status()
10801113

10811114
def _update_test_status(self):
1115+
self.instance.execution_time += time.time() - self._start_time
10821116
if not self.instance.testcases:
10831117
self.instance.init_cases()
10841118

1085-
# currently there is alays one testcase per bsim suite
1119+
# currently there is always one testcase per bsim suite
10861120
self.instance.testcases[0].status = self.status if self.status != TwisterStatus.NONE else \
10871121
TwisterStatus.FAIL
10881122
self.instance.status = self.instance.testcases[0].status
10891123
if self.instance.status in [TwisterStatus.ERROR, TwisterStatus.FAIL]:
1090-
logger.warning(f'BSIM test failed: {self.instance.reason}')
10911124
self.instance.reason = self.instance.reason or 'Bsim test failed'
10921125
self.instance.testcases[0].output = '\n'.join(self._tc_output)
10931126

@@ -1231,15 +1264,41 @@ def _parse_report_file(self, report):
12311264

12321265
class HarnessImporter:
12331266

1267+
cache = {}
1268+
cache_lock = threading.Lock()
1269+
12341270
@staticmethod
1235-
def get_harness(harness_name):
1271+
def get_harness(instance: TestInstance):
1272+
harness_class = HarnessImporter._get_harness_class(instance.testsuite.harness)
1273+
if not harness_class:
1274+
return None
1275+
1276+
harness = None
1277+
with HarnessImporter.cache_lock:
1278+
if harness_class.cacheable and instance.name in HarnessImporter.cache:
1279+
harness = HarnessImporter.cache[instance.name]
1280+
else:
1281+
harness = harness_class()
1282+
if harness_class.cacheable:
1283+
harness.configure(instance)
1284+
HarnessImporter.cache[instance.name] = harness
1285+
1286+
return harness
1287+
1288+
@staticmethod
1289+
def _get_harness_class(harness_name: str):
12361290
thismodule = sys.modules[__name__]
12371291
try:
12381292
if harness_name:
1239-
harness_class = getattr(thismodule, harness_name)
1293+
harness_class = getattr(thismodule, harness_name.capitalize())
12401294
else:
12411295
harness_class = thismodule.Test
1242-
return harness_class()
1296+
return harness_class
12431297
except AttributeError as e:
12441298
logger.debug(f"harness {harness_name} not implemented: {e}")
12451299
return None
1300+
1301+
@staticmethod
1302+
def get_harness_by_name(harness_name: str):
1303+
harness_class = HarnessImporter._get_harness_class(harness_name)
1304+
return harness_class() if harness_class else None

scripts/pylib/twister/twisterlib/runner.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,10 @@ def run_build(self, args=None):
630630

631631
return ret
632632

633+
@property
634+
def is_bsim_test(self):
635+
return self.instance.testsuite.harness == 'bsim'
636+
633637
def run_cmake(self, args="", filter_stages=None):
634638
if filter_stages is None:
635639
filter_stages = []
@@ -656,7 +660,7 @@ def run_cmake(self, args="", filter_stages=None):
656660
f'-DPython3_EXECUTABLE={pathlib.Path(sys.executable).as_posix()}'
657661
]
658662

659-
if self.instance.testsuite.harness == 'bsim':
663+
if self.is_bsim_test:
660664
cmake_args.extend([
661665
'-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
662666
'-DCONFIG_ASSERT=y',
@@ -1125,11 +1129,18 @@ def process(self, pipeline, done, message, lock, results):
11251129

11261130
# Run the generated binary using one of the supported handlers
11271131
elif op == "run":
1128-
try:
1132+
def run():
11291133
logger.debug(f"run test: {self.instance.name}")
11301134
self.run()
11311135
logger.debug(f"run status: {self.instance.name} {self.instance.status}")
11321136

1137+
try:
1138+
if self.is_bsim_test:
1139+
with lock:
1140+
run()
1141+
else:
1142+
run()
1143+
11331144
# to make it work with pickle
11341145
self.instance.handler.thread = None
11351146
self.instance.handler.duts = None
@@ -1700,6 +1711,9 @@ def cmake_assemble_args(extra_args, handler, extra_conf_files, extra_overlay_con
17001711
return args_expanded
17011712

17021713
def cmake(self, filter_stages=None):
1714+
if self.is_bsim_test:
1715+
HarnessImporter.get_harness(self.instance).clean_exes()
1716+
17031717
if filter_stages is None:
17041718
filter_stages = []
17051719
args = []
@@ -1729,10 +1743,10 @@ def cmake(self, filter_stages=None):
17291743
self.options.extra_args, # CMake extra args
17301744
self.instance.build_dir,
17311745
)
1732-
return self.run_cmake(args,filter_stages)
1746+
return self.run_cmake(args, filter_stages)
17331747

17341748
def build(self):
1735-
harness = HarnessImporter.get_harness(self.instance.testsuite.harness.capitalize())
1749+
harness = HarnessImporter.get_harness(self.instance)
17361750
build_result = self.run_build(['--build', self.build_dir])
17371751
try:
17381752
if harness:
@@ -1765,7 +1779,7 @@ def run(self):
17651779
if self.options.extra_test_args and instance.platform.arch == "posix":
17661780
instance.handler.extra_test_args = self.options.extra_test_args
17671781

1768-
harness = HarnessImporter.get_harness(instance.testsuite.harness.capitalize())
1782+
harness = HarnessImporter.get_harness(instance)
17691783
try:
17701784
harness.configure(instance)
17711785
except ConfigurationError as error:
@@ -1779,7 +1793,12 @@ def run(self):
17791793
elif isinstance(harness, Ctest):
17801794
harness.ctest_run(instance.handler.get_test_timeout())
17811795
elif isinstance(harness, Bsim):
1782-
harness.bsim_run(instance.handler.get_test_timeout())
1796+
if harness.wait_bsim_ready():
1797+
harness.bsim_run(instance.handler.get_test_timeout())
1798+
else:
1799+
instance.status = TwisterStatus.ERROR
1800+
instance.reason = str("BSIM not ready")
1801+
logger.error(instance.reason)
17831802
else:
17841803
instance.handler.handle(harness)
17851804

@@ -1939,6 +1958,7 @@ def add_tasks_to_queue(
19391958
test_only=False,
19401959
retry_build_errors=False
19411960
):
1961+
task_list = []
19421962
for instance in self.instances.values():
19431963
if build_only:
19441964
instance.run = False
@@ -1973,17 +1993,22 @@ def add_tasks_to_queue(
19731993
expr_parser.reserved.keys()
19741994
)
19751995

1976-
if test_only and instance.run:
1977-
pipeline.put({"op": "run", "test": instance})
1996+
if test_only or instance.testsuite.no_build and instance.run:
1997+
task_list.append({"op": "run", "test": instance})
19781998
elif instance.filter_stages and "full" not in instance.filter_stages:
1979-
pipeline.put({"op": "filter", "test": instance})
1999+
task_list.append({"op": "filter", "test": instance})
19802000
else:
19812001
cache_file = os.path.join(instance.build_dir, "CMakeCache.txt")
19822002
if os.path.exists(cache_file) and self.env.options.aggressive_no_clean:
1983-
pipeline.put({"op": "build", "test": instance})
2003+
task_list.append({"op": "build", "test": instance})
19842004
else:
1985-
pipeline.put({"op": "cmake", "test": instance})
2005+
task_list.append({"op": "cmake", "test": instance})
2006+
2007+
if all([inst.testsuite.harness == 'bsim' for inst in self.instances.values()]):
2008+
task_list.sort(key=lambda t: t['op'] == 'cmake')
19862009

2010+
for task in task_list:
2011+
pipeline.put(task)
19872012

19882013
def pipeline_mgr(self, pipeline, done_queue, lock, results):
19892014
try:

scripts/schemas/twister/testsuite-schema.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ schema;scenario-schema:
3838
"build_only":
3939
type: bool
4040
required: false
41+
"no_build":
42+
type: bool
43+
required: false
4144
"build_on_all":
4245
type: bool
4346
required: false
@@ -178,8 +181,10 @@ schema;scenario-schema:
178181
sequence:
179182
- type: str
180183
"bsim_exe_name":
181-
type: str
184+
type: seq
182185
required: false
186+
sequence:
187+
- type: str
183188
"bsim_test_ids":
184189
type: seq
185190
required: false

scripts/tests/twister/test_harness.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ def test_get_harness(name):
670670
harness_name = name
671671

672672
# Act
673-
harness_class = harnessimporter.get_harness(harness_name)
673+
harness_class = harnessimporter.get_harness_by_name(harness_name)
674674

675675
# Assert
676676
assert isinstance(harness_class, Test)

0 commit comments

Comments
 (0)