1#!/usr/bin/env python3 2# vim: set syntax=python ts=4 : 3# 4# Copyright (c) 2018-2025 Intel Corporation 5# Copyright 2022 NXP 6# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved. 7# 8# SPDX-License-Identifier: Apache-2.0 9 10import argparse 11import json 12import logging 13import os 14import re 15import shutil 16import subprocess 17import sys 18from collections.abc import Generator 19from datetime import datetime, timezone 20from importlib import metadata 21from pathlib import Path 22 23import zephyr_module 24from twisterlib.constants import SUPPORTED_SIMS 25from twisterlib.coverage import supported_coverage_formats 26from twisterlib.error import TwisterRuntimeError 27from twisterlib.log_helper import log_command 28 29logger = logging.getLogger('twister') 30logger.setLevel(logging.DEBUG) 31 32ZEPHYR_BASE = os.getenv("ZEPHYR_BASE") 33if not ZEPHYR_BASE: 34 sys.exit("$ZEPHYR_BASE environment variable undefined") 35 36# Use this for internal comparisons; that's what canonicalization is 37# for. Don't use it when invoking other components of the build system 38# to avoid confusing and hard to trace inconsistencies in error messages 39# and logs, generated Makefiles, etc. compared to when users invoke these 40# components directly. 41# Note "normalization" is different from canonicalization, see os.path. 42canonical_zephyr_base = os.path.realpath(ZEPHYR_BASE) 43 44 45def _get_installed_packages() -> Generator[str, None, None]: 46 """Return list of installed python packages.""" 47 for dist in metadata.distributions(): 48 yield dist.metadata['Name'] 49 50 51def python_version_guard(): 52 min_ver = (3, 10) 53 if sys.version_info < min_ver: 54 min_ver_str = '.'.join([str(v) for v in min_ver]) 55 cur_ver_line = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" 56 print(f"Unsupported Python version {cur_ver_line}.") 57 print(f"Currently, Twister requires at least Python {min_ver_str}.") 58 print("Install a newer Python version and retry.") 59 sys.exit(1) 60 61 62installed_packages: list[str] = list(_get_installed_packages()) 63PYTEST_PLUGIN_INSTALLED = 'pytest-twister-harness' in installed_packages 64 65 66def norm_path(astring): 67 newstring = os.path.normpath(astring).replace(os.sep, '/') 68 return newstring 69 70 71def add_parse_arguments(parser = None) -> argparse.ArgumentParser: 72 if parser is None: 73 parser = argparse.ArgumentParser( 74 description=__doc__, 75 formatter_class=argparse.RawDescriptionHelpFormatter, 76 allow_abbrev=False) 77 parser.fromfile_prefix_chars = "+" 78 79 case_select = parser.add_argument_group("Test case selection", 80 """ 81Artificially long but functional example: 82 $ ./scripts/twister -v \\ 83 --testsuite-root tests/ztest/base \\ 84 --testsuite-root tests/kernel \\ 85 --test tests/ztest/base/testing.ztest.verbose_0 \\ 86 --test tests/kernel/fifo/fifo_api/kernel.fifo 87 88 "kernel.fifo.poll" is one of the test section names in 89 __/fifo_api/testcase.yaml 90 """) 91 92 test_plan_report = parser.add_argument_group( 93 title="Test plan reporting", 94 description="Report the composed test plan details and exit (dry-run)." 95 ) 96 97 test_plan_report_xor = test_plan_report.add_mutually_exclusive_group() 98 99 platform_group_option = parser.add_mutually_exclusive_group() 100 101 run_group_option = parser.add_mutually_exclusive_group() 102 103 device = parser.add_mutually_exclusive_group() 104 105 test_or_build = parser.add_mutually_exclusive_group() 106 107 test_xor_subtest = case_select.add_mutually_exclusive_group() 108 109 test_xor_generator = case_select.add_mutually_exclusive_group() 110 111 valgrind_asan_group = parser.add_mutually_exclusive_group() 112 113 footprint_group = parser.add_argument_group( 114 title="Memory footprint", 115 description="Collect and report ROM/RAM size footprint for the test instance images built.") 116 117 coverage_group = parser.add_argument_group( 118 title="Code coverage", 119 description="Build with code coverage support, collect code coverage statistics " 120 "executing tests, compose code coverage report at the end.\n" 121 "Effective for devices with 'HAS_COVERAGE_SUPPORT'.") 122 123 test_plan_report_xor.add_argument( 124 "-E", 125 "--save-tests", 126 metavar="FILENAME", 127 action="store", 128 help="Write a list of tests and platforms to be run to %(metavar)s file and stop execution." 129 " The resulting file will have the same content as 'testplan.json'." 130 ) 131 132 case_select.add_argument( 133 "-F", 134 "--load-tests", 135 metavar="FILENAME", 136 action="store", 137 help="Load a list of tests and platforms to be run" 138 "from a JSON file ('testplan.json' schema)." 139 ) 140 141 case_select.add_argument( 142 "-T", "--testsuite-root", action="append", default=[], type = norm_path, 143 help="Base directory to recursively search for test cases. All " 144 "testcase.yaml files under here will be processed. May be " 145 "called multiple times. Defaults to the 'samples/' and " 146 "'tests/' directories at the base of the Zephyr tree.") 147 148 case_select.add_argument( 149 "-f", 150 "--only-failed", 151 action="store_true", 152 help="Run only those tests that failed the previous twister run " 153 "invocation.") 154 155 test_plan_report_xor.add_argument("--list-tests", action="store_true", 156 help="""List of all sub-test functions recursively found in 157 all --testsuite-root arguments. The output is flattened and reports detailed 158 sub-test names without their directories. 159 Note: sub-test names can share the same test scenario identifier prefix 160 (section.subsection) even if they are from different test projects. 161 """) 162 163 test_plan_report_xor.add_argument("--test-tree", action="store_true", 164 help="""Output the test plan in a tree form.""") 165 166 platform_group_option.add_argument( 167 "-G", 168 "--integration", 169 action="store_true", 170 help="Run integration tests") 171 172 platform_group_option.add_argument( 173 "--emulation-only", action="store_true", 174 help="Only build and run emulation platforms") 175 176 run_group_option.add_argument( 177 "--device-testing", action="store_true", 178 help="Test on device directly. Specify the serial device to " 179 "use with the --device-serial option.") 180 181 run_group_option.add_argument("--generate-hardware-map", 182 help="""Probe serial devices connected to this platform 183 and create a hardware map file to be used with 184 --device-testing 185 """) 186 187 run_group_option.add_argument( 188 "--simulation", dest="sim_name", choices=SUPPORTED_SIMS, 189 help="Selects which simulation to use. Must match one of the names defined in the board's " 190 "manifest. If multiple simulator are specified in the selected board and this " 191 "argument is not passed, then the first simulator is selected.") 192 193 194 device.add_argument("--device-serial", 195 help="""Serial device for accessing the board 196 (e.g., /dev/ttyACM0) 197 """) 198 199 device.add_argument("--device-serial-pty", 200 help="""Script for controlling pseudoterminal. 201 Twister believes that it interacts with a terminal 202 when it actually interacts with the script. 203 204 E.g "twister --device-testing 205 --device-serial-pty <script> 206 """) 207 208 device.add_argument("--hardware-map", 209 help="""Load hardware map from a file. This will be used 210 for testing on hardware that is listed in the file. 211 """) 212 213 parser.add_argument("--device-flash-timeout", type=int, default=60, 214 help="""Set timeout for the device flash operation in seconds. 215 """) 216 217 parser.add_argument("--device-flash-with-test", action="store_true", 218 help="""Add a test case timeout to the flash operation timeout 219 when flash operation also executes test case on the platform. 220 """) 221 222 parser.add_argument("--flash-before", action="store_true", default=False, 223 help="""Flash device before attaching to serial port. 224 This is useful for devices that share the same port for programming 225 and serial console, or use soft-USB, where flash must come first. 226 """) 227 228 test_or_build.add_argument( 229 "-b", 230 "--build-only", 231 action="store_true", 232 default="--prep-artifacts-for-testing" in sys.argv, 233 help="Only build the code, do not attempt to run the code on targets." 234 ) 235 236 test_or_build.add_argument( 237 "--prep-artifacts-for-testing", action="store_true", 238 help="Generate artifacts for testing, do not attempt to run the" 239 "code on targets.") 240 241 parser.add_argument( 242 "--package-artifacts", 243 help="Package artifacts needed for flashing in a file to be used with --test-only" 244 ) 245 246 test_or_build.add_argument( 247 "--test-only", action="store_true", 248 help="""Only run device tests with current artifacts, do not build 249 the code""") 250 251 parser.add_argument("--timeout-multiplier", type=float, default=1, 252 help="""Globally adjust tests timeouts by specified multiplier. The resulting test 253 timeout would be multiplication of test timeout value, board-level timeout multiplier 254 and global timeout multiplier (this parameter)""") 255 256 test_xor_subtest.add_argument( 257 "-s", "--test", "--scenario", action="append", type = norm_path, 258 help="""Run only the specified test suite scenario. These are named by 259 'path/relative/to/Zephyr/base/section.subsection_in_testcase_yaml', 260 or just 'section.subsection' identifier. With '--testsuite-root' option 261 the scenario will be found faster. 262 """) 263 264 test_xor_subtest.add_argument( 265 "--sub-test", action="append", 266 help="""Recursively find sub-test functions (test cases) and run the entire 267 test scenario (section.subsection) where they were found, including all sibling test 268 functions. Sub-tests are named by: 269 'section.subsection_in_testcase_yaml.ztest_suite.ztest_without_test_prefix'. 270 Example_1: 'kernel.fifo.fifo_api_1cpu.fifo_loop' where 'kernel.fifo' is a test scenario 271 name (section.subsection) and 'fifo_api_1cpu.fifo_loop' is a Ztest 'suite_name.test_name'. 272 Example_2: 'debug.coredump.logging_backend' is a standalone test scenario name. 273 Note: This selection mechanism works only for Ztest suite and test function names in 274 the source files which are not generated by macro-substitutions. 275 Note: With --no-detailed-test-id use only Ztest names without scenario name. 276 """) 277 278 parser.add_argument( 279 "--pytest-args", action="append", 280 help="""Pass additional arguments to the pytest subprocess. This parameter 281 will extend the pytest_args from the harness_config in YAML file. 282 """) 283 284 parser.add_argument( 285 "--ctest-args", action="append", 286 help="""Pass additional arguments to the ctest subprocess. This parameter 287 will extend the ctest_args from the harness_config in YAML file. 288 """) 289 290 valgrind_asan_group.add_argument( 291 "--enable-valgrind", action="store_true", 292 help="""Run binary through valgrind and check for several memory access 293 errors. Valgrind needs to be installed on the host. This option only 294 works with host binaries such as those generated for the native_sim 295 configuration and is mutual exclusive with --enable-asan. 296 """) 297 298 valgrind_asan_group.add_argument( 299 "--enable-asan", action="store_true", 300 help="""Enable address sanitizer to check for several memory access 301 errors. Libasan needs to be installed on the host. This option only 302 works with host binaries such as those generated for the native_sim 303 configuration and is mutual exclusive with --enable-valgrind. 304 """) 305 306 # Start of individual args place them in alpha-beta order 307 308 board_root_list = [f"{ZEPHYR_BASE}/boards", f"{ZEPHYR_BASE}/subsys/testsuite/boards"] 309 310 modules = zephyr_module.parse_modules(ZEPHYR_BASE) 311 for module in modules: 312 board_root = module.meta.get("build", {}).get("settings", {}).get("board_root") 313 if board_root: 314 board_root_list.append(os.path.join(module.project, board_root, "boards")) 315 316 parser.add_argument( 317 "-A", "--board-root", action="append", default=board_root_list, 318 help="""Directory to search for board configuration files. All .yaml 319files in the directory will be processed. The directory should have the same 320structure in the main Zephyr tree: boards/<vendor>/<board_name>/""") 321 322 parser.add_argument( 323 "--allow-installed-plugin", action="store_true", default=None, 324 help="Allow to use pytest plugin installed by pip for pytest tests." 325 ) 326 327 parser.add_argument( 328 "-a", "--arch", action="append", 329 help="Arch filter for testing. Takes precedence over --platform. " 330 "If unspecified, test all arches. Multiple invocations " 331 "are treated as a logical 'or' relationship") 332 333 parser.add_argument( 334 "-B", "--subset", 335 help="Only run a subset of the tests, 1/4 for running the first 25%%, " 336 "3/5 means run the 3rd fifth of the total. " 337 "This option is useful when running a large number of tests on " 338 "different hosts to speed up execution time.") 339 340 parser.add_argument( 341 "--shuffle-tests", action="store_true", default=None, 342 help="""Shuffle test execution order to get randomly distributed tests across subsets. 343 Used only when --subset is provided.""") 344 345 parser.add_argument( 346 "--shuffle-tests-seed", action="store", default=None, 347 help="""Seed value for random generator used to shuffle tests. 348 If not provided, seed in generated by system. 349 Used only when --shuffle-tests is provided.""") 350 351 parser.add_argument( 352 "-c", "--clobber-output", action="store_true", 353 help="Cleaning the output directory will simply delete it instead " 354 "of the default policy of renaming.") 355 356 parser.add_argument( 357 "--cmake-only", action="store_true", 358 help="Only run cmake, do not build or run.") 359 360 coverage_group.add_argument("--enable-coverage", action="store_true", 361 help="Enable code coverage collection using gcov.") 362 363 coverage_group.add_argument("-C", "--coverage", action="store_true", 364 help="Generate coverage reports. Implies " 365 "--enable-coverage to collect the coverage data first.") 366 367 coverage_group.add_argument("--gcov-tool", type=Path, default=None, 368 help="Path to the 'gcov' tool to use for code coverage reports. " 369 "By default it will be chosen in the following order:" 370 " using ZEPHYR_TOOLCHAIN_VARIANT ('llvm': from LLVM_TOOLCHAIN_PATH)," 371 " gcov installed on the host - for 'native' or 'unit' platform types," 372 " using ZEPHYR_SDK_INSTALL_DIR.") 373 374 coverage_group.add_argument("--coverage-basedir", default=ZEPHYR_BASE, 375 help="Base source directory for coverage report.") 376 377 coverage_group.add_argument("--coverage-platform", action="append", default=[], 378 help="Platforms to run coverage reports on. " 379 "This option may be used multiple times. " 380 "Default to what was selected with --platform.") 381 382 coverage_group.add_argument("--coverage-tool", choices=['lcov', 'gcovr'], default='gcovr', 383 help="Tool to use to generate coverage reports (%(default)s - default).") 384 385 coverage_group.add_argument("--coverage-formats", action="store", default=None, 386 help="Output formats to use for generated coverage reports " + 387 "as a comma-separated list without spaces. " + 388 "Valid options for 'gcovr' tool are: " + 389 ','.join(supported_coverage_formats['gcovr']) + " (html - default)." + 390 " Valid options for 'lcov' tool are: " + 391 ','.join(supported_coverage_formats['lcov']) + " (html,lcov - default).") 392 393 coverage_group.add_argument("--coverage-per-instance", action="store_true", default=False, 394 help="""Compose individual coverage reports for each test suite 395 when coverage reporting is enabled; it might run in addition to 396 the default aggregation mode which produces one coverage report for 397 all executed test suites. Default: %(default)s""") 398 399 coverage_group.add_argument("--disable-coverage-aggregation", 400 action="store_true", default=False, 401 help="""Don't aggregate coverage report statistics for all the test suites 402 selected to run with enabled coverage. Requires another reporting mode to be 403 active (`--coverage-per-instance`) to have at least one of these reporting 404 modes. Default: %(default)s""") 405 406 parser.add_argument( 407 "--test-config", 408 action="store", 409 default=os.path.join(ZEPHYR_BASE, "tests", "test_config.yaml"), 410 help="Path to file with plans and test configurations." 411 ) 412 413 parser.add_argument("--level", action="store", 414 help="Test level to be used. By default, no levels are used for filtering" 415 "and do the selection based on existing filters.") 416 417 parser.add_argument( 418 "--device-serial-baud", action="store", default=None, 419 help="Serial device baud rate (default 115200)") 420 421 parser.add_argument( 422 "--disable-suite-name-check", action="store_true", default=False, 423 help="Disable extended test suite name verification at the beginning " 424 "of Ztest test. This option could be useful for tests or " 425 "platforms, which from some reasons cannot print early logs.") 426 427 parser.add_argument("-e", "--exclude-tag", action="append", 428 help="Specify tags of tests that should not run. " 429 "Default is to run all tests with all tags.") 430 431 parser.add_argument( 432 "--enable-lsan", action="store_true", 433 help="""Enable leak sanitizer to check for heap memory leaks. 434 Libasan needs to be installed on the host. This option only 435 works with host binaries such as those generated for the native_sim 436 configuration and when --enable-asan is given. 437 """) 438 439 parser.add_argument( 440 "--enable-ubsan", action="store_true", 441 help="""Enable undefined behavior sanitizer to check for undefined 442 behaviour during program execution. It uses an optional runtime library 443 to provide better error diagnostics. This option only works with host 444 binaries such as those generated for the native_sim configuration. 445 """) 446 447 parser.add_argument( 448 "--filter", choices=['buildable', 'runnable'], 449 default='runnable' if "--device-testing" in sys.argv else 'buildable', 450 help="""Filter tests to be built and executed. By default everything is 451 built and if a test is runnable (emulation or a connected device), it 452 is run. This option allows for example to only build tests that can 453 actually be run. Runnable is a subset of buildable.""") 454 455 parser.add_argument("--force-color", action="store_true", 456 help="Always output ANSI color escape sequences " 457 "even when the output is redirected (not a tty)") 458 459 parser.add_argument("--force-toolchain", action="store_true", 460 help="Do not filter based on toolchain, use the set " 461 " toolchain unconditionally") 462 463 footprint_group.add_argument( 464 "--create-rom-ram-report", 465 action="store_true", 466 help="Generate detailed json reports with ROM/RAM symbol sizes for each test image built " 467 "using additional build option `--target footprint`.") 468 469 footprint_group.add_argument( 470 "--footprint-report", 471 nargs="?", 472 default=None, 473 choices=['all', 'ROM', 'RAM'], 474 const="all", 475 help="Select which memory area symbols' data to collect as 'footprint' property " 476 "of each test suite built, and report in 'twister_footprint.json' together " 477 "with the relevant execution metadata the same way as in `twister.json`. " 478 "Implies '--create-rom-ram-report' to generate the footprint data files. " 479 "No value means '%(const)s'. Default: %(default)s""") 480 481 footprint_group.add_argument( 482 "--enable-size-report", 483 action="store_true", 484 help="Collect and report ROM/RAM section sizes for each test image built.") 485 486 parser.add_argument( 487 "--disable-unrecognized-section-test", 488 action="store_true", 489 default=False, 490 help="Don't error on unrecognized sections in the binary images.") 491 492 footprint_group.add_argument( 493 "--footprint-from-buildlog", 494 action = "store_true", 495 help="Take ROM/RAM sections footprint summary values from the 'build.log' " 496 "instead of 'objdump' results used otherwise." 497 "Requires --enable-size-report or one of the baseline comparison modes." 498 "Warning: the feature will not work correctly with sysbuild.") 499 500 compare_group_option = footprint_group.add_mutually_exclusive_group() 501 502 compare_group_option.add_argument( 503 "-m", "--last-metrics", 504 action="store_true", 505 help="Compare footprints to the previous twister invocation as a baseline " 506 "running in the same output directory. " 507 "Implies --enable-size-report option.") 508 509 compare_group_option.add_argument( 510 "--compare-report", 511 help="Use this report file as a baseline for footprint comparison. " 512 "The file should be of 'twister.json' schema. " 513 "Implies --enable-size-report option.") 514 515 footprint_group.add_argument( 516 "--show-footprint", 517 action="store_true", 518 help="With footprint comparison to a baseline, log ROM/RAM section deltas. ") 519 520 footprint_group.add_argument( 521 "-H", "--footprint-threshold", 522 type=float, 523 default=5.0, 524 help="With footprint comparison to a baseline, " 525 "warn the user for any of the footprint metric change which is greater or equal " 526 "to the specified percentage value. " 527 "Default is %(default)s for %(default)s%% delta from the new footprint value. " 528 "Use zero to warn on any footprint metric increase.") 529 530 footprint_group.add_argument( 531 "-D", "--all-deltas", 532 action="store_true", 533 help="With footprint comparison to a baseline, " 534 "warn on any footprint change, increase or decrease. " 535 "Implies --footprint-threshold=0") 536 537 footprint_group.add_argument( 538 "-z", "--size", 539 action="append", 540 metavar='FILENAME', 541 help="Ignore all other command line options and just produce a report to " 542 "stdout with ROM/RAM section sizes on the specified binary images.") 543 544 parser.add_argument( 545 "-i", "--inline-logs", action="store_true", 546 help="Upon test failure, print relevant log data to stdout " 547 "instead of just a path to it.") 548 549 parser.add_argument("--ignore-platform-key", action="store_true", 550 help="Do not filter based on platform key") 551 552 parser.add_argument( 553 "-j", "--jobs", type=int, 554 help="Number of jobs for building, defaults to number of CPU threads, " 555 "overcommitted by factor 2 when --build-only.") 556 557 parser.add_argument( 558 "-K", "--force-platform", action="store_true", 559 help="""Force testing on selected platforms, 560 even if they are excluded in the test configuration (testcase.yaml).""" 561 ) 562 563 parser.add_argument( 564 "-l", "--all", action="store_true", 565 help="Build/test on all platforms. Any --platform arguments " 566 "ignored.") 567 568 test_plan_report_xor.add_argument("--list-tags", action="store_true", 569 help="List all tags occurring in selected tests.") 570 571 parser.add_argument("--log-file", metavar="FILENAME", action="store", 572 help="Specify a file where to save logs.") 573 574 parser.add_argument( 575 "-M", "--runtime-artifact-cleanup", choices=['pass', 'all'], 576 default=None, const='pass', nargs='?', 577 help="""Cleanup test artifacts. The default behavior is 'pass' 578 which only removes artifacts of passing tests. If you wish to 579 remove all artificats including those of failed tests, use 'all'.""") 580 581 parser.add_argument( 582 "--keep-artifacts", action="append", default=[], 583 help="""Keep specified artifacts when cleaning up at runtime. Multiple invocations 584 are possible.""" 585 ) 586 test_xor_generator.add_argument( 587 "-N", "--ninja", action="store_true", 588 default=not any(a in sys.argv for a in ("-k", "--make")), 589 help="Use the Ninja generator with CMake. (This is the default)") 590 591 test_xor_generator.add_argument( 592 "-k", "--make", action="store_true", 593 help="Use the unix Makefile generator with CMake.") 594 595 parser.add_argument( 596 "-n", "--no-clean", action="store_true", 597 help="Re-use the outdir before building. Will result in " 598 "faster compilation since builds will be incremental.") 599 600 parser.add_argument( 601 "--aggressive-no-clean", action="store_true", 602 help="Re-use the outdir before building and do not re-run cmake. Will result in " 603 "much faster compilation since builds will be incremental. This option might " 604 " result in build failures and inconsistencies if dependencies change or when " 605 " applied on a significantly changed code base. Use on your own " 606 " risk. It is recommended to only use this option for local " 607 " development and when testing localized change in a subsystem.") 608 609 parser.add_argument( 610 '--detailed-test-id', action='store_true', 611 help="Compose each test Suite name from its configuration path (relative to root) and " 612 "the appropriate Scenario name using PATH_TO_TEST_CONFIG/SCENARIO_NAME schema. " 613 "Also (for Ztest only), prefix each test Case name with its Scenario name. " 614 "For example: 'kernel.common.timing' Scenario with test Suite name " 615 "'tests/kernel/sleep/kernel.common.timing' and 'kernel.common.timing.sleep.usleep' " 616 "test Case (where 'sleep' is its Ztest suite name and 'usleep' is Ztest test name.") 617 618 parser.add_argument( 619 "--no-detailed-test-id", dest='detailed_test_id', action="store_false", 620 help="Don't prefix each test Suite name with its configuration path, " 621 "so it is the same as the appropriate Scenario name. " 622 "Also (for Ztest only), don't prefix each Ztest Case name with its Scenario name. " 623 "For example: 'kernel.common.timing' Scenario name, the same Suite name, " 624 "and 'sleep.usleep' test Case (where 'sleep' is its Ztest suite name " 625 "and 'usleep' is Ztest test name.") 626 627 # Include paths in names by default. 628 parser.set_defaults(detailed_test_id=True) 629 630 parser.add_argument( 631 "--detailed-skipped-report", action="store_true", 632 help="Generate a detailed report with all skipped test cases" 633 "including those that are filtered based on testsuite definition." 634 ) 635 636 parser.add_argument( 637 "-O", "--outdir", 638 default=os.path.join(os.getcwd(), "twister-out"), 639 help="Output directory for logs and binaries. " 640 "Default is 'twister-out' in the current directory. " 641 "This directory will be cleaned unless '--no-clean' is set. " 642 "The '--clobber-output' option controls what cleaning does.") 643 644 parser.add_argument( 645 "-o", "--report-dir", 646 help="""Output reports containing results of the test run into the 647 specified directory. 648 The output will be both in JSON and JUNIT format 649 (twister.json and twister.xml). 650 """) 651 652 parser.add_argument("--overflow-as-errors", action="store_true", 653 help="Treat RAM/SRAM overflows as errors.") 654 655 parser.add_argument("--report-filtered", action="store_true", 656 help="Include filtered tests in the reports.") 657 658 parser.add_argument("-P", "--exclude-platform", action="append", default=[], 659 help="""Exclude platforms and do not build or run any tests 660 on those platforms. This option can be called multiple times. 661 """ 662 ) 663 664 parser.add_argument("--persistent-hardware-map", action='store_true', 665 help="""With --generate-hardware-map, tries to use 666 persistent names for serial devices on platforms 667 that support this feature (currently only Linux). 668 """) 669 670 parser.add_argument( 671 "--vendor", action="append", default=[], 672 help="Vendor filter for testing") 673 674 parser.add_argument( 675 "-p", "--platform", action="append", default=[], 676 help="Platform filter for testing. This option may be used multiple " 677 "times. Test suites will only be built/run on the platforms " 678 "specified. If this option is not used, then platforms marked " 679 "as default in the platform metadata file will be chosen " 680 "to build and test. ") 681 682 parser.add_argument( 683 "--platform-reports", action="store_true", 684 help="""Create individual reports for each platform. 685 """) 686 687 parser.add_argument("--pre-script", 688 help="""specify a pre script. This will be executed 689 before device handler open serial port and invoke runner. 690 """) 691 692 parser.add_argument( 693 "--quarantine-list", 694 action="append", 695 metavar="FILENAME", 696 help="Load list of test scenarios under quarantine. The entries in " 697 "the file need to correspond to the test scenarios names as in " 698 "corresponding tests .yaml files. These scenarios " 699 "will be skipped with quarantine as the reason.") 700 701 parser.add_argument( 702 "--quarantine-verify", 703 action="store_true", 704 help="Use the list of test scenarios under quarantine and run them" 705 "to verify their current status.") 706 707 parser.add_argument( 708 "--quit-on-failure", 709 action="store_true", 710 help="""quit twister once there is build / run failure 711 """) 712 713 parser.add_argument( 714 "--report-name", 715 help="""Create a report with a custom name. 716 """) 717 718 parser.add_argument( 719 "--report-summary", action="store", nargs='?', type=int, const=0, 720 help="Show failed/error report from latest run. Default shows all items found. " 721 "However, you can specify the number of items (e.g. --report-summary 15). " 722 "It also works well with the --outdir switch.") 723 724 parser.add_argument( 725 "--report-suffix", 726 help="""Add a suffix to all generated file names, for example to add a 727 version or a commit ID. 728 """) 729 730 parser.add_argument( 731 "--report-all-options", action="store_true", 732 help="""Show all command line options applied, including defaults, as 733 environment.options object in twister.json. Default: show only non-default settings. 734 """) 735 736 parser.add_argument( 737 "--retry-failed", type=int, default=0, 738 help="Retry failing tests again, up to the number of times specified.") 739 740 parser.add_argument( 741 "--retry-interval", type=int, default=60, 742 help="Retry failing tests after specified period of time.") 743 744 parser.add_argument( 745 "--retry-build-errors", action="store_true", 746 help="Retry build errors as well.") 747 748 parser.add_argument( 749 "-S", "--enable-slow", action="store_true", 750 default="--enable-slow-only" in sys.argv, 751 help="Execute time-consuming test cases that have been marked " 752 "as 'slow' in testcase.yaml. Normally these are only built.") 753 754 parser.add_argument( 755 "--enable-slow-only", action="store_true", 756 help="Execute time-consuming test cases that have been marked " 757 "as 'slow' in testcase.yaml only. This also set the option --enable-slow") 758 759 parser.add_argument( 760 "--seed", type=int, 761 help="Seed for native_sim pseudo-random number generator") 762 763 parser.add_argument( 764 "--short-build-path", 765 action="store_true", 766 help="Create shorter build directory paths based on symbolic links. " 767 "The shortened build path will be used by CMake for generating " 768 "the build system and executing the build. Use this option if " 769 "you experience build failures related to path length, for " 770 "example on Windows OS. This option can be used only with " 771 "'--ninja' argument (to use Ninja build generator).") 772 773 parser.add_argument( 774 "-t", "--tag", action="append", 775 help="Specify tags to restrict which tests to run by tag value. " 776 "Default is to not do any tag filtering. Multiple invocations " 777 "are treated as a logical 'or' relationship.") 778 779 parser.add_argument("--timestamps", 780 action="store_true", 781 help="Print all messages with time stamps.") 782 783 parser.add_argument( 784 "-u", 785 "--no-update", 786 action="store_true", 787 help="Do not update the results of the last run. This option " 788 "is only useful when reusing the same output directory of " 789 "twister, for example when re-running failed tests with --only-failed " 790 "or --no-clean. This option is for debugging purposes only.") 791 792 parser.add_argument( 793 "-v", 794 "--verbose", 795 action="count", 796 default=0, 797 help="Call multiple times to increase verbosity.") 798 799 parser.add_argument( 800 "-ll", 801 "--log-level", 802 type=str.upper, 803 default='INFO', 804 choices=['NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], 805 help="Select the threshold event severity for which you'd like to receive logs in console." 806 " Default is INFO.") 807 808 parser.add_argument("-W", "--disable-warnings-as-errors", action="store_true", 809 help="Do not treat warning conditions as errors.") 810 811 parser.add_argument( 812 "--west-flash", nargs='?', const=[], 813 help="""Uses west instead of ninja or make to flash when running with 814 --device-testing. Supports comma-separated argument list. 815 816 E.g "twister --device-testing --device-serial /dev/ttyACM0 817 --west-flash="--board-id=foobar,--erase" 818 will translate to "west flash -- --board-id=foobar --erase" 819 820 NOTE: device-testing must be enabled to use this option. 821 """ 822 ) 823 parser.add_argument( 824 "--west-runner", 825 help="""Uses the specified west runner instead of default when running 826 with --west-flash. 827 828 E.g "twister --device-testing --device-serial /dev/ttyACM0 829 --west-flash --west-runner=pyocd" 830 will translate to "west flash --runner pyocd" 831 832 NOTE: west-flash must be enabled to use this option. 833 """ 834 ) 835 836 parser.add_argument( 837 "-X", "--fixture", action="append", default=[], 838 help="Specify a fixture that a board might support.") 839 840 parser.add_argument( 841 "-x", "--extra-args", action="append", default=[], 842 help="""Extra CMake cache entries to define when building test cases. 843 May be called multiple times. The key-value entries will be 844 prefixed with -D before being passed to CMake. 845 E.g 846 "twister -x=USE_CCACHE=0" 847 will translate to 848 "cmake -DUSE_CCACHE=0" 849 which will ultimately disable ccache. 850 """ 851 ) 852 853 parser.add_argument( 854 "-y", "--dry-run", action="store_true", 855 help="""Create the filtered list of test cases, but don't actually 856 run them. Useful if you're just interested in the test plan 857 generated for every run and saved in the specified output 858 directory (testplan.json). 859 """) 860 861 parser.add_argument("extra_test_args", nargs=argparse.REMAINDER, 862 help="Additional args following a '--' are passed to the test binary") 863 864 parser.add_argument("--alt-config-root", action="append", default=[], 865 help="Alternative test configuration root/s. When a test is found, " 866 "Twister will check if a test configuration file exist in any of " 867 "the alternative test configuration root folders. For example, " 868 "given $test_root/tests/foo/testcase.yaml, Twister will use " 869 "$alt_config_root/tests/foo/testcase.yaml if it exists") 870 871 return parser 872 873 874def parse_arguments( 875 parser: argparse.ArgumentParser, 876 args, 877 options = None, 878 on_init=True 879) -> argparse.Namespace: 880 if options is None: 881 options = parser.parse_args(args) 882 883 # Very early error handling 884 if options.short_build_path and not options.ninja: 885 logger.error("--short-build-path requires Ninja to be enabled") 886 sys.exit(1) 887 888 if options.device_serial_pty and os.name == "nt": # OS is Windows 889 logger.error("--device-serial-pty is not supported on Windows OS") 890 sys.exit(1) 891 892 if options.west_runner and options.west_flash is None: 893 logger.error("west-runner requires west-flash to be enabled") 894 sys.exit(1) 895 896 if options.west_flash and not options.device_testing: 897 logger.error("west-flash requires device-testing to be enabled") 898 sys.exit(1) 899 900 if not options.testsuite_root: 901 # if we specify a test scenario which is part of a suite directly, do 902 # not set testsuite root to default, just point to the test directory 903 # directly. 904 if options.test: 905 for scenario in options.test: 906 if dirname := os.path.dirname(scenario): 907 options.testsuite_root.append(dirname) 908 909 # check again and make sure we have something set 910 if not options.testsuite_root: 911 options.testsuite_root = [os.path.join(ZEPHYR_BASE, "tests"), 912 os.path.join(ZEPHYR_BASE, "samples")] 913 914 if options.last_metrics or options.compare_report: 915 options.enable_size_report = True 916 917 if options.footprint_report: 918 options.create_rom_ram_report = True 919 920 if options.aggressive_no_clean: 921 options.no_clean = True 922 923 if options.coverage: 924 options.enable_coverage = True 925 926 if options.enable_coverage and not options.coverage_platform: 927 options.coverage_platform = options.platform 928 929 if ( 930 (not options.coverage) 931 and (options.disable_coverage_aggregation or options.coverage_per_instance) 932 ): 933 logger.error("Enable coverage reporting to set its aggregation mode.") 934 sys.exit(1) 935 936 if ( 937 options.coverage 938 and options.disable_coverage_aggregation and (not options.coverage_per_instance) 939 ): 940 logger.error("At least one coverage reporting mode should be enabled: " 941 "either aggregation, or per-instance, or both.") 942 sys.exit(1) 943 944 if options.coverage_formats: 945 for coverage_format in options.coverage_formats.split(','): 946 if coverage_format not in supported_coverage_formats[options.coverage_tool]: 947 logger.error(f"Unsupported coverage report formats:'{options.coverage_formats}' " 948 f"for {options.coverage_tool}") 949 sys.exit(1) 950 951 if options.enable_valgrind and not shutil.which("valgrind"): 952 logger.error("valgrind enabled but valgrind executable not found") 953 sys.exit(1) 954 955 if ( 956 (not options.device_testing) 957 and (options.device_serial or options.device_serial_pty or options.hardware_map) 958 ): 959 logger.error( 960 "Use --device-testing with --device-serial, or --device-serial-pty, or --hardware-map." 961 ) 962 sys.exit(1) 963 964 if ( 965 options.device_testing 966 and (options.device_serial or options.device_serial_pty) and len(options.platform) != 1 967 ): 968 logger.error("When --device-testing is used with --device-serial " 969 "or --device-serial-pty, exactly one platform must " 970 "be specified") 971 sys.exit(1) 972 973 if options.device_flash_with_test and not options.device_testing: 974 logger.error("--device-flash-with-test requires --device_testing") 975 sys.exit(1) 976 977 if options.flash_before and options.device_flash_with_test: 978 logger.error("--device-flash-with-test does not apply when --flash-before is used") 979 sys.exit(1) 980 981 if options.flash_before and options.device_serial_pty: 982 logger.error("--device-serial-pty cannot be used when --flash-before is set (for now)") 983 sys.exit(1) 984 985 if options.shuffle_tests and options.subset is None: 986 logger.error("--shuffle-tests requires --subset") 987 sys.exit(1) 988 989 if options.shuffle_tests_seed and options.shuffle_tests is None: 990 logger.error("--shuffle-tests-seed requires --shuffle-tests") 991 sys.exit(1) 992 993 if options.size: 994 from twisterlib.size_calc import SizeCalculator 995 for fn in options.size: 996 sc = SizeCalculator(fn, []) 997 sc.size_report() 998 sys.exit(0) 999 1000 if options.footprint_from_buildlog: 1001 logger.warning("WARNING: Using --footprint-from-buildlog will give inconsistent results " 1002 "for configurations using sysbuild. It is recommended to not use this flag " 1003 "when building configurations using sysbuild.") 1004 if not options.enable_size_report: 1005 logger.error("--footprint-from-buildlog requires --enable-size-report") 1006 sys.exit(1) 1007 1008 if len(options.extra_test_args) > 0: 1009 # extra_test_args is a list of CLI args that Twister did not recognize 1010 # and are intended to be passed through to the ztest executable. This 1011 # list should begin with a "--". If not, there is some extra 1012 # unrecognized arg(s) that shouldn't be there. Tell the user there is a 1013 # syntax error. 1014 if options.extra_test_args[0] != "--": 1015 try: 1016 double_dash = options.extra_test_args.index("--") 1017 except ValueError: 1018 double_dash = len(options.extra_test_args) 1019 unrecognized = " ".join(options.extra_test_args[0:double_dash]) 1020 1021 logger.error( 1022 f"Unrecognized arguments found: '{unrecognized}'." 1023 " Use -- to delineate extra arguments for test binary or pass -h for help." 1024 ) 1025 1026 sys.exit(1) 1027 1028 # Strip off the initial "--" following validation. 1029 options.extra_test_args = options.extra_test_args[1:] 1030 1031 if on_init and not options.allow_installed_plugin and PYTEST_PLUGIN_INSTALLED: 1032 logger.error("By default Twister should work without pytest-twister-harness " 1033 "plugin being installed, so please, uninstall it by " 1034 "`pip uninstall pytest-twister-harness` and `git clean " 1035 "-dxf scripts/pylib/pytest-twister-harness`.") 1036 sys.exit(1) 1037 elif on_init and options.allow_installed_plugin and PYTEST_PLUGIN_INSTALLED: 1038 logger.warning("You work with installed version of " 1039 "pytest-twister-harness plugin.") 1040 1041 return options 1042 1043def strip_ansi_sequences(s: str) -> str: 1044 """Remove ANSI escape sequences from a string.""" 1045 return re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', "", s) 1046 1047class TwisterEnv: 1048 1049 def __init__(self, options : argparse.Namespace, default_options=None) -> None: 1050 self.version = "Unknown" 1051 self.toolchain = None 1052 self.commit_date = "Unknown" 1053 self.run_date = None 1054 self.options = options 1055 self.default_options = default_options 1056 1057 if options.ninja: 1058 self.generator_cmd = "ninja" 1059 self.generator = "Ninja" 1060 else: 1061 self.generator_cmd = "make" 1062 self.generator = "Unix Makefiles" 1063 logger.info(f"Using {self.generator}..") 1064 1065 self.test_roots = options.testsuite_root 1066 1067 if not isinstance(options.board_root, list): 1068 self.board_roots = [options.board_root] 1069 else: 1070 self.board_roots = options.board_root 1071 self.outdir = os.path.abspath(options.outdir) 1072 1073 self.snippet_roots = [Path(ZEPHYR_BASE)] 1074 modules = zephyr_module.parse_modules(ZEPHYR_BASE) 1075 for module in modules: 1076 snippet_root = module.meta.get("build", {}).get("settings", {}).get("snippet_root") 1077 if snippet_root: 1078 self.snippet_roots.append(Path(module.project) / snippet_root) 1079 1080 1081 self.soc_roots = [Path(ZEPHYR_BASE), Path(ZEPHYR_BASE) / 'subsys' / 'testsuite'] 1082 self.dts_roots = [Path(ZEPHYR_BASE)] 1083 self.arch_roots = [Path(ZEPHYR_BASE)] 1084 1085 for module in modules: 1086 soc_root = module.meta.get("build", {}).get("settings", {}).get("soc_root") 1087 if soc_root: 1088 self.soc_roots.append(Path(module.project) / Path(soc_root)) 1089 dts_root = module.meta.get("build", {}).get("settings", {}).get("dts_root") 1090 if dts_root: 1091 self.dts_roots.append(Path(module.project) / Path(dts_root)) 1092 arch_root = module.meta.get("build", {}).get("settings", {}).get("arch_root") 1093 if arch_root: 1094 self.arch_roots.append(Path(module.project) / Path(arch_root)) 1095 1096 self.hwm = None 1097 1098 self.test_config = options.test_config 1099 1100 self.alt_config_root = options.alt_config_root 1101 1102 def non_default_options(self) -> dict: 1103 """Returns current command line options which are set to non-default values.""" 1104 diff = {} 1105 if not self.default_options: 1106 return diff 1107 dict_options = vars(self.options) 1108 dict_default = vars(self.default_options) 1109 for k in dict_options: 1110 if k not in dict_default or dict_options[k] != dict_default[k]: 1111 diff[k] = dict_options[k] 1112 return diff 1113 1114 def discover(self): 1115 self.check_zephyr_version() 1116 self.get_toolchain() 1117 self.run_date = datetime.now(timezone.utc).isoformat(timespec='seconds') 1118 1119 def check_zephyr_version(self): 1120 try: 1121 subproc = subprocess.run(["git", "describe", "--abbrev=12", "--always"], 1122 stdout=subprocess.PIPE, 1123 text=True, 1124 cwd=ZEPHYR_BASE) 1125 if subproc.returncode == 0: 1126 _version = subproc.stdout.strip() 1127 if _version: 1128 self.version = _version 1129 logger.info(f"Zephyr version: {self.version}") 1130 except OSError: 1131 logger.exception("Failure while reading Zephyr version.") 1132 1133 if self.version == "Unknown": 1134 logger.warning("Could not determine version") 1135 1136 try: 1137 subproc = subprocess.run(["git", "show", "-s", "--format=%cI", "HEAD"], 1138 stdout=subprocess.PIPE, 1139 text=True, 1140 cwd=ZEPHYR_BASE) 1141 if subproc.returncode == 0: 1142 self.commit_date = subproc.stdout.strip() 1143 except OSError: 1144 logger.exception("Failure while reading head commit date.") 1145 1146 @staticmethod 1147 def run_cmake_script(args=None): 1148 if args is None: 1149 args = [] 1150 script = os.fspath(args[0]) 1151 1152 logger.debug(f"Running cmake script {script}") 1153 1154 cmake_args = ["-D{}".format(a.replace('"', '')) for a in args[1:]] 1155 cmake_args.extend(['-P', script]) 1156 1157 cmake = shutil.which('cmake') 1158 if not cmake: 1159 msg = "Unable to find `cmake` in path" 1160 logger.error(msg) 1161 raise Exception(msg) 1162 cmd = [cmake] + cmake_args 1163 log_command(logger, "Calling cmake", cmd) 1164 1165 kwargs = dict() 1166 kwargs['stdout'] = subprocess.PIPE 1167 # CMake sends the output of message() to stderr unless it's STATUS 1168 kwargs['stderr'] = subprocess.STDOUT 1169 1170 p = subprocess.Popen(cmd, **kwargs) 1171 out, _ = p.communicate() 1172 1173 # It might happen that the environment adds ANSI escape codes like \x1b[0m, 1174 # for instance if twister is executed from inside a makefile. In such a 1175 # scenario it is then necessary to remove them, as otherwise the JSON decoding 1176 # will fail. 1177 out = strip_ansi_sequences(out.decode()) 1178 1179 if p.returncode == 0: 1180 msg = f"Finished running {args[0]}" 1181 logger.debug(msg) 1182 results = {"returncode": p.returncode, "msg": msg, "stdout": out} 1183 1184 else: 1185 logger.error(f"CMake script failure: {args[0]}") 1186 results = {"returncode": p.returncode, "returnmsg": out} 1187 1188 return results 1189 1190 def get_toolchain(self): 1191 toolchain_script = Path(ZEPHYR_BASE) / Path('cmake/verify-toolchain.cmake') 1192 result = self.run_cmake_script([toolchain_script, "FORMAT=json"]) 1193 1194 try: 1195 if result['returncode']: 1196 raise TwisterRuntimeError(f"E: {result['returnmsg']}") 1197 except Exception as e: 1198 print(str(e)) 1199 sys.exit(2) 1200 self.toolchain = json.loads(result['stdout'])['ZEPHYR_TOOLCHAIN_VARIANT'] 1201 logger.info(f"Using '{self.toolchain}' toolchain.") 1202