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