|  | #!/usr/bin/env python3 | 
|  | # vim: set syntax=python ts=4 : | 
|  | # Copyright (c) 2020 Intel Corporation | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  | """Zephyr Test Runner (twister) | 
|  |  | 
|  | Also check the "User and Developer Guides" at https://docs.zephyrproject.org/ | 
|  |  | 
|  | This script scans for the set of unit test applications in the git | 
|  | repository and attempts to execute them. By default, it tries to | 
|  | build each test case on one platform per architecture, using a precedence | 
|  | list defined in an architecture configuration file, and if possible | 
|  | run the tests in any available emulators or simulators on the system. | 
|  |  | 
|  | Test cases are detected by the presence of a 'testcase.yaml' or a sample.yaml | 
|  | files in the application's project directory. This file may contain one or more | 
|  | blocks, each identifying a test scenario. The title of the block is a name for | 
|  | the test case, which only needs to be unique for the test cases specified in | 
|  | that testsuite meta-data. The full canonical name for each test case is <path to | 
|  | test case>/<block>. | 
|  |  | 
|  | Each test block in the testsuite meta data can define the following key/value | 
|  | pairs: | 
|  |  | 
|  | tags: <list of tags> (required) | 
|  | A set of string tags for the testsuite. Usually pertains to | 
|  | functional domains but can be anything. Command line invocations | 
|  | of this script can filter the set of tests to run based on tag. | 
|  |  | 
|  | skip: <True|False> (default False) | 
|  | skip testsuite unconditionally. This can be used for broken tests. | 
|  |  | 
|  | slow: <True|False> (default False) | 
|  | Don't build or run this test case unless --enable-slow was passed | 
|  | in on the command line. Intended for time-consuming test cases | 
|  | that are only run under certain circumstances, like daily | 
|  | builds. | 
|  |  | 
|  | extra_args: <list of extra arguments> | 
|  | Extra cache entries to pass to CMake when building or running the | 
|  | test case. | 
|  |  | 
|  | extra_configs: <list of extra configurations> | 
|  | Extra configuration options to be merged with a master prj.conf | 
|  | when building or running the test case. | 
|  |  | 
|  | build_only: <True|False> (default False) | 
|  | If true, don't try to run the test even if the selected platform | 
|  | supports it. | 
|  |  | 
|  | build_on_all: <True|False> (default False) | 
|  | If true, attempt to build test on all available platforms. | 
|  |  | 
|  | depends_on: <list of features> | 
|  | A board or platform can announce what features it supports, this option | 
|  | will enable the test only those platforms that provide this feature. | 
|  |  | 
|  | min_ram: <integer> | 
|  | minimum amount of RAM needed for this test to build and run. This is | 
|  | compared with information provided by the board metadata. | 
|  |  | 
|  | min_flash: <integer> | 
|  | minimum amount of ROM needed for this test to build and run. This is | 
|  | compared with information provided by the board metadata. | 
|  |  | 
|  | modules: <list of modules> | 
|  | Add list of modules needed for this sample to build and run. | 
|  |  | 
|  | timeout: <number of seconds> | 
|  | Length of time to run test in emulator before automatically killing it. | 
|  | Default to 60 seconds. | 
|  |  | 
|  | arch_allow: <list of arches, such as x86, arm, arc> | 
|  | Set of architectures that this test case should only be run for. | 
|  |  | 
|  | arch_exclude: <list of arches, such as x86, arm, arc> | 
|  | Set of architectures that this test case should not run on. | 
|  |  | 
|  | platform_allow: <list of platforms> | 
|  | Set of platforms that this test case should only be run for. | 
|  |  | 
|  | platform_exclude: <list of platforms> | 
|  | Set of platforms that this test case should not run on. | 
|  |  | 
|  | extra_sections: <list of extra binary sections> | 
|  | When computing sizes, twister will report errors if it finds | 
|  | extra, unexpected sections in the Zephyr binary unless they are named | 
|  | here. They will not be included in the size calculation. | 
|  |  | 
|  | filter: <expression> | 
|  | Filter whether the testsuite should be run by evaluating an expression | 
|  | against an environment containing the following values: | 
|  |  | 
|  | { ARCH : <architecture>, | 
|  | PLATFORM : <platform>, | 
|  | <all CONFIG_* key/value pairs in the test's generated defconfig>, | 
|  | <all DT_* key/value pairs in the test's generated device tree file>, | 
|  | <all CMake key/value pairs in the test's generated CMakeCache.txt file>, | 
|  | *<env>: any environment variable available | 
|  | } | 
|  |  | 
|  | The grammar for the expression language is as follows: | 
|  |  | 
|  | expression ::= expression "and" expression | 
|  | | expression "or" expression | 
|  | | "not" expression | 
|  | | "(" expression ")" | 
|  | | symbol "==" constant | 
|  | | symbol "!=" constant | 
|  | | symbol "<" number | 
|  | | symbol ">" number | 
|  | | symbol ">=" number | 
|  | | symbol "<=" number | 
|  | | symbol "in" list | 
|  | | symbol ":" string | 
|  | | symbol | 
|  |  | 
|  | list ::= "[" list_contents "]" | 
|  |  | 
|  | list_contents ::= constant | 
|  | | list_contents "," constant | 
|  |  | 
|  | constant ::= number | 
|  | | string | 
|  |  | 
|  |  | 
|  | For the case where expression ::= symbol, it evaluates to true | 
|  | if the symbol is defined to a non-empty string. | 
|  |  | 
|  | Operator precedence, starting from lowest to highest: | 
|  |  | 
|  | or (left associative) | 
|  | and (left associative) | 
|  | not (right associative) | 
|  | all comparison operators (non-associative) | 
|  |  | 
|  | arch_allow, arch_exclude, platform_allow, platform_exclude | 
|  | are all syntactic sugar for these expressions. For instance | 
|  |  | 
|  | arch_exclude = x86 arc | 
|  |  | 
|  | Is the same as: | 
|  |  | 
|  | filter = not ARCH in ["x86", "arc"] | 
|  |  | 
|  | The ':' operator compiles the string argument as a regular expression, | 
|  | and then returns a true value only if the symbol's value in the environment | 
|  | matches. For example, if CONFIG_SOC="stm32f107xc" then | 
|  |  | 
|  | filter = CONFIG_SOC : "stm.*" | 
|  |  | 
|  | Would match it. | 
|  |  | 
|  | The set of test cases that actually run depends on directives in the testsuite | 
|  | files and options passed in on the command line. If there is any confusion, | 
|  | running with -v or examining the test plan report (testplan.json) | 
|  | can help show why particular test cases were skipped. | 
|  |  | 
|  | To load arguments from a file, write '+' before the file name, e.g., | 
|  | +file_name. File content must be one or more valid arguments separated by | 
|  | line break instead of white spaces. | 
|  |  | 
|  | Most everyday users will run with no arguments. | 
|  |  | 
|  | """ | 
|  |  | 
|  | import os | 
|  | import sys | 
|  | import logging | 
|  | import time | 
|  | import shutil | 
|  | import colorama | 
|  | from colorama import Fore | 
|  | from pathlib import Path | 
|  |  | 
|  |  | 
|  | ZEPHYR_BASE = os.getenv("ZEPHYR_BASE") | 
|  | if not ZEPHYR_BASE: | 
|  | # This file has been zephyr/scripts/twister for years, | 
|  | # and that is not going to change anytime soon. Let the user | 
|  | # run this script as ./scripts/twister without making them | 
|  | # set ZEPHYR_BASE. | 
|  | ZEPHYR_BASE = str(Path(__file__).resolve().parents[1]) | 
|  |  | 
|  | # Propagate this decision to child processes. | 
|  | os.environ['ZEPHYR_BASE'] = ZEPHYR_BASE | 
|  |  | 
|  | print(f'ZEPHYR_BASE unset, using "{ZEPHYR_BASE}"') | 
|  |  | 
|  | sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/pylib/twister/")) | 
|  |  | 
|  | from twisterlib.testplan import TestPlan | 
|  | from twisterlib.environment import TwisterEnv, parse_arguments | 
|  | from twisterlib.reports import Reporting | 
|  | from twisterlib.hardwaremap import HardwareMap | 
|  | from twisterlib.coverage import run_coverage | 
|  | from twisterlib.runner import TwisterRunner | 
|  |  | 
|  | logger = logging.getLogger('twister') | 
|  | logger.setLevel(logging.DEBUG) | 
|  |  | 
|  | def setup_logging(outdir, log_file, verbose, timestamps): | 
|  | # create file handler which logs even debug messages | 
|  | if log_file: | 
|  | fh = logging.FileHandler(log_file) | 
|  | else: | 
|  | fh = logging.FileHandler(os.path.join(outdir, "twister.log")) | 
|  |  | 
|  | fh.setLevel(logging.DEBUG) | 
|  |  | 
|  | # create console handler with a higher log level | 
|  | ch = logging.StreamHandler() | 
|  |  | 
|  | if verbose > 1: | 
|  | ch.setLevel(logging.DEBUG) | 
|  | else: | 
|  | ch.setLevel(logging.INFO) | 
|  |  | 
|  | # create formatter and add it to the handlers | 
|  | if timestamps: | 
|  | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') | 
|  | else: | 
|  | formatter = logging.Formatter('%(levelname)-7s - %(message)s') | 
|  |  | 
|  | formatter_file = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') | 
|  | ch.setFormatter(formatter) | 
|  | fh.setFormatter(formatter_file) | 
|  |  | 
|  | # add the handlers to logger | 
|  | logger.addHandler(ch) | 
|  | logger.addHandler(fh) | 
|  |  | 
|  |  | 
|  | def init_color(colorama_strip): | 
|  | colorama.init(strip=colorama_strip) | 
|  |  | 
|  | def main(): | 
|  | start_time = time.time() | 
|  |  | 
|  | options = parse_arguments(sys.argv[1:]) | 
|  |  | 
|  | # Configure color output | 
|  | color_strip = False if options.force_color else None | 
|  |  | 
|  | colorama.init(strip=color_strip) | 
|  | init_color(colorama_strip=color_strip) | 
|  |  | 
|  | previous_results = None | 
|  | # Cleanup | 
|  | if options.no_clean or options.only_failed or options.test_only: | 
|  | if os.path.exists(options.outdir): | 
|  | print("Keeping artifacts untouched") | 
|  | elif options.last_metrics: | 
|  | ls = os.path.join(options.outdir, "twister.json") | 
|  | if os.path.exists(ls): | 
|  | with open(ls, "r") as fp: | 
|  | previous_results = fp.read() | 
|  | else: | 
|  | sys.exit(f"Can't compare metrics with non existing file {ls}") | 
|  | elif os.path.exists(options.outdir): | 
|  | if options.clobber_output: | 
|  | print("Deleting output directory {}".format(options.outdir)) | 
|  | shutil.rmtree(options.outdir) | 
|  | else: | 
|  | for i in range(1, 100): | 
|  | new_out = options.outdir + ".{}".format(i) | 
|  | if not os.path.exists(new_out): | 
|  | print("Renaming output directory to {}".format(new_out)) | 
|  | shutil.move(options.outdir, new_out) | 
|  | break | 
|  |  | 
|  | previous_results_file = None | 
|  | os.makedirs(options.outdir, exist_ok=True) | 
|  | if options.last_metrics and previous_results: | 
|  | previous_results_file = os.path.join(options.outdir, "baseline.json") | 
|  | with open(previous_results_file, "w") as fp: | 
|  | fp.write(previous_results) | 
|  |  | 
|  | VERBOSE = options.verbose | 
|  | setup_logging(options.outdir, options.log_file, VERBOSE, options.timestamps) | 
|  |  | 
|  | env = TwisterEnv(options) | 
|  | env.discover() | 
|  |  | 
|  | hwm = HardwareMap(env) | 
|  | ret = hwm.discover() | 
|  | if ret == 0: | 
|  | return 0 | 
|  |  | 
|  | env.hwm = hwm | 
|  |  | 
|  | tplan = TestPlan(env) | 
|  | try: | 
|  | tplan.discover() | 
|  | except RuntimeError as e: | 
|  | logger.error(f"{e}") | 
|  | return 1 | 
|  |  | 
|  | if tplan.report() == 0: | 
|  | return 0 | 
|  |  | 
|  | try: | 
|  | tplan.load() | 
|  | except RuntimeError as e: | 
|  | logger.error(f"{e}") | 
|  | return 1 | 
|  |  | 
|  | if options.list_tests and options.platform: | 
|  | tplan.report_platform_tests(options.platform) | 
|  | return 0 | 
|  |  | 
|  | if VERBOSE > 1: | 
|  | # if we are using command line platform filter, no need to list every | 
|  | # other platform as excluded, we know that already. | 
|  | # Show only the discards that apply to the selected platforms on the | 
|  | # command line | 
|  |  | 
|  | for i in tplan.instances.values(): | 
|  | if i.status == "filtered": | 
|  | if options.platform and i.platform.name not in options.platform: | 
|  | continue | 
|  | logger.debug( | 
|  | "{:<25} {:<50} {}SKIPPED{}: {}".format( | 
|  | i.platform.name, | 
|  | i.testsuite.name, | 
|  | Fore.YELLOW, | 
|  | Fore.RESET, | 
|  | i.reason)) | 
|  |  | 
|  | if options.report_excluded: | 
|  | tplan.report_excluded_tests() | 
|  | return 0 | 
|  |  | 
|  | report = Reporting(tplan, env) | 
|  | plan_file = os.path.join(options.outdir, "testplan.json") | 
|  | if not os.path.exists(plan_file): | 
|  | report.json_report(plan_file) | 
|  |  | 
|  | if options.save_tests: | 
|  | report.json_report(options.save_tests) | 
|  | return 0 | 
|  |  | 
|  | if options.device_testing and not options.build_only: | 
|  | print("\nDevice testing on:") | 
|  | hwm.dump(filtered=tplan.selected_platforms) | 
|  | print("") | 
|  |  | 
|  | if options.dry_run: | 
|  | duration = time.time() - start_time | 
|  | logger.info("Completed in %d seconds" % (duration)) | 
|  | return 0 | 
|  |  | 
|  | if options.short_build_path: | 
|  | tplan.create_build_dir_links() | 
|  |  | 
|  | runner = TwisterRunner(tplan.instances, tplan.testsuites, env) | 
|  | runner.duts = hwm.duts | 
|  | runner.run() | 
|  |  | 
|  | # figure out which report to use for size comparison | 
|  | report_to_use = None | 
|  | if options.compare_report: | 
|  | report_to_use = options.compare_report | 
|  | elif options.last_metrics: | 
|  | report_to_use = previous_results_file | 
|  |  | 
|  | report.footprint_reports(report_to_use, | 
|  | options.show_footprint, | 
|  | options.all_deltas, | 
|  | options.footprint_threshold, | 
|  | options.last_metrics) | 
|  |  | 
|  | duration = time.time() - start_time | 
|  |  | 
|  | runner.results.summary() | 
|  |  | 
|  | report.summary(runner.results, options.disable_unrecognized_section_test, duration) | 
|  |  | 
|  | if options.coverage: | 
|  | if not options.build_only: | 
|  | run_coverage(tplan, options) | 
|  | else: | 
|  | logger.info("Skipping coverage report generation due to --build-only.") | 
|  |  | 
|  | if options.device_testing and not options.build_only: | 
|  | hwm.summary(tplan.selected_platforms) | 
|  |  | 
|  | report.save_reports(options.report_name, | 
|  | options.report_suffix, | 
|  | options.report_dir, | 
|  | options.no_update, | 
|  | options.platform_reports | 
|  | ) | 
|  |  | 
|  | logger.info("Run completed") | 
|  | if runner.results.failed or runner.results.error or (tplan.warnings and options.warnings_as_errors): | 
|  | return 1 | 
|  |  | 
|  | return 0 | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | ret = 0 | 
|  | try: | 
|  | ret = main() | 
|  | finally: | 
|  | if (os.name != "nt") and os.isatty(1): | 
|  | # (OS is not Windows) and (stdout is interactive) | 
|  | os.system("stty sane <&1") | 
|  |  | 
|  | sys.exit(ret) |