1#!/usr/bin/env python3 2# vim: set syntax=python ts=4 : 3# 4# Copyright (c) 2018-2024 Intel Corporation 5# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved. 6# 7# SPDX-License-Identifier: Apache-2.0 8import collections 9import copy 10import itertools 11import json 12import logging 13import os 14import random 15import re 16import subprocess 17import sys 18from argparse import Namespace 19from collections import OrderedDict 20from itertools import islice 21from pathlib import Path 22 23import snippets 24 25try: 26 from anytree import Node, RenderTree, find 27except ImportError: 28 print("Install the anytree module to use the --test-tree option") 29 30import scl 31from twisterlib.config_parser import TwisterConfigParser 32from twisterlib.error import TwisterRuntimeError 33from twisterlib.platform import Platform, generate_platforms 34from twisterlib.quarantine import Quarantine 35from twisterlib.statuses import TwisterStatus 36from twisterlib.testinstance import TestInstance 37from twisterlib.testsuite import TestSuite, scan_testsuite_path 38from zephyr_module import parse_modules 39 40logger = logging.getLogger('twister') 41logger.setLevel(logging.DEBUG) 42 43ZEPHYR_BASE = os.getenv("ZEPHYR_BASE") 44if not ZEPHYR_BASE: 45 sys.exit("$ZEPHYR_BASE environment variable undefined") 46 47# This is needed to load edt.pickle files. 48sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts", "dts", 49 "python-devicetree", "src")) 50from devicetree import edtlib # pylint: disable=unused-import 51 52sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/")) 53 54class Filters: 55 # platform keys 56 PLATFORM_KEY = 'platform key filter' 57 # filters provided on command line by the user/tester 58 CMD_LINE = 'command line filter' 59 # filters in the testsuite yaml definition 60 TESTSUITE = 'testsuite filter' 61 # filters in the testplan yaml definition 62 TESTPLAN = 'testplan filter' 63 # filters related to platform definition 64 PLATFORM = 'Platform related filter' 65 # in case a test suite was quarantined. 66 QUARANTINE = 'Quarantine filter' 67 # in case a test suite is skipped intentionally . 68 SKIP = 'Skip filter' 69 # in case of incompatibility between selected and allowed toolchains. 70 TOOLCHAIN = 'Toolchain filter' 71 # in case where an optional module is not available 72 MODULE = 'Module filter' 73 # in case of missing env. variable required for a platform 74 ENVIRONMENT = 'Environment filter' 75 76 77class TestLevel: 78 name = None 79 levels = [] 80 scenarios = [] 81 82 83class TestPlan: 84 __test__ = False # for pytest to skip this class when collects tests 85 config_re = re.compile('(CONFIG_[A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$') 86 dt_re = re.compile('([A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$') 87 88 suite_schema = scl.yaml_load( 89 os.path.join(ZEPHYR_BASE, 90 "scripts", "schemas", "twister", "testsuite-schema.yaml")) 91 quarantine_schema = scl.yaml_load( 92 os.path.join(ZEPHYR_BASE, 93 "scripts", "schemas", "twister", "quarantine-schema.yaml")) 94 95 tc_schema_path = os.path.join( 96 ZEPHYR_BASE, 97 "scripts", 98 "schemas", 99 "twister", 100 "test-config-schema.yaml" 101 ) 102 103 SAMPLE_FILENAME = 'sample.yaml' 104 TESTSUITE_FILENAME = 'testcase.yaml' 105 106 def __init__(self, env: Namespace): 107 108 self.options = env.options 109 self.env = env 110 111 # Keep track of which test cases we've filtered out and why 112 self.testsuites = {} 113 self.quarantine = None 114 self.platforms = [] 115 self.platform_names = [] 116 self.selected_platforms = [] 117 self.default_platforms = [] 118 self.load_errors = 0 119 self.instances = dict() 120 self.instance_fail_count = 0 121 self.warnings = 0 122 123 self.scenarios = [] 124 125 self.hwm = env.hwm 126 # used during creating shorter build paths 127 self.link_dir_counter = 0 128 self.modules = [] 129 130 self.run_individual_testsuite = [] 131 self.levels = [] 132 self.test_config = {} 133 134 self.name = "unnamed" 135 136 def get_level(self, name): 137 level = next((lvl for lvl in self.levels if lvl.name == name), None) 138 return level 139 140 def parse_configuration(self, config_file): 141 if os.path.exists(config_file): 142 tc_schema = scl.yaml_load(self.tc_schema_path) 143 self.test_config = scl.yaml_load_verify(config_file, tc_schema) 144 else: 145 raise TwisterRuntimeError(f"File {config_file} not found.") 146 147 levels = self.test_config.get('levels', []) 148 149 # Do first pass on levels to get initial data. 150 for level in levels: 151 adds = [] 152 for s in level.get('adds', []): 153 r = re.compile(s) 154 adds.extend(list(filter(r.fullmatch, self.scenarios))) 155 156 tl = TestLevel() 157 tl.name = level['name'] 158 tl.scenarios = adds 159 tl.levels = level.get('inherits', []) 160 self.levels.append(tl) 161 162 # Go over levels again to resolve inheritance. 163 for level in levels: 164 inherit = level.get('inherits', []) 165 _level = self.get_level(level['name']) 166 if inherit: 167 for inherted_level in inherit: 168 _inherited = self.get_level(inherted_level) 169 assert _inherited, "Unknown inherited level {inherted_level}" 170 _inherited_scenarios = _inherited.scenarios 171 level_scenarios = _level.scenarios if _level else [] 172 level_scenarios.extend(_inherited_scenarios) 173 174 def find_subtests(self): 175 sub_tests = self.options.sub_test 176 if sub_tests: 177 for subtest in sub_tests: 178 _subtests = self.get_testcase(subtest) 179 for _subtest in _subtests: 180 self.run_individual_testsuite.append(_subtest.name) 181 182 if self.run_individual_testsuite: 183 logger.info("Running the following tests:") 184 for test in self.run_individual_testsuite: 185 print(f" - {test}") 186 else: 187 raise TwisterRuntimeError("Tests not found") 188 189 def discover(self): 190 self.handle_modules() 191 if self.options.test: 192 self.run_individual_testsuite = self.options.test 193 194 self.add_configurations() 195 num = self.add_testsuites(testsuite_filter=self.run_individual_testsuite) 196 if num == 0: 197 raise TwisterRuntimeError("No testsuites found at the specified location...") 198 if self.load_errors: 199 raise TwisterRuntimeError( 200 f"Found {self.load_errors} errors loading {num} test configurations." 201 ) 202 203 self.find_subtests() 204 # get list of scenarios we have parsed into one list 205 for _, ts in self.testsuites.items(): 206 self.scenarios.append(ts.id) 207 208 self.report_duplicates() 209 self.parse_configuration(config_file=self.env.test_config) 210 211 # handle quarantine 212 ql = self.options.quarantine_list 213 qv = self.options.quarantine_verify 214 if qv and not ql: 215 logger.error("No quarantine list given to be verified") 216 raise TwisterRuntimeError("No quarantine list given to be verified") 217 if ql: 218 for quarantine_file in ql: 219 try: 220 # validate quarantine yaml file against the provided schema 221 scl.yaml_load_verify(quarantine_file, self.quarantine_schema) 222 except scl.EmptyYamlFileException: 223 logger.debug(f'Quarantine file {quarantine_file} is empty') 224 self.quarantine = Quarantine(ql) 225 226 def load(self): 227 228 if self.options.report_suffix: 229 last_run = os.path.join( 230 self.options.outdir, 231 f"twister_{self.options.report_suffix}.json" 232 ) 233 else: 234 last_run = os.path.join(self.options.outdir, "twister.json") 235 236 if self.options.only_failed or self.options.report_summary is not None: 237 self.load_from_file(last_run) 238 self.selected_platforms = set(p.platform.name for p in self.instances.values()) 239 elif self.options.load_tests: 240 self.load_from_file(self.options.load_tests) 241 self.selected_platforms = set(p.platform.name for p in self.instances.values()) 242 elif self.options.test_only: 243 # Get list of connected hardware and filter tests to only be run on connected hardware. 244 # If the platform does not exist in the hardware map or was not specified by --platform, 245 # just skip it. 246 247 connected_list = [] 248 excluded_list = [] 249 for _cp in self.options.platform: 250 if _cp in self.platform_names: 251 connected_list.append(self.get_platform(_cp).name) 252 253 if self.options.exclude_platform: 254 for _p in self.options.exclude_platform: 255 if _p in self.platform_names: 256 excluded_list.append(self.get_platform(_p).name) 257 for excluded in excluded_list: 258 if excluded in connected_list: 259 connected_list.remove(excluded) 260 261 self.load_from_file(last_run, filter_platform=connected_list) 262 self.selected_platforms = set(p.platform.name for p in self.instances.values()) 263 else: 264 self.apply_filters() 265 266 if self.options.subset: 267 s = self.options.subset 268 try: 269 subset, sets = (int(x) for x in s.split("/")) 270 except ValueError as err: 271 raise TwisterRuntimeError("Bad subset value.") from err 272 273 if subset > sets: 274 raise TwisterRuntimeError("subset should not exceed the total number of sets") 275 276 if int(subset) > 0 and int(sets) >= int(subset): 277 logger.info(f"Running only a subset: {subset}/{sets}") 278 else: 279 raise TwisterRuntimeError( 280 f"You have provided a wrong subset value: {self.options.subset}." 281 ) 282 283 self.generate_subset(subset, int(sets)) 284 285 def generate_subset(self, subset, sets): 286 # Test instances are sorted depending on the context. For CI runs 287 # the execution order is: "plat1-testA, plat1-testB, ..., 288 # plat1-testZ, plat2-testA, ...". For hardware tests 289 # (device_testing), were multiple physical platforms can run the tests 290 # in parallel, it is more efficient to run in the order: 291 # "plat1-testA, plat2-testA, ..., plat1-testB, plat2-testB, ..." 292 if self.options.device_testing: 293 self.instances = OrderedDict(sorted(self.instances.items(), 294 key=lambda x: x[0][x[0].find("/") + 1:])) 295 else: 296 self.instances = OrderedDict(sorted(self.instances.items())) 297 298 if self.options.shuffle_tests: 299 seed_value = int.from_bytes(os.urandom(8), byteorder="big") 300 if self.options.shuffle_tests_seed is not None: 301 seed_value = self.options.shuffle_tests_seed 302 303 logger.info(f"Shuffle tests with seed: {seed_value}") 304 random.seed(seed_value) 305 temp_list = list(self.instances.items()) 306 random.shuffle(temp_list) 307 self.instances = OrderedDict(temp_list) 308 309 # Do calculation based on what is actually going to be run and evaluated 310 # at runtime, ignore the cases we already know going to be skipped. 311 # This fixes an issue where some sets would get majority of skips and 312 # basically run nothing beside filtering. 313 to_run = {k : v for k,v in self.instances.items() if v.status == TwisterStatus.NONE} 314 total = len(to_run) 315 per_set = int(total / sets) 316 num_extra_sets = total - (per_set * sets) 317 318 # Try and be more fair for rounding error with integer division 319 # so the last subset doesn't get overloaded, we add 1 extra to 320 # subsets 1..num_extra_sets. 321 if subset <= num_extra_sets: 322 start = (subset - 1) * (per_set + 1) 323 end = start + per_set + 1 324 else: 325 base = num_extra_sets * (per_set + 1) 326 start = ((subset - num_extra_sets - 1) * per_set) + base 327 end = start + per_set 328 329 sliced_instances = islice(to_run.items(), start, end) 330 skipped = {k : v for k,v in self.instances.items() if v.status == TwisterStatus.SKIP} 331 errors = {k : v for k,v in self.instances.items() if v.status == TwisterStatus.ERROR} 332 self.instances = OrderedDict(sliced_instances) 333 if subset == 1: 334 # add all pre-filtered tests that are skipped or got error status 335 # to the first set to allow for better distribution among all sets. 336 self.instances.update(skipped) 337 self.instances.update(errors) 338 339 340 def handle_modules(self): 341 # get all enabled west projects 342 modules_meta = parse_modules(ZEPHYR_BASE) 343 self.modules = [module.meta.get('name') for module in modules_meta] 344 345 346 def report(self): 347 if self.options.test_tree: 348 if not self.options.detailed_test_id: 349 logger.info("Test tree is always shown with detailed test-id.") 350 self.report_test_tree() 351 return 0 352 elif self.options.list_tests: 353 if not self.options.detailed_test_id: 354 logger.info("Test list is always shown with detailed test-id.") 355 self.report_test_list() 356 return 0 357 elif self.options.list_tags: 358 self.report_tag_list() 359 return 0 360 361 return 1 362 363 def report_duplicates(self): 364 dupes = [item for item, count in collections.Counter(self.scenarios).items() if count > 1] 365 if dupes: 366 msg = "Duplicated test scenarios found:\n" 367 for dupe in dupes: 368 msg += (f"- {dupe} found in:\n") 369 for dc in self.get_testsuite(dupe): 370 msg += (f" - {dc.yamlfile}\n") 371 raise TwisterRuntimeError(msg) 372 else: 373 logger.debug("No duplicates found.") 374 375 def report_tag_list(self): 376 tags = set() 377 for _, tc in self.testsuites.items(): 378 tags = tags.union(tc.tags) 379 380 for t in tags: 381 print(f"- {t}") 382 383 def report_test_tree(self): 384 tests_list = self.get_tests_list() 385 386 testsuite = Node("Testsuite") 387 samples = Node("Samples", parent=testsuite) 388 tests = Node("Tests", parent=testsuite) 389 390 for test in sorted(tests_list): 391 if test.startswith("sample."): 392 sec = test.split(".") 393 area = find( 394 samples, 395 lambda node, sname=sec[1]: node.name == sname and node.parent == samples 396 ) 397 if not area: 398 area = Node(sec[1], parent=samples) 399 400 Node(test, parent=area) 401 else: 402 sec = test.split(".") 403 area = find( 404 tests, 405 lambda node, sname=sec[0]: node.name == sname and node.parent == tests 406 ) 407 if not area: 408 area = Node(sec[0], parent=tests) 409 410 if area and len(sec) > 2: 411 subarea = find( 412 area, lambda node, sname=sec[1], sparent=area: node.name == sname 413 and node.parent == sparent 414 ) 415 if not subarea: 416 subarea = Node(sec[1], parent=area) 417 Node(test, parent=subarea) 418 419 for pre, _, node in RenderTree(testsuite): 420 print(f"{pre}{node.name}") 421 422 def report_test_list(self): 423 tests_list = self.get_tests_list() 424 425 cnt = 0 426 for test in sorted(tests_list): 427 cnt = cnt + 1 428 print(f" - {test}") 429 print(f"{cnt} total.") 430 431 432 # Debug Functions 433 @staticmethod 434 def info(what): 435 sys.stdout.write(what + "\n") 436 sys.stdout.flush() 437 438 def add_configurations(self): 439 # Create a list of board roots as defined by the build system in general 440 # Note, internally in twister a board root includes the `boards` folder 441 # but in Zephyr build system, the board root is without the `boards` in folder path. 442 board_roots = [Path(os.path.dirname(root)) for root in self.env.board_roots] 443 soc_roots = self.env.soc_roots 444 arch_roots = self.env.arch_roots 445 446 platform_config = self.test_config.get('platforms', {}) 447 448 for platform in generate_platforms(board_roots, soc_roots, arch_roots): 449 if not platform.twister: 450 continue 451 self.platforms.append(platform) 452 453 if not platform_config.get('override_default_platforms', False): 454 if platform.default: 455 self.default_platforms.append(platform.name) 456 #logger.debug(f"adding {platform.name} to default platforms") 457 continue 458 for pp in platform_config.get('default_platforms', []): 459 if pp in platform.aliases: 460 logger.debug(f"adding {platform.name} to default platforms (override mode)") 461 self.default_platforms.append(platform.name) 462 463 self.platform_names = [a for p in self.platforms for a in p.aliases] 464 465 def get_all_tests(self): 466 testcases = [] 467 for _, ts in self.testsuites.items(): 468 for case in ts.testcases: 469 testcases.append(case.name) 470 471 return testcases 472 473 def get_tests_list(self): 474 testcases = [] 475 if tag_filter := self.options.tag: 476 for _, ts in self.testsuites.items(): 477 if ts.tags.intersection(tag_filter): 478 for case in ts.testcases: 479 testcases.append(case.detailed_name) 480 else: 481 for _, ts in self.testsuites.items(): 482 for case in ts.testcases: 483 testcases.append(case.detailed_name) 484 485 if exclude_tag := self.options.exclude_tag: 486 for _, ts in self.testsuites.items(): 487 if ts.tags.intersection(exclude_tag): 488 for case in ts.testcases: 489 if case.detailed_name in testcases: 490 testcases.remove(case.detailed_name) 491 return testcases 492 493 def add_testsuites(self, testsuite_filter=None): 494 if testsuite_filter is None: 495 testsuite_filter = [] 496 for root in self.env.test_roots: 497 root = os.path.abspath(root) 498 499 logger.debug(f"Reading testsuite configuration files under {root}...") 500 501 for dirpath, _, filenames in os.walk(root, topdown=True): 502 if self.SAMPLE_FILENAME in filenames: 503 filename = self.SAMPLE_FILENAME 504 elif self.TESTSUITE_FILENAME in filenames: 505 filename = self.TESTSUITE_FILENAME 506 else: 507 continue 508 509 logger.debug("Found possible testsuite in " + dirpath) 510 511 suite_yaml_path = os.path.join(dirpath, filename) 512 suite_path = os.path.dirname(suite_yaml_path) 513 514 for alt_config_root in self.env.alt_config_root: 515 alt_config = os.path.join(os.path.abspath(alt_config_root), 516 os.path.relpath(suite_path, root), 517 filename) 518 if os.path.exists(alt_config): 519 logger.info( 520 f"Using alternative configuration from {os.path.normpath(alt_config)}" 521 ) 522 suite_yaml_path = alt_config 523 break 524 525 try: 526 parsed_data = TwisterConfigParser(suite_yaml_path, self.suite_schema) 527 parsed_data.load() 528 subcases = None 529 ztest_suite_names = None 530 531 for name in parsed_data.scenarios: 532 suite_dict = parsed_data.get_scenario(name) 533 suite = TestSuite( 534 root, 535 suite_path, 536 name, 537 data=suite_dict, 538 detailed_test_id=self.options.detailed_test_id 539 ) 540 541 # convert to fully qualified names 542 suite.integration_platforms = self.verify_platforms_existence( 543 suite.integration_platforms, 544 f"integration_platforms in {suite.name}") 545 suite.platform_exclude = self.verify_platforms_existence( 546 suite.platform_exclude, 547 f"platform_exclude in {suite.name}") 548 suite.platform_allow = self.verify_platforms_existence( 549 suite.platform_allow, 550 f"platform_allow in {suite.name}") 551 552 if suite.harness in ['ztest', 'test']: 553 if subcases is None: 554 # scan it only once per testsuite 555 subcases, ztest_suite_names = scan_testsuite_path(suite_path) 556 suite.add_subcases(suite_dict, subcases, ztest_suite_names) 557 else: 558 suite.add_subcases(suite_dict) 559 560 if testsuite_filter: 561 scenario = os.path.basename(suite.name) 562 if ( 563 suite.name 564 and (suite.name in testsuite_filter or scenario in testsuite_filter) 565 ): 566 self.testsuites[suite.name] = suite 567 elif suite.name in self.testsuites: 568 msg = ( 569 f"test suite '{suite.name}' in '{suite.yamlfile}' is already added" 570 ) 571 if suite.yamlfile == self.testsuites[suite.name].yamlfile: 572 logger.debug(f"Skip - {msg}") 573 else: 574 msg = ( 575 f"Duplicate {msg} from '{self.testsuites[suite.name].yamlfile}'" 576 ) 577 raise TwisterRuntimeError(msg) 578 else: 579 self.testsuites[suite.name] = suite 580 581 except Exception as e: 582 logger.error(f"{suite_path}: can't load (skipping): {e!r}") 583 self.load_errors += 1 584 return len(self.testsuites) 585 586 def __str__(self): 587 return self.name 588 589 def get_platform(self, name): 590 selected_platform = None 591 for platform in self.platforms: 592 if name in platform.aliases: 593 selected_platform = platform 594 break 595 return selected_platform 596 597 def handle_quarantined_tests(self, instance: TestInstance, plat: Platform): 598 if self.quarantine: 599 simulator = plat.simulator_by_name(self.options) 600 matched_quarantine = self.quarantine.get_matched_quarantine( 601 instance.testsuite.id, 602 plat.name, 603 plat.arch, 604 simulator.name if simulator is not None else 'na' 605 ) 606 if matched_quarantine and not self.options.quarantine_verify: 607 instance.add_filter("Quarantine: " + matched_quarantine, Filters.QUARANTINE) 608 return 609 if not matched_quarantine and self.options.quarantine_verify: 610 instance.add_filter("Not under quarantine", Filters.QUARANTINE) 611 612 def load_from_file(self, file, filter_platform=None): 613 if filter_platform is None: 614 filter_platform = [] 615 try: 616 with open(file) as json_test_plan: 617 jtp = json.load(json_test_plan) 618 instance_list = [] 619 for ts in jtp.get("testsuites", []): 620 logger.debug(f"loading {ts['name']}...") 621 testsuite = ts["name"] 622 toolchain = ts["toolchain"] 623 624 platform = self.get_platform(ts["platform"]) 625 if filter_platform and platform.name not in filter_platform: 626 continue 627 instance = TestInstance( 628 self.testsuites[testsuite], platform, toolchain, self.env.outdir 629 ) 630 if ts.get("run_id"): 631 instance.run_id = ts.get("run_id") 632 633 instance.run = instance.check_runnable( 634 self.options, 635 self.hwm 636 ) 637 638 if self.options.test_only and not instance.run: 639 continue 640 641 instance.metrics['handler_time'] = ts.get('execution_time', 0) 642 instance.metrics['used_ram'] = ts.get("used_ram", 0) 643 instance.metrics['used_rom'] = ts.get("used_rom",0) 644 instance.metrics['available_ram'] = ts.get('available_ram', 0) 645 instance.metrics['available_rom'] = ts.get('available_rom', 0) 646 647 status = TwisterStatus(ts.get('status')) 648 reason = ts.get("reason", "Unknown") 649 if status in [TwisterStatus.ERROR, TwisterStatus.FAIL]: 650 if self.options.report_summary is not None: 651 instance.status = status 652 instance.reason = reason 653 self.instance_fail_count += 1 654 else: 655 instance.status = TwisterStatus.NONE 656 instance.reason = None 657 instance.retries += 1 658 # test marked as built only can run when --test-only is used. 659 # Reset status to capture new results. 660 elif status == TwisterStatus.NOTRUN and instance.run and self.options.test_only: 661 instance.status = TwisterStatus.NONE 662 instance.reason = None 663 else: 664 instance.status = status 665 instance.reason = reason 666 667 self.handle_quarantined_tests(instance, platform) 668 669 for tc in ts.get('testcases', []): 670 identifier = tc['identifier'] 671 tc_status = TwisterStatus(tc.get('status')) 672 tc_reason = None 673 # we set reason only if status is valid, it might have been 674 # reset above... 675 if instance.status != TwisterStatus.NONE: 676 tc_reason = tc.get('reason') 677 if tc_status != TwisterStatus.NONE: 678 case = instance.set_case_status_by_name( 679 identifier, 680 tc_status, 681 tc_reason 682 ) 683 case.duration = tc.get('execution_time', 0) 684 if tc.get('log'): 685 case.output = tc.get('log') 686 687 instance.create_overlay(platform, 688 self.options.enable_asan, 689 self.options.enable_ubsan, 690 self.options.enable_coverage, 691 self.options.coverage_platform 692 ) 693 instance_list.append(instance) 694 self.add_instances(instance_list) 695 except FileNotFoundError as e: 696 logger.error(f"{e}") 697 return 1 698 699 def check_platform(self, platform, platform_list): 700 return any(p in platform.aliases for p in platform_list) 701 702 def apply_filters(self, **kwargs): 703 704 platform_filter = self.options.platform 705 vendor_filter = self.options.vendor 706 exclude_platform = self.options.exclude_platform 707 testsuite_filter = self.run_individual_testsuite 708 arch_filter = self.options.arch 709 tag_filter = self.options.tag 710 exclude_tag = self.options.exclude_tag 711 all_filter = self.options.all 712 runnable = (self.options.device_testing or self.options.filter == 'runnable') 713 force_toolchain = self.options.force_toolchain 714 force_platform = self.options.force_platform 715 slow_only = self.options.enable_slow_only 716 ignore_platform_key = self.options.ignore_platform_key 717 emu_filter = self.options.emulation_only 718 719 logger.debug("platform filter: " + str(platform_filter)) 720 logger.debug(" vendor filter: " + str(vendor_filter)) 721 logger.debug(" arch_filter: " + str(arch_filter)) 722 logger.debug(" tag_filter: " + str(tag_filter)) 723 logger.debug(" exclude_tag: " + str(exclude_tag)) 724 725 default_platforms = False 726 vendor_platforms = False 727 emulation_platforms = False 728 729 if all_filter: 730 logger.info("Selecting all possible platforms per testsuite scenario") 731 # When --all used, any --platform arguments ignored 732 platform_filter = [] 733 elif not platform_filter and not emu_filter and not vendor_filter: 734 logger.info("Selecting default platforms per testsuite scenario") 735 default_platforms = True 736 elif emu_filter: 737 logger.info("Selecting emulation platforms per testsuite scenraio") 738 emulation_platforms = True 739 elif vendor_filter: 740 vendor_platforms = True 741 742 _platforms = [] 743 if platform_filter: 744 logger.debug(f"Checking platform filter: {platform_filter}") 745 # find in aliases and rename 746 platform_filter = self.verify_platforms_existence(platform_filter, "platform_filter") 747 platforms = list(filter(lambda p: p.name in platform_filter, self.platforms)) 748 elif emu_filter: 749 platforms = list( 750 filter(lambda p: bool(p.simulator_by_name(self.options.sim_name)), self.platforms) 751 ) 752 elif vendor_filter: 753 platforms = list(filter(lambda p: p.vendor in vendor_filter, self.platforms)) 754 logger.info(f"Selecting platforms by vendors: {','.join(vendor_filter)}") 755 elif arch_filter: 756 platforms = list(filter(lambda p: p.arch in arch_filter, self.platforms)) 757 elif default_platforms: 758 _platforms = list(filter(lambda p: p.name in self.default_platforms, self.platforms)) 759 platforms = [] 760 # default platforms that can't be run are dropped from the list of 761 # the default platforms list. Default platforms should always be 762 # runnable. 763 for p in _platforms: 764 sim = p.simulator_by_name(self.options.sim_name) 765 if (not sim) or sim.is_runnable(): 766 platforms.append(p) 767 else: 768 platforms = self.platforms 769 770 platform_config = self.test_config.get('platforms', {}) 771 logger.info("Building initial testsuite list...") 772 773 keyed_tests = {} 774 775 for _, ts in self.testsuites.items(): 776 if ( 777 ts.build_on_all 778 and not platform_filter 779 and platform_config.get('increased_platform_scope', True) 780 ): 781 platform_scope = self.platforms 782 elif ts.integration_platforms: 783 integration_platforms = list( 784 filter(lambda item: item.name in ts.integration_platforms, self.platforms) 785 ) 786 if self.options.integration: 787 platform_scope = integration_platforms 788 else: 789 # if not in integration mode, still add integration platforms to the list 790 if not platform_filter: 791 platform_scope = platforms + integration_platforms 792 else: 793 platform_scope = platforms 794 else: 795 platform_scope = platforms 796 797 integration = self.options.integration and ts.integration_platforms 798 799 # If there isn't any overlap between the platform_allow list and the platform_scope 800 # we set the scope to the platform_allow list 801 if ( 802 ts.platform_allow 803 and not platform_filter 804 and not integration 805 and platform_config.get('increased_platform_scope', True) 806 ): 807 a = set(platform_scope) 808 b = set(filter(lambda item: item.name in ts.platform_allow, self.platforms)) 809 c = a.intersection(b) 810 if not c: 811 platform_scope = list( 812 filter(lambda item: item.name in ts.platform_allow, self.platforms) 813 ) 814 # list of instances per testsuite, aka configurations. 815 instance_list = [] 816 for itoolchain, plat in itertools.product( 817 ts.integration_toolchains or [None], platform_scope 818 ): 819 if itoolchain: 820 toolchain = itoolchain 821 elif plat.arch in ['posix', 'unit']: 822 # workaround until toolchain variant in zephyr is overhauled and improved. 823 if self.env.toolchain in ['llvm']: 824 toolchain = 'llvm' 825 else: 826 toolchain = 'host' 827 else: 828 toolchain = "zephyr" if not self.env.toolchain else self.env.toolchain 829 830 instance = TestInstance(ts, plat, toolchain, self.env.outdir) 831 instance.run = instance.check_runnable( 832 self.options, 833 self.hwm 834 ) 835 836 if not force_platform and self.check_platform(plat,exclude_platform): 837 instance.add_filter("Platform is excluded on command line.", Filters.CMD_LINE) 838 839 if (plat.arch == "unit") != (ts.type == "unit"): 840 # Discard silently 841 continue 842 843 if ts.modules and self.modules and not set(ts.modules).issubset(set(self.modules)): 844 instance.add_filter( 845 f"one or more required modules not available: {','.join(ts.modules)}", 846 Filters.MODULE 847 ) 848 849 if self.options.level: 850 tl = self.get_level(self.options.level) 851 if tl is None: 852 instance.add_filter( 853 f"Unknown test level '{self.options.level}'", 854 Filters.TESTPLAN 855 ) 856 else: 857 planned_scenarios = tl.scenarios 858 if ( 859 ts.id not in planned_scenarios 860 and not set(ts.levels).intersection(set(tl.levels)) 861 ): 862 instance.add_filter("Not part of requested test plan", Filters.TESTPLAN) 863 864 if runnable and not instance.run: 865 instance.add_filter("Not runnable on device", Filters.CMD_LINE) 866 867 if ( 868 self.options.integration 869 and ts.integration_platforms 870 and plat.name not in ts.integration_platforms 871 ): 872 instance.add_filter("Not part of integration platforms", Filters.TESTSUITE) 873 874 if ts.skip: 875 instance.add_filter("Skip filter", Filters.SKIP) 876 877 if tag_filter and not ts.tags.intersection(tag_filter): 878 instance.add_filter("Command line testsuite tag filter", Filters.CMD_LINE) 879 880 if slow_only and not ts.slow: 881 instance.add_filter("Not a slow test", Filters.CMD_LINE) 882 883 if exclude_tag and ts.tags.intersection(exclude_tag): 884 instance.add_filter("Command line testsuite exclude filter", Filters.CMD_LINE) 885 886 if testsuite_filter: 887 normalized_f = [os.path.basename(_ts) for _ts in testsuite_filter] 888 if ts.id not in normalized_f: 889 instance.add_filter("Testsuite name filter", Filters.CMD_LINE) 890 891 if arch_filter and plat.arch not in arch_filter: 892 instance.add_filter("Command line testsuite arch filter", Filters.CMD_LINE) 893 894 if not force_platform: 895 896 if ts.arch_allow and plat.arch not in ts.arch_allow: 897 instance.add_filter("Not in testsuite arch allow list", Filters.TESTSUITE) 898 899 if ts.arch_exclude and plat.arch in ts.arch_exclude: 900 instance.add_filter("In testsuite arch exclude", Filters.TESTSUITE) 901 902 if ts.vendor_allow and plat.vendor not in ts.vendor_allow: 903 instance.add_filter( 904 "Not in testsuite vendor allow list", 905 Filters.TESTSUITE 906 ) 907 908 if ts.vendor_exclude and plat.vendor in ts.vendor_exclude: 909 instance.add_filter("In testsuite vendor exclude", Filters.TESTSUITE) 910 911 if ts.platform_exclude and plat.name in ts.platform_exclude: 912 instance.add_filter("In testsuite platform exclude", Filters.TESTSUITE) 913 914 if ts.toolchain_exclude and toolchain in ts.toolchain_exclude: 915 instance.add_filter("In testsuite toolchain exclude", Filters.TOOLCHAIN) 916 917 if platform_filter and plat.name not in platform_filter: 918 instance.add_filter("Command line platform filter", Filters.CMD_LINE) 919 920 if ts.platform_allow \ 921 and plat.name not in ts.platform_allow \ 922 and not (platform_filter and force_platform): 923 instance.add_filter("Not in testsuite platform allow list", Filters.TESTSUITE) 924 925 if ts.platform_type and plat.type not in ts.platform_type: 926 instance.add_filter("Not in testsuite platform type list", Filters.TESTSUITE) 927 928 if ts.toolchain_allow and toolchain not in ts.toolchain_allow: 929 instance.add_filter("Not in testsuite toolchain allow list", Filters.TOOLCHAIN) 930 931 if not plat.env_satisfied: 932 instance.add_filter( 933 "Environment ({}) not satisfied".format(", ".join(plat.env)), 934 Filters.ENVIRONMENT 935 ) 936 if plat.type == 'native' and sys.platform != 'linux': 937 instance.add_filter("Native platform requires Linux", Filters.ENVIRONMENT) 938 939 if not force_toolchain \ 940 and toolchain and (toolchain not in plat.supported_toolchains): 941 instance.add_filter( 942 f"Not supported by the toolchain: {toolchain}", 943 Filters.PLATFORM 944 ) 945 946 if plat.ram < ts.min_ram: 947 instance.add_filter("Not enough RAM", Filters.PLATFORM) 948 949 if ts.harness: 950 sim = plat.simulator_by_name(self.options.sim_name) 951 if ts.harness == 'robot' and not (sim and sim.name == 'renode'): 952 instance.add_filter( 953 "No robot support for the selected platform", 954 Filters.SKIP 955 ) 956 957 if ts.depends_on: 958 dep_intersection = ts.depends_on.intersection(set(plat.supported)) 959 if dep_intersection != set(ts.depends_on): 960 instance.add_filter( 961 f"No hardware support for {set(ts.depends_on)-dep_intersection}", 962 Filters.PLATFORM 963 ) 964 965 if plat.flash < ts.min_flash: 966 instance.add_filter("Not enough FLASH", Filters.PLATFORM) 967 968 if set(plat.ignore_tags) & ts.tags: 969 instance.add_filter( 970 "Excluded tags per platform (exclude_tags)", 971 Filters.PLATFORM 972 ) 973 974 if plat.only_tags and not set(plat.only_tags) & ts.tags: 975 instance.add_filter("Excluded tags per platform (only_tags)", Filters.PLATFORM) 976 977 if ts.required_snippets: 978 missing_snippet = False 979 snippet_args = {"snippets": ts.required_snippets} 980 found_snippets = snippets.find_snippets_in_roots( 981 snippet_args, 982 [*self.env.snippet_roots, Path(ts.source_dir)] 983 ) 984 985 # Search and check that all required snippet files are found 986 for this_snippet in snippet_args['snippets']: 987 if this_snippet not in found_snippets: 988 logger.error( 989 f"Can't find snippet '{this_snippet}' for test '{ts.name}'" 990 ) 991 instance.status = TwisterStatus.ERROR 992 instance.reason = f"Snippet {this_snippet} not found" 993 missing_snippet = True 994 break 995 996 if not missing_snippet: 997 # Look for required snippets and check that they are applicable for these 998 # platforms/boards 999 for this_snippet in snippet_args['snippets']: 1000 matched_snippet_board = False 1001 1002 # If the "appends" key is present with at least one entry then this 1003 # snippet applies to all boards and further platform-specific checks 1004 # are not required 1005 if found_snippets[this_snippet].appends: 1006 continue 1007 1008 for this_board in found_snippets[this_snippet].board2appends: 1009 if this_board.startswith('/'): 1010 match = re.search(this_board[1:-1], plat.name) 1011 if match is not None: 1012 matched_snippet_board = True 1013 break 1014 elif this_board == plat.name: 1015 matched_snippet_board = True 1016 break 1017 1018 if matched_snippet_board is False: 1019 instance.add_filter("Snippet not supported", Filters.PLATFORM) 1020 break 1021 1022 # handle quarantined tests 1023 self.handle_quarantined_tests(instance, plat) 1024 1025 # platform_key is a list of unique platform attributes that form a unique key 1026 # a test will match against to determine if it should be scheduled to run. 1027 # A key containing a field name that the platform does not have 1028 # will filter the platform. 1029 # 1030 # A simple example is keying on arch and simulation 1031 # to run a test once per unique (arch, simulation) platform. 1032 if ( 1033 not ignore_platform_key 1034 and hasattr(ts, 'platform_key') 1035 and len(ts.platform_key) > 0 1036 ): 1037 key_fields = sorted(set(ts.platform_key)) 1038 keys = [getattr(plat, key_field, None) for key_field in key_fields] 1039 for key in keys: 1040 if key is None or key == 'na': 1041 instance.add_filter( 1042 "Excluded platform missing key fields" 1043 f" demanded by test {key_fields}", 1044 Filters.PLATFORM 1045 ) 1046 break 1047 else: 1048 test_keys = copy.deepcopy(keys) 1049 test_keys.append(ts.name) 1050 test_keys = tuple(test_keys) 1051 keyed_test = keyed_tests.get(test_keys) 1052 if keyed_test is not None: 1053 plat_key = { 1054 key_field: getattr( 1055 keyed_test['plat'], 1056 key_field 1057 ) for key_field in key_fields 1058 } 1059 instance.add_filter( 1060 f"Already covered for key {key}" 1061 f" by platform {keyed_test['plat'].name} having key {plat_key}", 1062 Filters.PLATFORM_KEY 1063 ) 1064 else: 1065 # do not add a platform to keyed tests if previously 1066 # filtered 1067 1068 if not instance.filters: 1069 keyed_tests[test_keys] = {'plat': plat, 'ts': ts} 1070 1071 # if nothing stopped us until now, it means this configuration 1072 # needs to be added. 1073 instance_list.append(instance) 1074 1075 # no configurations, so jump to next testsuite 1076 if not instance_list: 1077 continue 1078 1079 # if twister was launched with no platform options at all, we 1080 # take all default platforms 1081 if default_platforms and not ts.build_on_all and not integration: 1082 if ts.platform_allow: 1083 _default_p = set(self.default_platforms) 1084 _platform_allow = set(ts.platform_allow) 1085 _intersection = _default_p.intersection(_platform_allow) 1086 if _intersection: 1087 aa = list( 1088 filter( 1089 lambda _scenario: _scenario.platform.name in _intersection, 1090 instance_list 1091 ) 1092 ) 1093 self.add_instances(aa) 1094 else: 1095 self.add_instances(instance_list) 1096 else: 1097 # add integration platforms to the list of default 1098 # platforms, even if we are not in integration mode 1099 _platforms = self.default_platforms + ts.integration_platforms 1100 instances = list( 1101 filter(lambda ts: ts.platform.name in _platforms, instance_list) 1102 ) 1103 self.add_instances(instances) 1104 elif integration: 1105 instances = list( 1106 filter( 1107 lambda item: item.platform.name in ts.integration_platforms, 1108 instance_list 1109 ) 1110 ) 1111 self.add_instances(instances) 1112 1113 elif emulation_platforms: 1114 self.add_instances(instance_list) 1115 for instance in list( 1116 filter( 1117 lambda inst: not inst.platform.simulator_by_name(self.options.sim_name), 1118 instance_list 1119 ) 1120 ): 1121 instance.add_filter("Not an emulated platform", Filters.CMD_LINE) 1122 elif vendor_platforms: 1123 self.add_instances(instance_list) 1124 for instance in list( 1125 filter( 1126 lambda inst: inst.platform.vendor not in vendor_filter, 1127 instance_list 1128 ) 1129 ): 1130 instance.add_filter("Not a selected vendor platform", Filters.CMD_LINE) 1131 else: 1132 self.add_instances(instance_list) 1133 1134 for _, case in self.instances.items(): 1135 # Do not create files for filtered instances 1136 if case.status == TwisterStatus.FILTER: 1137 continue 1138 # set run_id for each unfiltered instance 1139 case.setup_run_id() 1140 case.create_overlay(case.platform, 1141 self.options.enable_asan, 1142 self.options.enable_ubsan, 1143 self.options.enable_coverage, 1144 self.options.coverage_platform) 1145 1146 self.selected_platforms = set(p.platform.name for p in self.instances.values()) 1147 1148 filtered_instances = list( 1149 filter(lambda item: item.status == TwisterStatus.FILTER, self.instances.values()) 1150 ) 1151 for filtered_instance in filtered_instances: 1152 change_skip_to_error_if_integration(self.options, filtered_instance) 1153 1154 filtered_instance.add_missing_case_status(filtered_instance.status) 1155 1156 def add_instances(self, instance_list): 1157 for instance in instance_list: 1158 self.instances[instance.name] = instance 1159 1160 1161 def get_testsuite(self, identifier): 1162 results = [] 1163 for _, ts in self.testsuites.items(): 1164 if ts.id == identifier: 1165 results.append(ts) 1166 return results 1167 1168 def get_testcase(self, identifier): 1169 results = [] 1170 for _, ts in self.testsuites.items(): 1171 for case in ts.testcases: 1172 if case.name == identifier: 1173 results.append(ts) 1174 return results 1175 1176 def verify_platforms_existence(self, platform_names_to_verify, log_info=""): 1177 """ 1178 Verify if platform name (passed by --platform option, or in yaml file 1179 as platform_allow or integration_platforms options) is correct. If not - 1180 log and raise error. 1181 """ 1182 _platforms = [] 1183 for platform in platform_names_to_verify: 1184 if platform in self.platform_names: 1185 p = self.get_platform(platform) 1186 if p: 1187 _platforms.append(p.name) 1188 else: 1189 logger.error(f"{log_info} - unrecognized platform - {platform}") 1190 sys.exit(2) 1191 return _platforms 1192 1193 def create_build_dir_links(self): 1194 """ 1195 Iterate through all no-skipped instances in suite and create links 1196 for each one build directories. Those links will be passed in the next 1197 steps to the CMake command. 1198 """ 1199 1200 links_dir_name = "twister_links" # folder for all links 1201 links_dir_path = os.path.join(self.env.outdir, links_dir_name) 1202 if not os.path.exists(links_dir_path): 1203 os.mkdir(links_dir_path) 1204 1205 for instance in self.instances.values(): 1206 if instance.status != TwisterStatus.SKIP: 1207 self._create_build_dir_link(links_dir_path, instance) 1208 1209 def _create_build_dir_link(self, links_dir_path, instance): 1210 """ 1211 Create build directory with original "long" path. Next take shorter 1212 path and link them with original path - create link. At the end 1213 replace build_dir to created link. This link will be passed to CMake 1214 command. This action helps to limit path length which can be 1215 significant during building by CMake on Windows OS. 1216 """ 1217 1218 os.makedirs(instance.build_dir, exist_ok=True) 1219 1220 link_name = f"test_{self.link_dir_counter}" 1221 link_path = os.path.join(links_dir_path, link_name) 1222 1223 if os.name == "nt": # if OS is Windows 1224 command = ["mklink", "/J", f"{link_path}", os.path.normpath(instance.build_dir)] 1225 subprocess.call(command, shell=True) 1226 else: # for Linux and MAC OS 1227 os.symlink(instance.build_dir, link_path) 1228 1229 # Here original build directory is replaced with symbolic link. It will 1230 # be passed to CMake command 1231 instance.build_dir = link_path 1232 1233 self.link_dir_counter += 1 1234 1235 1236def change_skip_to_error_if_integration(options, instance): 1237 ''' All skips on integration_platforms are treated as errors.''' 1238 if instance.platform.name in instance.testsuite.integration_platforms: 1239 # Do not treat this as error if filter type is among ignore_filters 1240 filters = {t['type'] for t in instance.filters} 1241 ignore_filters ={Filters.CMD_LINE, Filters.SKIP, Filters.PLATFORM_KEY, 1242 Filters.TOOLCHAIN, Filters.MODULE, Filters.TESTPLAN, 1243 Filters.QUARANTINE, Filters.ENVIRONMENT} 1244 if filters.intersection(ignore_filters): 1245 return 1246 instance.status = TwisterStatus.ERROR 1247 instance.reason += " but is one of the integration platforms" 1248 logger.debug( 1249 f"Changing status of {instance.name} to ERROR because it is an integration platform" 1250 ) 1251