diff --git a/scripts/pylib/twister/twisterlib/config_parser.py b/scripts/pylib/twister/twisterlib/config_parser.py index 843fce51a7cf31c..23837d6e449ecf6 100644 --- a/scripts/pylib/twister/twisterlib/config_parser.py +++ b/scripts/pylib/twister/twisterlib/config_parser.py @@ -93,7 +93,7 @@ def __init__(self, filename, schema): self.common = {} def load(self): - self.data = scl.yaml_load_verify(self.filename, self.schema) + self.data = scl.yaml_load_verify(str(self.filename), self.schema) if 'tests' in self.data: self.scenarios = self.data['tests'] diff --git a/scripts/pylib/twister/twisterlib/coverage.py b/scripts/pylib/twister/twisterlib/coverage.py index 8a0ea95569b826e..a8ce16d0d564b80 100644 --- a/scripts/pylib/twister/twisterlib/coverage.py +++ b/scripts/pylib/twister/twisterlib/coverage.py @@ -220,6 +220,7 @@ def run_command(self, cmd, coveragelog): "--ignore-errors", "mismatch,mismatch", ] + cmd = [str(c) for c in cmd] cmd_str = " ".join(cmd) logger.debug(f"Running {cmd_str}...") return subprocess.call(cmd, stdout=coveragelog) @@ -345,6 +346,7 @@ def _generate(self, outdir, coveragelog): "--gcov-executable", self.gcov_tool, "-e", "tests/*"] cmd += excludes + mode_options + ["--json", "-o", coveragefile, outdir] + cmd = [str(c) for c in cmd] cmd_str = " ".join(cmd) logger.debug(f"Running {cmd_str}...") subprocess.call(cmd, stdout=coveragelog) diff --git a/scripts/pylib/twister/twisterlib/environment.py b/scripts/pylib/twister/twisterlib/environment.py index 7219e674b42157c..bf8ef94db457159 100644 --- a/scripts/pylib/twister/twisterlib/environment.py +++ b/scripts/pylib/twister/twisterlib/environment.py @@ -15,7 +15,6 @@ import sys from datetime import datetime, timezone from importlib import metadata -from pathlib import Path from typing import Generator, List from twisterlib.coverage import supported_coverage_formats @@ -25,6 +24,7 @@ from twisterlib.error import TwisterRuntimeError from twisterlib.log_helper import log_command +from twisterlib.twister_path import TPath ZEPHYR_BASE = os.getenv("ZEPHYR_BASE") if not ZEPHYR_BASE: @@ -112,7 +112,7 @@ def add_parse_arguments(parser = None): help="Load a list of tests and platforms to be run from file.") case_select.add_argument( - "-T", "--testsuite-root", action="append", default=[], type = norm_path, + "-T", "--testsuite-root", action="append", default=[], type = TPath, help="Base directory to recursively search for test cases. All " "testcase.yaml files under here will be processed. May be " "called multiple times. Defaults to the 'samples/' and " @@ -217,7 +217,7 @@ def add_parse_arguments(parser = None): and global timeout multiplier (this parameter)""") test_xor_subtest.add_argument( - "-s", "--test", "--scenario", action="append", type = norm_path, + "-s", "--test", "--scenario", action="append", type = TPath, help="Run only the specified testsuite scenario. These are named by " "") @@ -255,17 +255,17 @@ def add_parse_arguments(parser = None): # Start of individual args place them in alpha-beta order - board_root_list = ["%s/boards" % ZEPHYR_BASE, - "%s/subsys/testsuite/boards" % ZEPHYR_BASE] + board_root_list = [TPath("%s/boards" % ZEPHYR_BASE), + TPath("%s/subsys/testsuite/boards" % ZEPHYR_BASE)] modules = zephyr_module.parse_modules(ZEPHYR_BASE) for module in modules: board_root = module.meta.get("build", {}).get("settings", {}).get("board_root") if board_root: - board_root_list.append(os.path.join(module.project, board_root, "boards")) + board_root_list.append(TPath(os.path.join(module.project, board_root, "boards"))) parser.add_argument( - "-A", "--board-root", action="append", default=board_root_list, + "-A", "--board-root", action="append", default=board_root_list, type=TPath, help="""Directory to search for board configuration files. All .yaml files in the directory will be processed. The directory should have the same structure in the main Zephyr tree: boards///""") @@ -312,7 +312,7 @@ def add_parse_arguments(parser = None): "--cmake-only", action="store_true", help="Only run cmake, do not build or run.") - parser.add_argument("--coverage-basedir", default=ZEPHYR_BASE, + parser.add_argument("--coverage-basedir", default=ZEPHYR_BASE, type=TPath, help="Base source directory for coverage report.") parser.add_argument("--coverage-platform", action="append", default=[], @@ -330,7 +330,8 @@ def add_parse_arguments(parser = None): " Valid options for 'lcov' tool are: " + ','.join(supported_coverage_formats['lcov']) + " (html,lcov - default).") - parser.add_argument("--test-config", action="store", default=os.path.join(ZEPHYR_BASE, "tests", "test_config.yaml"), + parser.add_argument("--test-config", action="store", type=TPath, + default=TPath(os.path.join(ZEPHYR_BASE, "tests", "test_config.yaml")), help="Path to file with plans and test configurations.") parser.add_argument("--level", action="store", @@ -386,7 +387,7 @@ def add_parse_arguments(parser = None): help="Do not filter based on toolchain, use the set " " toolchain unconditionally") - parser.add_argument("--gcov-tool", type=Path, default=None, + parser.add_argument("--gcov-tool", type=TPath, default=None, help="Path to the gcov tool to use for code coverage " "reports") @@ -468,6 +469,7 @@ def add_parse_arguments(parser = None): "-z", "--size", action="append", metavar='FILENAME', + type=TPath, help="Ignore all other command line options and just produce a report to " "stdout with ROM/RAM section sizes on the specified binary images.") @@ -498,7 +500,7 @@ def add_parse_arguments(parser = None): parser.add_argument("--list-tags", action="store_true", help="List all tags occurring in selected tests.") - parser.add_argument("--log-file", metavar="FILENAME", action="store", + parser.add_argument("--log-file", metavar="FILENAME", action="store", type=TPath, help="Specify a file where to save logs.") parser.add_argument( @@ -553,15 +555,15 @@ def add_parse_arguments(parser = None): ) parser.add_argument( - "-O", "--outdir", - default=os.path.join(os.getcwd(), "twister-out"), + "-O", "--outdir", type=TPath, + default=TPath(os.path.join(os.getcwd(), "twister-out")), help="Output directory for logs and binaries. " "Default is 'twister-out' in the current directory. " "This directory will be cleaned unless '--no-clean' is set. " "The '--clobber-output' option controls what cleaning does.") parser.add_argument( - "-o", "--report-dir", + "-o", "--report-dir", type=TPath, help="""Output reports containing results of the test run into the specified directory. The output will be both in JSON and JUNIT format @@ -612,6 +614,7 @@ def add_parse_arguments(parser = None): "--quarantine-list", action="append", metavar="FILENAME", + type=TPath, help="Load list of test scenarios under quarantine. The entries in " "the file need to correspond to the test scenarios names as in " "corresponding tests .yaml files. These scenarios " @@ -766,7 +769,7 @@ def add_parse_arguments(parser = None): parser.add_argument("extra_test_args", nargs=argparse.REMAINDER, help="Additional args following a '--' are passed to the test binary") - parser.add_argument("--alt-config-root", action="append", default=[], + parser.add_argument("--alt-config-root", action="append", default=[], type=TPath, help="Alternative test configuration root/s. When a test is found, " "Twister will check if a test configuration file exist in any of " "the alternative test configuration root folders. For example, " @@ -808,8 +811,8 @@ def parse_arguments(parser, args, options = None, on_init=True): # check again and make sure we have something set if not options.testsuite_root: - options.testsuite_root = [os.path.join(ZEPHYR_BASE, "tests"), - os.path.join(ZEPHYR_BASE, "samples")] + options.testsuite_root = [TPath(os.path.join(ZEPHYR_BASE, "tests")), + TPath(os.path.join(ZEPHYR_BASE, "samples"))] if options.last_metrics or options.compare_report: options.enable_size_report = True @@ -946,17 +949,17 @@ def __init__(self, options=None, default_options=None) -> None: self.board_roots = [self.options.board_root] else: self.board_roots = self.options.board_root - self.outdir = os.path.abspath(options.outdir) + self.outdir = TPath(os.path.abspath(options.outdir)) else: self.board_roots = None self.outdir = None - self.snippet_roots = [Path(ZEPHYR_BASE)] + self.snippet_roots = [TPath(ZEPHYR_BASE)] modules = zephyr_module.parse_modules(ZEPHYR_BASE) for module in modules: snippet_root = module.meta.get("build", {}).get("settings", {}).get("snippet_root") if snippet_root: - self.snippet_roots.append(Path(module.project) / snippet_root) + self.snippet_roots.append(TPath(module.project) / snippet_root) self.hwm = None @@ -1051,7 +1054,7 @@ def run_cmake_script(args=[]): return results def get_toolchain(self): - toolchain_script = Path(ZEPHYR_BASE) / Path('cmake/verify-toolchain.cmake') + toolchain_script = TPath(ZEPHYR_BASE) / TPath('cmake/verify-toolchain.cmake') result = self.run_cmake_script([toolchain_script, "FORMAT=json"]) try: diff --git a/scripts/pylib/twister/twisterlib/handlers.py b/scripts/pylib/twister/twisterlib/handlers.py index dab77fc1a74e83b..7e998bb8597506c 100755 --- a/scripts/pylib/twister/twisterlib/handlers.py +++ b/scripts/pylib/twister/twisterlib/handlers.py @@ -57,15 +57,23 @@ def terminate_process(proc): so we need to use try_kill_process_by_pid. """ - for child in psutil.Process(proc.pid).children(recursive=True): + parent = psutil.Process(proc.pid) + to_terminate = parent.children(recursive=True) + to_terminate.append(parent) + + for p in to_terminate: + try: + p.terminate() + except (ProcessLookupError, psutil.NoSuchProcess): + pass + _, alive = psutil.wait_procs(to_terminate, timeout=1) + + for p in alive: try: - os.kill(child.pid, signal.SIGTERM) + p.kill() except (ProcessLookupError, psutil.NoSuchProcess): pass - proc.terminate() - # sleep for a while before attempting to kill - time.sleep(0.5) - proc.kill() + _, alive = psutil.wait_procs(to_terminate, timeout=1) class Handler: @@ -186,10 +194,8 @@ def try_kill_process_by_pid(self): pid = int(open(self.pid_fn).read()) os.unlink(self.pid_fn) self.pid_fn = None # clear so we don't try to kill the binary twice - try: - os.kill(pid, signal.SIGKILL) - except (ProcessLookupError, psutil.NoSuchProcess): - pass + p = psutil.Process(pid) + terminate_process(p) def _output_reader(self, proc): self.line = proc.stdout.readline() diff --git a/scripts/pylib/twister/twisterlib/package.py b/scripts/pylib/twister/twisterlib/package.py index e767587b70bb50b..dceb095d5bed518 100644 --- a/scripts/pylib/twister/twisterlib/package.py +++ b/scripts/pylib/twister/twisterlib/package.py @@ -30,7 +30,13 @@ def package(self): if t['status'] != TwisterStatus.FILTER: p = t['platform'] normalized = p.replace("/", "_") - dirs.append(os.path.join(self.options.outdir, normalized, t['name'])) + print(os.getcwd()) + print(self.options.outdir) + print(normalized) + print(t['name']) + dir = os.path.join(self.options.outdir, normalized, t['name']) + print(dir) + dirs.append(dir) dirs.extend( [ @@ -38,4 +44,7 @@ def package(self): os.path.join(self.options.outdir, "testplan.json") ] ) + + print(dirs) + self.make_tarfile(self.options.package_artifacts, dirs) diff --git a/scripts/pylib/twister/twisterlib/reports.py b/scripts/pylib/twister/twisterlib/reports.py index 780f05f2424e177..73f8198823b582d 100644 --- a/scripts/pylib/twister/twisterlib/reports.py +++ b/scripts/pylib/twister/twisterlib/reports.py @@ -12,7 +12,9 @@ import xml.etree.ElementTree as ET import string from datetime import datetime -from pathlib import PosixPath +from pathlib import PosixPath, WindowsPath + +from twisterlib.twister_path import TPath from twisterlib.statuses import TwisterStatus @@ -267,8 +269,14 @@ def json_report(self, filename, version="NA", platform=None, filters=None): report_options = self.env.non_default_options() # Resolve known JSON serialization problems. - for k,v in report_options.items(): - report_options[k] = str(v) if type(v) in [PosixPath] else v + for k, v in report_options.items(): + pathlikes = [PosixPath, WindowsPath, TPath] + value = v + if type(v) in pathlikes: + value = os.fspath(v) + if type(v) in [list]: + value = [os.fspath(x) if type(x) in pathlikes else x for x in v] + report_options[k] = value report = {} report["environment"] = {"os": os.name, @@ -310,7 +318,7 @@ def json_report(self, filename, version="NA", platform=None, filters=None): "name": instance.testsuite.name, "arch": instance.platform.arch, "platform": instance.platform.name, - "path": instance.testsuite.source_dir_rel + "path": os.fspath(instance.testsuite.source_dir_rel) } if instance.run_id: suite['run_id'] = instance.run_id diff --git a/scripts/pylib/twister/twisterlib/size_calc.py b/scripts/pylib/twister/twisterlib/size_calc.py index 0c6634c874213dd..9cac04c10fee905 100644 --- a/scripts/pylib/twister/twisterlib/size_calc.py +++ b/scripts/pylib/twister/twisterlib/size_calc.py @@ -205,7 +205,7 @@ def _check_is_xip(self) -> None: # Search for CONFIG_XIP in the ELF's list of symbols using NM and AWK. # GREP can not be used as it returns an error if the symbol is not # found. - is_xip_command = "nm " + self.elf_filename + \ + is_xip_command = "nm " + str(self.elf_filename) + \ " | awk '/CONFIG_XIP/ { print $3 }'" is_xip_output = subprocess.check_output( is_xip_command, shell=True, stderr=subprocess.STDOUT).decode( @@ -221,7 +221,7 @@ def _check_is_xip(self) -> None: def _get_info_elf_sections(self) -> None: """Calculate RAM and ROM usage and information about issues by section""" - objdump_command = "objdump -h " + self.elf_filename + objdump_command = "objdump -h " + str(self.elf_filename) objdump_output = subprocess.check_output( objdump_command, shell=True).decode("utf-8").splitlines() diff --git a/scripts/pylib/twister/twisterlib/testinstance.py b/scripts/pylib/twister/twisterlib/testinstance.py index 6ababf62d0cad81..896273c13c53d81 100644 --- a/scripts/pylib/twister/twisterlib/testinstance.py +++ b/scripts/pylib/twister/twisterlib/testinstance.py @@ -65,7 +65,7 @@ def __init__(self, testsuite, platform, outdir): self.build_dir = os.path.join(outdir, platform.normalized_name, testsuite.name) else: # if suite is not in zephyr, keep only the part after ".." in reconstructed dir structure - source_dir_rel = testsuite.source_dir_rel.rsplit(os.pardir+os.path.sep, 1)[-1] + source_dir_rel = testsuite.source_dir_rel.get_rel_after_dots() self.build_dir = os.path.join(outdir, platform.normalized_name, source_dir_rel, testsuite.name) self.run_id = self._get_run_id() self.domains = None diff --git a/scripts/pylib/twister/twisterlib/testplan.py b/scripts/pylib/twister/twisterlib/testplan.py index 44862cd5a63b921..19793c4c1d7825b 100755 --- a/scripts/pylib/twister/twisterlib/testplan.py +++ b/scripts/pylib/twister/twisterlib/testplan.py @@ -34,6 +34,7 @@ from twisterlib.config_parser import TwisterConfigParser from twisterlib.statuses import TwisterStatus from twisterlib.testinstance import TestInstance +from twisterlib.twister_path import TPath from twisterlib.quarantine import Quarantine import list_boards @@ -528,13 +529,13 @@ def add_testsuites(self, testsuite_filter=[]): logger.debug("Found possible testsuite in " + dirpath) - suite_yaml_path = os.path.join(dirpath, filename) + suite_yaml_path = TPath(os.path.join(dirpath, filename)) suite_path = os.path.dirname(suite_yaml_path) for alt_config_root in self.env.alt_config_root: - alt_config = os.path.join(os.path.abspath(alt_config_root), + alt_config = TPath(os.path.join(os.path.abspath(alt_config_root), os.path.relpath(suite_path, root), - filename) + filename)) if os.path.exists(alt_config): logger.info("Using alternative configuration from %s" % os.path.normpath(alt_config)) diff --git a/scripts/pylib/twister/twisterlib/testsuite.py b/scripts/pylib/twister/twisterlib/testsuite.py index 5759aa9499961c0..caf340097d569ba 100644 --- a/scripts/pylib/twister/twisterlib/testsuite.py +++ b/scripts/pylib/twister/twisterlib/testsuite.py @@ -17,6 +17,7 @@ from twisterlib.environment import canonical_zephyr_base from twisterlib.error import TwisterException, TwisterRuntimeError from twisterlib.statuses import TwisterStatus +from twisterlib.twister_path import TPath logger = logging.getLogger('twister') logger.setLevel(logging.DEBUG) @@ -271,11 +272,16 @@ def find_c_files_in(path: str, extensions: list = ['c', 'cpp', 'cxx', 'cc']) -> os.chdir(path) filenames = [] + ffs = [] for ext in extensions: # glob.glob('**/*.c') does not pick up the base directory - filenames += [os.path.join(path, x) for x in glob.glob(f'*.{ext}')] + filenames += [TPath(path, x) for x in glob.glob(f'*.{ext}')] # glob matches in subdirectories too - filenames += [os.path.join(path, x) for x in glob.glob(f'**/*.{ext}')] + filenames += [TPath(path, x) for x in glob.glob(f'**/*.{ext}')] + # glob.glob('**/*.c') does not pick up the base directory + ffs += [os.path.join(path, x) for x in glob.glob(f'*.{ext}')] + # glob matches in subdirectories too + ffs += [os.path.join(path, x) for x in glob.glob(f'**/*.{ext}')] # restore previous CWD os.chdir(oldpwd) @@ -347,10 +353,10 @@ def _find_src_dir_path(test_dir_path): optimization reasons it is placed in upper directory. """ src_dir_name = "src" - src_dir_path = os.path.join(test_dir_path, src_dir_name) + src_dir_path = TPath(test_dir_path, src_dir_name) if os.path.isdir(src_dir_path): return src_dir_path - src_dir_path = os.path.join(test_dir_path, "..", src_dir_name) + src_dir_path = TPath(os.path.join(test_dir_path, "..", src_dir_name)) if os.path.isdir(src_dir_path): return src_dir_path return "" @@ -414,7 +420,7 @@ def __init__(self, suite_root, suite_path, name, data=None, detailed_test_id=Tru the testcase.yaml defines multiple tests """ - workdir = os.path.relpath(suite_path, suite_root) + workdir = TPath(os.path.relpath(suite_path, suite_root)) assert self.check_suite_name(name, suite_root, workdir) self.detailed_test_id = detailed_test_id @@ -422,7 +428,7 @@ def __init__(self, suite_root, suite_path, name, data=None, detailed_test_id=Tru self.id = name self.source_dir = suite_path - self.source_dir_rel = os.path.relpath(os.path.realpath(suite_path), start=canonical_zephyr_base) + self.source_dir_rel = TPath(os.path.relpath(os.path.realpath(suite_path), start=canonical_zephyr_base)) self.yamlfile = suite_path self.testcases = [] self.integration_platforms = [] @@ -482,18 +488,18 @@ def add_testcase(self, name, freeform=False): @staticmethod def get_unique(testsuite_root, workdir, name): - canonical_testsuite_root = os.path.realpath(testsuite_root) + canonical_testsuite_root = TPath(os.path.realpath(testsuite_root)) if Path(canonical_zephyr_base) in Path(canonical_testsuite_root).parents: # This is in ZEPHYR_BASE, so include path in name for uniqueness # FIXME: We should not depend on path of test for unique names. - relative_ts_root = os.path.relpath(canonical_testsuite_root, - start=canonical_zephyr_base) + relative_ts_root = TPath(os.path.relpath(canonical_testsuite_root, + start=canonical_zephyr_base)) else: relative_ts_root = "" # workdir can be "." - unique = os.path.normpath(os.path.join(relative_ts_root, workdir, name)).replace(os.sep, '/') - return unique + unique = TPath(os.path.normpath(os.path.join(relative_ts_root, workdir, name))) + return unique.get_rel_after_dots_str() @staticmethod def check_suite_name(name, testsuite_root, workdir): diff --git a/scripts/pylib/twister/twisterlib/twister_main.py b/scripts/pylib/twister/twisterlib/twister_main.py index b813713a1ce8b99..3a23b1a55d3707d 100644 --- a/scripts/pylib/twister/twisterlib/twister_main.py +++ b/scripts/pylib/twister/twisterlib/twister_main.py @@ -20,6 +20,7 @@ from twisterlib.runner import TwisterRunner from twisterlib.environment import TwisterEnv from twisterlib.package import Artifacts +from twisterlib.twister_path import TPath logger = logging.getLogger("twister") logger.setLevel(logging.DEBUG) @@ -30,7 +31,7 @@ def setup_logging(outdir, log_file, verbose, timestamps): if log_file: fh = logging.FileHandler(log_file) else: - fh = logging.FileHandler(os.path.join(outdir, "twister.log")) + fh = logging.FileHandler(outdir / "twister.log") fh.setLevel(logging.DEBUG) @@ -78,7 +79,7 @@ def main(options, default_options): if os.path.exists(options.outdir): print("Keeping artifacts untouched") elif options.last_metrics: - ls = os.path.join(options.outdir, "twister.json") + ls = options.outdir / "twister.json" if os.path.exists(ls): with open(ls, "r") as fp: previous_results = fp.read() @@ -90,7 +91,7 @@ def main(options, default_options): shutil.rmtree(options.outdir) else: for i in range(1, 100): - new_out = options.outdir + ".{}".format(i) + new_out = TPath(str(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) @@ -102,7 +103,7 @@ def main(options, default_options): 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") + previous_results_file = options.outdir / "baseline.json" with open(previous_results_file, "w") as fp: fp.write(previous_results) @@ -156,7 +157,7 @@ def main(options, default_options): ) report = Reporting(tplan, env) - plan_file = os.path.join(options.outdir, "testplan.json") + plan_file = options.outdir / "testplan.json" if not os.path.exists(plan_file): report.json_report(plan_file) @@ -235,6 +236,12 @@ def main(options, default_options): artifacts.package() logger.info("Run completed") + handlers = logger.handlers[:] + for handler in handlers: + logger.removeHandler(handler) + handler.close() + + if ( runner.results.failed or runner.results.error diff --git a/scripts/pylib/twister/twisterlib/twister_path.py b/scripts/pylib/twister/twisterlib/twister_path.py new file mode 100644 index 000000000000000..e32ddf4dc30e022 --- /dev/null +++ b/scripts/pylib/twister/twisterlib/twister_path.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# vim: set syntax=python ts=4 : +# +# Copyright (c) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os + +from pathlib import Path, PosixPath + +ZEPHYR_BASE = os.getenv("ZEPHYR_BASE") +canonical_zephyr_base = os.path.realpath(ZEPHYR_BASE) + + +class TPath(os.PathLike): + def __init__(self, path, *args): + self.path = Path(path) + + for p in args: + self.path = self._joinpath(p) + + def get_longpath(self): + normalised = os.fspath(self.path.resolve()) + + if isinstance(self.path, PosixPath): + return Path(normalised) + + # On Windows, without this prefix, there is 260-character path length limit. + if not normalised.startswith('\\\\?\\'): + normalised = '\\\\?\\' + normalised + return Path(normalised) + + def _joinpath(self, other): + return self.path.joinpath(str(other)) + + def joinpath(self, other): + res = TPath(self._joinpath(other)) + return res + + def get_rel_str(self): + str_path = os.path.relpath(str(self.path), start=canonical_zephyr_base) + return str_path + + def get_rel_after_dots(self): + str_path = str(self.path) + str_path = str_path.rsplit(os.pardir+os.path.sep, 1)[-1] + return TPath(str_path) + + def get_rel_after_dots_str(self): + str_path = str(self.path) + if os.path.isabs(str_path): + return str_path + str_path = os.path.relpath(str(self.path), start=canonical_zephyr_base) + str_path = str_path.rsplit(os.pardir+os.path.sep, 1)[-1] + return str_path + + def is_dir(self): + return self.path.is_dir() + + def __hash__(self): + return hash(os.path.abspath(os.fspath(self))) + + def __eq__(self, other): + try: + return os.path.abspath(os.fspath(self)) == os.path.abspath(os.fspath(other)) + except TypeError: + return False + + def __lt__(self, other): + return str(self) < str(other) + + def __truediv__(self, other): + return self.joinpath(other) + + def __rtruediv__(self, other): + return self.joinpath(other) + + def __add__(self, other): + return self.joinpath(other) + + def __radd__(self, other): + return self.joinpath(other) + + def __iadd__(self, other): + self.path = self._joinpath(other) + + def __str__(self): + return str(self.get_longpath()) + + def __repr__(self): + return '' + + def __fspath__(self): + return os.fspath(self.path) + + def __format__(self, format_spec): + return self.__str__().format(format_spec) diff --git a/scripts/tests/twister/conftest.py b/scripts/tests/twister/conftest.py index bc391ad5e3d54f1..97213374ffcdcbe 100644 --- a/scripts/tests/twister/conftest.py +++ b/scripts/tests/twister/conftest.py @@ -57,7 +57,6 @@ def testplan_obj(test_data, class_env, testsuites_dir, tmpdir_factory): env = class_env env.board_roots = [test_data +"board_config/1_level/2_level/"] env.test_roots = [testsuites_dir + '/tests', testsuites_dir + '/samples'] - env.outdir = tmpdir_factory.mktemp("sanity_out_demo") plan = TestPlan(env) plan.parse_configuration(config_file=env.test_config) return plan diff --git a/scripts/tests/twister/pytest_integration/test_harness_pytest.py b/scripts/tests/twister/pytest_integration/test_harness_pytest.py index e4382d5f8d0ae93..43df194dd717378 100644 --- a/scripts/tests/twister/pytest_integration/test_harness_pytest.py +++ b/scripts/tests/twister/pytest_integration/test_harness_pytest.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations +import os import pytest import textwrap @@ -16,7 +17,7 @@ @pytest.fixture def testinstance() -> TestInstance: - testsuite = TestSuite('.', 'samples/hello', 'unit.test') + testsuite = TestSuite('.', os.path.join('samples', 'hello'), 'unit.test') testsuite.harness_config = {} testsuite.ignore_faults = False testsuite.sysbuild = False @@ -40,9 +41,9 @@ def test_pytest_command(testinstance: TestInstance, device_type): testinstance.handler.type_str = device_type ref_command = [ 'pytest', - 'samples/hello/pytest', + os.path.join('samples', 'hello', 'pytest'), f'--build-dir={testinstance.build_dir}', - f'--junit-xml={testinstance.build_dir}/report.xml', + f'--junit-xml={os.path.join(testinstance.build_dir, "report.xml")}', f'--device-type={device_type}', '--twister-fixture=fixture1:option1', '--twister-fixture=fixture2' @@ -89,39 +90,52 @@ def test_pytest_command_extra_args_in_options(testinstance: TestInstance): ('pytest_root', 'expected'), [ ( - ['pytest/test_shell_help.py'], - ['samples/hello/pytest/test_shell_help.py'] + [os.path.join('pytest', 'test_shell_help.py')], + [os.path.join('samples', 'hello', 'pytest', 'test_shell_help.py')] ), ( - ['pytest/test_shell_help.py', 'pytest/test_shell_version.py', 'test_dir'], - ['samples/hello/pytest/test_shell_help.py', - 'samples/hello/pytest/test_shell_version.py', - 'samples/hello/test_dir'] + [ + os.path.join('pytest', 'test_shell_help.py'), + os.path.join('pytest', 'test_shell_version.py'), + os.path.join('test_dir') + ], + [ + os.path.join('samples', 'hello', 'pytest', 'test_shell_help.py'), + os.path.join('samples', 'hello', 'pytest', 'test_shell_version.py'), + os.path.join('samples', 'hello', 'test_dir') + ] ), ( - ['../shell/pytest/test_shell.py'], - ['samples/shell/pytest/test_shell.py'] + [os.path.join('..', 'shell', 'pytest', 'test_shell.py')], + [os.path.join('samples', 'shell', 'pytest', 'test_shell.py')] ), ( - ['/tmp/test_temp.py'], - ['/tmp/test_temp.py'] + [os.path.abspath(os.path.join(os.sep, 'tmp', 'test_temp.py'))], + [os.path.abspath(os.path.join(os.sep, 'tmp', 'test_temp.py'))] ), ( - ['~/tmp/test_temp.py'], - ['/home/joe/tmp/test_temp.py'] + [os.path.join('~', 'tmp', 'test_temp.py')], + [os.path.abspath(os.path.join(os.sep, 'home', 'joe', 'tmp', 'test_temp.py'))] ), ( - ['$ZEPHYR_BASE/samples/subsys/testsuite/pytest/shell/pytest'], - ['/zephyr_base/samples/subsys/testsuite/pytest/shell/pytest'] + [os.path.join('$ZEPHYR_BASE', 'samples', 'subsys', 'testsuite', + 'pytest', 'shell', 'pytest')], + [os.path.abspath(os.path.join(os.sep, 'zephyr_base', 'samples', 'subsys', 'testsuite', + 'pytest', 'shell', 'pytest'))] ), ( - ['pytest/test_shell_help.py::test_A', 'pytest/test_shell_help.py::test_B'], - ['samples/hello/pytest/test_shell_help.py::test_A', - 'samples/hello/pytest/test_shell_help.py::test_B'] + [ + os.path.join('pytest', 'test_shell_help.py::test_A'), + os.path.join('pytest', 'test_shell_help.py::test_B') + ], + [ + os.path.join('samples', 'hello', 'pytest', 'test_shell_help.py::test_A'), + os.path.join('samples', 'hello', 'pytest', 'test_shell_help.py::test_B') + ] ), ( - ['pytest/test_shell_help.py::test_A[param_a]'], - ['samples/hello/pytest/test_shell_help.py::test_A[param_a]'] + [os.path.join('pytest', 'test_shell_help.py::test_A[param_a]')], + [os.path.join('samples', 'hello', 'pytest', 'test_shell_help.py::test_A[param_a]')] ) ], ids=[ @@ -136,12 +150,14 @@ def test_pytest_command_extra_args_in_options(testinstance: TestInstance): ] ) def test_pytest_handle_source_list(testinstance: TestInstance, monkeypatch, pytest_root, expected): - monkeypatch.setenv('ZEPHYR_BASE', '/zephyr_base') - monkeypatch.setenv('HOME', '/home/joe') + monkeypatch.setenv('ZEPHYR_BASE', os.path.abspath(os.path.join(os.sep, 'zephyr_base'))) + monkeypatch.setenv('HOME', os.path.abspath(os.path.join(os.sep, 'home', 'joe'))) + monkeypatch.setenv('USERPROFILE', os.path.abspath(os.path.join(os.sep, 'home', 'joe'))) testinstance.testsuite.harness_config['pytest_root'] = pytest_root pytest_harness = Pytest() pytest_harness.configure(testinstance) command = pytest_harness.generate_command() + for pytest_src in expected: assert pytest_src in command diff --git a/scripts/tests/twister/test_environment.py b/scripts/tests/twister/test_environment.py index 13db3ed25275ad3..dc0436a4b072e8f 100644 --- a/scripts/tests/twister/test_environment.py +++ b/scripts/tests/twister/test_environment.py @@ -24,13 +24,6 @@ ['--short-build-path', '-k'], '--short-build-path requires Ninja to be enabled' ), - ( - 'nt', - None, - None, - ['--device-serial-pty', 'dummy'], - '--device-serial-pty is not supported on Windows OS' - ), ( None, None, @@ -128,24 +121,39 @@ ), ] +TESTIDS_1 = [ + 'short build path without ninja', + 'west runner without west flash', + 'west-flash without device-testing', + 'valgrind without executable', + 'device serial without platform', + 'device serial with multiple platforms', + 'device flash with test without device testing', + 'shuffle-tests without subset', + 'shuffle-tests-seed without shuffle-tests', + 'unrecognised argument', + 'pytest-twister-harness installed' +] + +if os.name == 'nt': + + TESTDATA_1 += [ + ( + 'nt', + None, + None, + ['--device-serial-pty', 'dummy'], + '--device-serial-pty is not supported on Windows OS' + ) + ] + + TESTIDS_1 += ['device-serial-pty on Windows'] + @pytest.mark.parametrize( 'os_name, which_dict, pytest_plugin, args, expected_error', TESTDATA_1, - ids=[ - 'short build path without ninja', - 'device-serial-pty on Windows', - 'west runner without west flash', - 'west-flash without device-testing', - 'valgrind without executable', - 'device serial without platform', - 'device serial with multiple platforms', - 'device flash with test without device testing', - 'shuffle-tests without subset', - 'shuffle-tests-seed without shuffle-tests', - 'unrecognised argument', - 'pytest-twister-harness installed' - ] + ids=TESTIDS_1 ) def test_parse_arguments_errors( caplog, @@ -244,8 +252,11 @@ def test_parse_arguments(zephyr_base, additional_args): options = twisterlib.environment.parse_arguments(parser, args) - assert os.path.join(zephyr_base, 'tests') in options.testsuite_root - assert os.path.join(zephyr_base, 'samples') in options.testsuite_root + expected_tests_path = os.path.realpath(os.path.join(zephyr_base, 'tests')) + expected_samples_path = os.path.realpath(os.path.join(zephyr_base, 'samples')) + + assert expected_tests_path in options.testsuite_root + assert expected_samples_path in options.testsuite_root assert options.enable_size_report @@ -355,7 +366,7 @@ def mocked_abspath(path): if path == 'dummy_abspath': return 'dummy_abspath' elif isinstance(path, mock.Mock): - return None + return '' else: return original_abspath(path) @@ -459,7 +470,7 @@ def mocked_abspath(path): if path == 'dummy_abspath': return 'dummy_abspath' elif isinstance(path, mock.Mock): - return None + return '' else: return original_abspath(path) @@ -468,8 +479,7 @@ def mocked_abspath(path): with mock.patch('subprocess.run', mock.Mock(side_effect=mock_run)): twister_env.check_zephyr_version() - print(expected_logs) - print(caplog.text) + assert twister_env.version == expected_version assert twister_env.commit_date == expected_commit_date assert all([expected_log in caplog.text for expected_log in expected_logs]) @@ -587,7 +597,7 @@ def mocked_abspath(path): if path == 'dummy_abspath': return 'dummy_abspath' elif isinstance(path, mock.Mock): - return None + return '' else: return original_abspath(path) diff --git a/scripts/tests/twister/test_errors.py b/scripts/tests/twister/test_errors.py index 426258f4cb8f023..8e68172acea11e8 100644 --- a/scripts/tests/twister/test_errors.py +++ b/scripts/tests/twister/test_errors.py @@ -8,6 +8,7 @@ import os import pytest +import re from pathlib import Path from twisterlib.error import ConfigurationError @@ -18,5 +19,5 @@ def test_configurationerror(): expected_err = f'{os.path.join("some", "path")}: dummy message' - with pytest.raises(ConfigurationError, match=expected_err): + with pytest.raises(ConfigurationError, match=re.escape(expected_err)): raise ConfigurationError(cfile, message) diff --git a/scripts/tests/twister/test_handlers.py b/scripts/tests/twister/test_handlers.py index 4d5505ae473353d..0d3bfb9bf7181c7 100644 --- a/scripts/tests/twister/test_handlers.py +++ b/scripts/tests/twister/test_handlers.py @@ -31,6 +31,7 @@ BinaryHandler, DeviceHandler, QEMUHandler, + QEMUWinHandler, SimulationHandler ) from twisterlib.hardwaremap import ( @@ -76,19 +77,21 @@ def time(self): return Counter() - +TESTIDS_1 = ['import pty nt', 'import serial+pty posix'] TESTDATA_1 = [ - (True, False, 'posix', ['Install pyserial python module with pip to use' \ - ' --device-testing option.'], None), (False, True, 'nt', [], None), (True, True, 'posix', ['Install pyserial python module with pip to use' \ ' --device-testing option.'], ImportError), ] +if sys.platform == 'linux': + TESTDATA_1.append((True, False, 'posix', ['Install pyserial python module with pip to use --device-testing option.'], None)) + TESTIDS_1.append('import serial') + @pytest.mark.parametrize( 'fail_serial, fail_pty, os_name, expected_outs, expected_error', TESTDATA_1, - ids=['import serial', 'import pty nt', 'import serial+pty posix'] + ids=TESTIDS_1 ) def test_imports( capfd, @@ -106,8 +109,8 @@ def find_spec(self, fullname, path, target=None): raise ImportError() modules_mock = sys.modules.copy() - modules_mock['serial'] = None if fail_serial else modules_mock['serial'] - modules_mock['pty'] = None if fail_pty else modules_mock['pty'] + modules_mock['serial'] = None if fail_serial else modules_mock.get('serial') + modules_mock['pty'] = None if fail_pty else modules_mock.get('pty') meta_path_mock = sys.meta_path[:] meta_path_mock.insert(0, ImportRaiser()) @@ -213,65 +216,108 @@ def test_handler_missing_suite_name(mocked_instance): def test_handler_terminate(mocked_instance): - def mock_kill_function(pid, sig): + def mock_kill_function(pid): if pid < 0: raise ProcessLookupError + def mock_process_init(pid): + return mock.Mock( + pid = pid, + kill = mock.Mock(side_effect=lambda: mock_kill_function(pid)), + terminate = mock.Mock(side_effect=lambda: mock_kill_function(pid)), + children = mock.Mock(return_value=[]), + wait = mock.Mock() + ) + + def mock_wait_procs(proc_list, timeout=None): + gone = [] + alive = [] + for p in proc_list: + if p.pid > 0: + gone.append(p) + else: + if p.pid == 0 and len(p.kill.call_args_list) > 0: + gone.append(p) + else: + alive.append(p) + + return gone, alive + + mock_parent = mock_process_init(0) + mock_child_neg1 = mock_process_init(-1) + mock_child1 = mock_process_init(1) + mock_child2 = mock_process_init(2) + mock_parent.children = mock.Mock(return_value=[mock_child1, mock_child2]) + + def mock_process_get(pid): + if pid == -1: + return mock_child_neg1 + if pid == 0: + return mock_parent + if pid == 1: + return mock_child1 + if pid == 2: + return mock_child2 + raise ProcessLookupError + instance = mocked_instance handler = Handler(instance) - mock_process = mock.Mock() - mock_child1 = mock.Mock(pid=1) - mock_child2 = mock.Mock(pid=2) - mock_process.children = mock.Mock(return_value=[mock_child1, mock_child2]) - mock_proc = mock.Mock(pid=0) mock_proc.terminate = mock.Mock(return_value=None) mock_proc.kill = mock.Mock(return_value=None) - with mock.patch('psutil.Process', return_value=mock_process), \ - mock.patch( - 'os.kill', - mock.Mock(side_effect=mock_kill_function) - ) as mock_kill: + with mock.patch('psutil.Process', side_effect=mock_process_get), \ + mock.patch('psutil.wait_procs', side_effect=mock_wait_procs): handler.terminate(mock_proc) assert handler.terminated - mock_proc.terminate.assert_called_once() - mock_proc.kill.assert_called_once() - mock_kill.assert_has_calls( - [mock.call(1, signal.SIGTERM), mock.call(2, signal.SIGTERM)] - ) + mock_parent.terminate.assert_called_once() + mock_child1.terminate.assert_called_once() + mock_child2.terminate.assert_called_once() + mock_parent.kill.assert_called_once() - mock_child_neg1 = mock.Mock(pid=-1) - mock_process.children = mock.Mock( + mock_parent.children = mock.Mock( return_value=[mock_child_neg1, mock_child2] ) handler.terminated = False - mock_kill.reset_mock() + mock_parent.reset_mock() + mock_parent.terminate.reset_mock() + mock_parent.kill.reset_mock() + mock_child2.reset_mock() + mock_child2.terminate.reset_mock() + mock_child2.kill.reset_mock() handler.terminate(mock_proc) - mock_kill.assert_has_calls( - [mock.call(-1, signal.SIGTERM), mock.call(2, signal.SIGTERM)] - ) + mock_parent.terminate.assert_called_once() + mock_child_neg1.terminate.assert_called_once() + mock_child2.terminate.assert_called_once() + mock_child_neg1.kill.assert_called_once() + mock_parent.kill.assert_called_once() def test_binaryhandler_try_kill_process_by_pid(mocked_instance): - def mock_kill_function(pid, sig): + def mock_kill_function(pid): if pid < 0: raise ProcessLookupError + def mock_process_init(pid): + return mock.Mock( + pid = pid, + kill = mock.Mock(side_effect=lambda: mock_kill_function(pid)), + terminate = mock.Mock(side_effect=lambda: mock_kill_function(pid)), + children = mock.Mock(return_value=[]), + wait = mock.Mock() + ) + instance = mocked_instance handler = BinaryHandler(instance, 'build') handler.pid_fn = os.path.join('dummy', 'path', 'to', 'pid.pid') - with mock.patch( - 'os.kill', - mock.Mock(side_effect=mock_kill_function) - ) as mock_kill, \ + with mock.patch('psutil.Process', mock.Mock(side_effect=mock_process_init)) as mock_proc, \ mock.patch('os.unlink', mock.Mock()) as mock_unlink: with mock.patch('builtins.open', mock.mock_open(read_data='1')): handler.try_kill_process_by_pid() @@ -279,10 +325,10 @@ def mock_kill_function(pid, sig): mock_unlink.assert_called_once_with( os.path.join('dummy', 'path', 'to', 'pid.pid') ) - mock_kill.assert_called_once_with(1, signal.SIGKILL) + mock_proc.assert_called_with(1) mock_unlink.reset_mock() - mock_kill.reset_mock() + mock_proc.reset_mock() handler.pid_fn = os.path.join('dummy', 'path', 'to', 'pid.pid') with mock.patch('builtins.open', mock.mock_open(read_data='-1')): @@ -291,7 +337,7 @@ def mock_kill_function(pid, sig): mock_unlink.assert_called_once_with( os.path.join('dummy', 'path', 'to', 'pid.pid') ) - mock_kill.assert_called_once_with(-1, signal.SIGKILL) + mock_proc.assert_called_with(-1) TESTDATA_3 = [ @@ -420,7 +466,8 @@ def wait(self, *args, **kwargs): '--log-file=build_dir/valgrind.log', '--track-origins=yes', 'generator']), (False, True, False, 123, None, ['generator', 'run', '--seed=123']), - (False, False, False, None, ['ex1', 'ex2'], ['build_dir/zephyr/zephyr.exe', 'ex1', 'ex2']), + (False, False, False, None, ['ex1', 'ex2'], + [os.path.join('build_dir', 'zephyr', 'zephyr.exe'), 'ex1', 'ex2']), ] @pytest.mark.parametrize( @@ -1288,16 +1335,22 @@ def mock_serial(*args, **kwargs): ser_pty_process.communicate.assert_called_once() +TESTIDS_16 = ['no pty'] TESTDATA_16 = [ - ('dummy1 dummy2', None, 'slave name'), - ('dummy1,dummy2', CalledProcessError, None), (None, None, 'dummy hardware serial'), ] +if os.name != 'nt': + TESTIDS_16.extend(['pty', 'pty process error']) + TESTDATA_16.extend([ + ('dummy1 dummy2', None, 'slave name'), + ('dummy1,dummy2', CalledProcessError, None), + ]) + @pytest.mark.parametrize( 'serial_pty, popen_exception, expected_device', TESTDATA_16, - ids=['pty', 'pty process error', 'no pty'] + ids=TESTIDS_16 ) def test_devicehandler_get_serial_device( mocked_instance, @@ -1319,8 +1372,8 @@ def mock_popen(command, *args, **kwargs): ttyname_mock = mock.Mock(side_effect=lambda x: x + ' name') with mock.patch('subprocess.Popen', popen_mock), \ - mock.patch('pty.openpty', openpty_mock), \ - mock.patch('os.ttyname', ttyname_mock): + mock.patch('pty.openpty', openpty_mock) if os.name != 'nt' else nullcontext(), \ + mock.patch('os.ttyname', ttyname_mock) if os.name != 'nt' else nullcontext(): result = handler._get_serial_device(serial_pty, hardware_serial) if popen_exception: @@ -1487,6 +1540,7 @@ def mock_popen(command, *args, **kwargs): (False, False, False), ] +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') @pytest.mark.parametrize( 'ignore_qemu_crash, expected_ignore_crash, expected_ignore_unexpected_eof', TESTDATA_18, @@ -1506,6 +1560,7 @@ def test_qemuhandler_init( assert handler.ignore_unexpected_eof == expected_ignore_unexpected_eof +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') def test_qemuhandler_get_cpu_time(): def mock_process(pid): return mock.Mock( @@ -1538,6 +1593,7 @@ def mock_process(pid): ), ] +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') @pytest.mark.parametrize( 'self_sysbuild, self_build_dir, build_dir, expected', TESTDATA_19, @@ -1580,6 +1636,7 @@ def test_qemuhandler_get_default_domain_build_dir( ), ] +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') @pytest.mark.parametrize( 'self_log, self_pid_fn, sysbuild_build_dir, exists_pid_fn', TESTDATA_20, @@ -1615,6 +1672,7 @@ def test_qemuhandler_set_qemu_filenames( os.path.sep + 'qemu.pid') +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') def test_qemuhandler_create_command(mocked_instance): sysbuild_build_dir = os.path.join('sysbuild', 'dummy_dir') @@ -1680,6 +1738,7 @@ def test_qemuhandler_create_command(mocked_instance): ), ] +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') @pytest.mark.parametrize( 'self_returncode, self_ignore_qemu_crash,' \ ' self_instance_reason, harness_status, is_timeout,' \ @@ -1716,6 +1775,7 @@ def test_qemuhandler_update_instance_info( ) +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') def test_qemuhandler_thread_get_fifo_names(): fifo_fn = 'dummy' @@ -1724,6 +1784,7 @@ def test_qemuhandler_thread_get_fifo_names(): assert fifo_in == 'dummy.in' assert fifo_out == 'dummy.out' + TESTDATA_22 = [ (False, False), (False, True), @@ -1731,6 +1792,7 @@ def test_qemuhandler_thread_get_fifo_names(): (True, True), ] +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') @pytest.mark.parametrize( 'fifo_in_exists, fifo_out_exists', TESTDATA_22, @@ -1778,6 +1840,7 @@ def mock_exists(path): (True, False) ] +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') @pytest.mark.parametrize( 'is_pid, is_lookup_error', TESTDATA_23, @@ -1825,6 +1888,7 @@ def mock_kill(pid, sig): (TwisterStatus.NONE, None, TwisterStatus.NONE, 'Unknown'), ] +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') @pytest.mark.parametrize( '_status, _reason, expected_status, expected_reason', TESTDATA_24, @@ -1928,6 +1992,7 @@ def test_qemuhandler_thread_update_instance_info( ), ] +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') @pytest.mark.parametrize( 'content, timeout, pid, harness_statuses, cputime, capture_coverage,' \ ' expected_status, expected_reason, expected_log_calls', @@ -2046,6 +2111,7 @@ def mocked_open(filename, *args, **kwargs): (False, True, TwisterStatus.FAIL, False, ['return code from QEMU (None): 1']), ] +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') @pytest.mark.parametrize( 'isatty, do_timeout, harness_status, exists_pid_fn, expected_logs', TESTDATA_26, @@ -2123,10 +2189,21 @@ def mock_filenames(sysbuild_build_dir): assert all([expected_log in caplog.text for expected_log in expected_logs]) -def test_qemuhandler_get_fifo(mocked_instance): +@pytest.mark.skipif(os.name == 'nt', reason='QEMUWinHandler is used on Windows, not QEMUHandler.') +def test_qemuhandler_get_fifo_windows(mocked_instance): handler = QEMUHandler(mocked_instance, 'build') handler.fifo_fn = 'fifo_fn' result = handler.get_fifo() assert result == 'fifo_fn' + + +@pytest.mark.skipif(os.name != 'nt', reason='QEMUWinHandler is used only on Windows.') +def test_qemuhandler_get_fifo_linux(mocked_instance): + handler = QEMUWinHandler(mocked_instance, 'build') + handler.fifo_fn = 'fifo_fn' + + result = handler.get_fifo() + + assert result == 'fifo_fn' diff --git a/scripts/tests/twister/test_hardwaremap.py b/scripts/tests/twister/test_hardwaremap.py index 8bb0c27ffa8273f..05acf2ef9853f13 100644 --- a/scripts/tests/twister/test_hardwaremap.py +++ b/scripts/tests/twister/test_hardwaremap.py @@ -7,6 +7,7 @@ """ import mock +import platform import pytest import sys @@ -329,16 +330,8 @@ def mock_open(*args, **kwargs): assert all([getattr(dut, k) == v for k, v in expected[dut.id].items()]) +TESTIDS_4 = ['no map (not linux)', 'no map (nonpersistent)'] TESTDATA_4 = [ - ( - True, - 'Linux', - ['', '', '', - '', '', - '', - '', - ''] - ), ( True, 'nt', @@ -359,11 +352,24 @@ def mock_open(*args, **kwargs): ) ] +if platform.system == 'Linux': + TESTDATA_4.append( + ( + True, + 'Linux', + ['', '', '', + '', '', + '', + '', + ''] + ) + ) + TESTIDS_4.append('linux persistent map') @pytest.mark.parametrize( 'persistent, system, expected_reprs', TESTDATA_4, - ids=['linux persistent map', 'no map (not linux)', 'no map (nonpersistent)'] + ids=TESTIDS_4 ) def test_hardwaremap_scan( caplog, diff --git a/scripts/tests/twister/test_harness.py b/scripts/tests/twister/test_harness.py index 5f9ee6adec1d3cd..02af83e495335ef 100644 --- a/scripts/tests/twister/test_harness.py +++ b/scripts/tests/twister/test_harness.py @@ -813,7 +813,7 @@ def test_bsim_build(monkeypatch, tmp_path): build_dir = tmp_path / 'build_dir' os.makedirs(build_dir) mocked_instance.build_dir = str(build_dir) - mocked_instance.name = 'platform_name/test/dummy.test' + mocked_instance.name = os.path.join('platform_name', 'test', 'dummy.test') mocked_instance.testsuite.harness_config = {} harness = Bsim() diff --git a/scripts/tests/twister/test_runner.py b/scripts/tests/twister/test_runner.py index cde4a26456c3281..4a404f31076b493 100644 --- a/scripts/tests/twister/test_runner.py +++ b/scripts/tests/twister/test_runner.py @@ -113,7 +113,7 @@ class MockHandler: ["extra_overlay.conf"], ["x.overlay;y.overlay", "z.overlay"], ["cmake1=foo", "cmake2=bar"], - "/builddir/", + os.path.join('', 'builddir', ''), ) == [ "-DCONFIG_t=\"test\"", "-Dcmake1=foo", "-Dcmake2=bar", @@ -121,8 +121,8 @@ class MockHandler: "-Dhandler_arg1", "-Dhandler_arg2", "-DCONF_FILE=a.conf;b.conf;c.conf", "-DDTC_OVERLAY_FILE=x.overlay;y.overlay;z.overlay", - "-DOVERLAY_CONFIG=extra_overlay.conf " - "/builddir/twister/testsuite_extra.conf", + "-DOVERLAY_CONFIG=extra_overlay.conf " + \ + os.path.join('', 'builddir', 'twister', 'testsuite_extra.conf'), ]) @@ -1870,7 +1870,7 @@ def mock_exists(fname): ( { 'CMakeCache.txt': mock.mock_open( - read_data='canonical/zephyr/base/dummy.file: ERROR' + read_data=f'{os.path.join("canonical", "zephyr", "base", "dummy.file")}: ERROR' ) }, { @@ -1880,7 +1880,7 @@ def mock_exists(fname): ( { os.path.join('zephyr', 'runners.yaml'): mock.mock_open( - read_data='There was canonical/zephyr/base/dummy.file here' + read_data=f'There was {os.path.join("canonical", "zephyr", "base", "dummy.file")} here' ) }, { @@ -1899,7 +1899,7 @@ def test_projectbuilder_sanitize_zephyr_base_from_files( text_mocks, expected_write_texts ): - build_dir_path = 'canonical/zephyr/base/build_dir/' + build_dir_path = os.path.join('canonical', 'zephyr', 'base', 'build_dir', '') def mock_exists(fname): if not fname.startswith(build_dir_path): @@ -1920,7 +1920,7 @@ def mock_open(fname, *args, **kwargs): with mock.patch('os.path.exists', mock_exists), \ mock.patch('builtins.open', mock_open), \ mock.patch('twisterlib.runner.canonical_zephyr_base', - 'canonical/zephyr/base'): + os.path.join('canonical', 'zephyr', 'base', '')): pb._sanitize_zephyr_base_from_files() for fname, fhandler in text_mocks.items(): @@ -2373,12 +2373,13 @@ def test_projectbuilder_calc_size( ('linux', '???', {'jobs': 4}, False, 4, 'JobClient'), ] +@pytest.mark.skipif(sys.platform != 'linux', reason='JobClient family only works on Linux.') @pytest.mark.parametrize( 'platform, os_name, options, jobclient_from_environ, expected_jobs,' \ ' expected_jobserver', TESTDATA_17, ids=['GNUMakeJobClient', 'GNUMakeJobServer', - 'JobClient', 'Jobclient+options'] + 'JobClient', 'JobClient+options'] ) def test_twisterrunner_run( caplog, @@ -2614,9 +2615,6 @@ def mock_get_cmake_filter_stages(filter, keys): if retry_build_errors: tr.get_cmake_filter_stages.assert_any_call('some', mock.ANY) - print(pipeline_mock.put.call_args_list) - print([mock.call(el) for el in expected_pipeline_elements]) - assert pipeline_mock.put.call_args_list == \ [mock.call(el) for el in expected_pipeline_elements] diff --git a/scripts/tests/twister/test_testinstance.py b/scripts/tests/twister/test_testinstance.py index d6cc2d95ec8b225..7ad713bc8cc3dad 100644 --- a/scripts/tests/twister/test_testinstance.py +++ b/scripts/tests/twister/test_testinstance.py @@ -20,7 +20,7 @@ from twisterlib.testinstance import TestInstance from twisterlib.error import BuildError from twisterlib.runner import TwisterRunner -from twisterlib.handlers import QEMUHandler +from twisterlib.handlers import QEMUHandler, QEMUWinHandler from expr_parser import reserved @@ -34,8 +34,11 @@ (False, True, "sensor", "native", "", True, [], (True, False)), ] @pytest.mark.parametrize( - "build_only, slow, harness, platform_type, platform_sim, device_testing,fixture, expected", - TESTDATA_PART_1 + "build_only, slow, harness, platform_type, platform_sim, device_testing, fixture, expected", + TESTDATA_PART_1, + ids=['console, na, qemu', 'console, native, qemu', 'console, native, nsim, build only', + 'console, native, renode, build_only, slow', 'sensor, native', + 'sensor, na', 'sensor, native, slow, device_testing'] ) def test_check_build_or_run( class_testplan, @@ -55,9 +58,9 @@ def test_check_build_or_run( Scenario 2: Test if build_only is enabled when the OS is Windows""" class_testplan.testsuites = all_testsuites_dict - testsuite = class_testplan.testsuites.get('scripts/tests/twister/test_data/testsuites/tests/' - 'test_a/test_a.check_1') - print(testsuite) + testsuite_path = 'scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_1' + testsuite_path = testsuite_path.replace('/', os.sep) + testsuite = class_testplan.testsuites.get(testsuite_path) class_testplan.platforms = platforms_list platform = class_testplan.get_platform("demo_board_2") @@ -68,9 +71,11 @@ def test_check_build_or_run( testsuite.slow = slow testinstance = TestInstance(testsuite, platform, class_testplan.env.outdir) - run = testinstance.check_runnable(slow, device_testing, fixture) - _, r = expected - assert run == r + + with mock.patch('os.name', 'posix'): + run = testinstance.check_runnable(slow, device_testing, fixture) + _, r = expected + assert run == r with mock.patch('os.name', 'nt'): # path to QEMU binary is not in QEMU_BIN_PATH environment variable @@ -128,8 +133,9 @@ def test_create_overlay( ): """Test correct content is written to testcase_extra.conf based on if conditions.""" class_testplan.testsuites = all_testsuites_dict - testcase = class_testplan.testsuites.get('scripts/tests/twister/test_data/testsuites/samples/' - 'test_app/sample_test.app') + testcase_path = 'scripts/tests/twister/test_data/testsuites/samples/test_app/sample_test.app' + testcase_path = testcase_path.replace('/', os.sep) + testcase = class_testplan.testsuites.get(testcase_path) if extra_configs: testcase.extra_configs = extra_configs @@ -144,8 +150,9 @@ def test_create_overlay( def test_calculate_sizes(class_testplan, all_testsuites_dict, platforms_list): """ Test Calculate sizes method for zephyr elf""" class_testplan.testsuites = all_testsuites_dict - testcase = class_testplan.testsuites.get('scripts/tests/twister/test_data/testsuites/samples/' - 'test_app/sample_test.app') + testcase_path = 'scripts/tests/twister/test_data/testsuites/samples/test_app/sample_test.app' + testcase_path = testcase_path.replace('/', os.sep) + testcase = class_testplan.testsuites.get(testcase_path) class_testplan.platforms = platforms_list platform = class_testplan.get_platform("demo_board_2") testinstance = TestInstance(testcase, platform, class_testplan.env.outdir) @@ -194,6 +201,7 @@ def sample_testinstance(all_testsuites_dict, class_testplan, platforms_list, req testsuite_path += '/samples/test_app/sample_test.app' elif request.param['testsuite_kind'] == 'tests': testsuite_path += '/tests/test_a/test_a.check_1' + testsuite_path = testsuite_path.replace('/', os.sep) class_testplan.testsuites = all_testsuites_dict testsuite = class_testplan.testsuites.get(testsuite_path) @@ -211,20 +219,23 @@ def sample_testinstance(all_testsuites_dict, class_testplan, platforms_list, req @pytest.mark.parametrize('detailed_test_id', TESTDATA_1) def test_testinstance_init(all_testsuites_dict, class_testplan, platforms_list, detailed_test_id): - testsuite_path = 'scripts/tests/twister/test_data/testsuites/samples/test_app/sample_test.app' + testsuite_rel_path = 'scripts/tests/twister/test_data/testsuites/samples/test_app/sample_test.app' + testsuite_path = testsuite_rel_path.replace('/', os.sep) class_testplan.testsuites = all_testsuites_dict testsuite = class_testplan.testsuites.get(testsuite_path) testsuite.detailed_test_id = detailed_test_id class_testplan.platforms = platforms_list - print(class_testplan.platforms) + platform = class_testplan.get_platform("demo_board_2") testinstance = TestInstance(testsuite, platform, class_testplan.env.outdir) if detailed_test_id: - assert testinstance.build_dir == os.path.join(class_testplan.env.outdir, platform.name, testsuite_path) + expected_path = os.path.join(class_testplan.env.outdir, platform.name, testsuite_rel_path) else: - assert testinstance.build_dir == os.path.join(class_testplan.env.outdir, platform.name, testsuite.source_dir_rel, testsuite.name) + expected_path = os.path.join(class_testplan.env.outdir, platform.name, testsuite.source_dir_rel, testsuite.name) + expected_path = os.path.abspath(os.fspath(expected_path)) + assert testinstance.build_dir == expected_path @pytest.mark.parametrize('testinstance', [{'testsuite_kind': 'sample'}], indirect=True) @@ -247,8 +258,6 @@ def test_testinstance_record(testinstance): ) as mock_writerows: testinstance.record(recording) - print(mock_file.mock_calls) - mock_file.assert_called_with( os.path.join(testinstance.build_dir, 'recording.csv'), 'wt' @@ -273,6 +282,7 @@ def test_testinstance_add_filter(testinstance): def test_testinstance_init_cases(all_testsuites_dict, class_testplan, platforms_list): testsuite_path = 'scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_1' + testsuite_path = testsuite_path.replace('/', os.sep) class_testplan.testsuites = all_testsuites_dict testsuite = class_testplan.testsuites.get(testsuite_path) class_testplan.platforms = platforms_list @@ -327,6 +337,8 @@ def test_testinstance_add_missing_case_status(testinstance, reason, expected_rea def test_testinstance_dunders(all_testsuites_dict, class_testplan, platforms_list): testsuite_path = 'scripts/tests/twister/test_data/testsuites/samples/test_app/sample_test.app' + testsuite_name = os.path.normpath(testsuite_path) + testsuite_path = testsuite_path.replace('/', os.sep) class_testplan.testsuites = all_testsuites_dict testsuite = class_testplan.testsuites.get(testsuite_path) class_testplan.platforms = platforms_list @@ -350,7 +362,7 @@ def test_testinstance_dunders(all_testsuites_dict, class_testplan, platforms_lis assert not testinstance < testinstance_copy assert not testinstance_copy < testinstance - assert testinstance.__repr__() == f'' + assert testinstance.__repr__() == f'' @pytest.mark.parametrize('testinstance', [{'testsuite_kind': 'tests'}], indirect=True) @@ -431,6 +443,7 @@ def test_testinstance_testsuite_runnable( expected_can_run ): testsuite_path = 'scripts/tests/twister/test_data/testsuites/samples/test_app/sample_test.app' + testsuite_path = testsuite_path.replace('/', os.sep) class_testplan.testsuites = all_testsuites_dict testsuite = class_testplan.testsuites.get(testsuite_path) @@ -458,7 +471,7 @@ def test_testinstance_testsuite_runnable( ' expected_handler_type, expected_handler_args, expected_handler_ready', TESTDATA_4, ids=['preexisting handler', 'device testing', 'qemu simulation', - 'non-qemu simulation with exec', 'unit teting', 'no handler'] + 'non-qemu simulation with exec', 'unit testing', 'no handler'] ) @pytest.mark.parametrize('testinstance', [{'testsuite_kind': 'tests'}], indirect=True) def test_testinstance_setup_handler( @@ -483,12 +496,14 @@ def test_testinstance_setup_handler( ) with mock.patch.object(QEMUHandler, 'get_fifo', return_value=1), \ + mock.patch.object(QEMUWinHandler, 'get_fifo', return_value=1), \ mock.patch('shutil.which', return_value=True): testinstance.setup_handler(env) if expected_handler_type: assert testinstance.handler.type_str == expected_handler_type assert testinstance.handler.ready == expected_handler_ready + assert all([arg in testinstance.handler.args for arg in expected_handler_args]) diff --git a/scripts/tests/twister/test_testplan.py b/scripts/tests/twister/test_testplan.py index f2053c0e519d7ec..be6c258439002ce 100644 --- a/scripts/tests/twister/test_testplan.py +++ b/scripts/tests/twister/test_testplan.py @@ -49,8 +49,10 @@ def test_testplan_add_testsuites_short(class_testplan): assert sorted(testsuite_list) == sorted(expected_testsuites) # Test 2 : Assert Testcase name is expected & all testsuites values are testcase class objects - suite = class_testplan.testsuites.get(tests_rel_dir + 'test_a/test_a.check_1') - assert suite.name == tests_rel_dir + 'test_a/test_a.check_1' + testsuite_rel_path = tests_rel_dir + 'test_a/test_a.check_1' + testsuite_rel_path = testsuite_rel_path.replace('/', os.sep) + suite = class_testplan.testsuites.get(testsuite_rel_path) + assert suite.name == testsuite_rel_path assert all(isinstance(n, TestSuite) for n in class_testplan.testsuites.values()) @pytest.mark.parametrize("board_root_dir", [("board_config_file_not_exist"), ("board_config")]) @@ -266,34 +268,34 @@ def test_add_instances_short(tmp_path, class_env, all_testsuites_dict, platforms instance_list.append(instance) plan.add_instances(instance_list) assert list(plan.instances.keys()) == \ - [platform.name + '/' + s for s in list(all_testsuites_dict.keys())] + [os.path.join(platform.name, s) for s in list(all_testsuites_dict.keys())] assert all(isinstance(n, TestInstance) for n in list(plan.instances.values())) assert list(plan.instances.values()) == instance_list QUARANTINE_BASIC = { - 'demo_board_1/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_1' : 'a1 on board_1 and board_3', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_1' : 'a1 on board_1 and board_3' + 'demo_board_1/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_1'.replace('/', os.sep) : 'a1 on board_1 and board_3', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_1'.replace('/', os.sep) : 'a1 on board_1 and board_3' } QUARANTINE_WITH_REGEXP = { - 'demo_board_2/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_2' : 'a2 and c2 on x86', - 'demo_board_1/scripts/tests/twister/test_data/testsuites/tests/test_d/test_d.check_1' : 'all test_d', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_d/test_d.check_1' : 'all test_d', - 'demo_board_2/scripts/tests/twister/test_data/testsuites/tests/test_d/test_d.check_1' : 'all test_d', - 'demo_board_2/scripts/tests/twister/test_data/testsuites/tests/test_c/test_c.check_2' : 'a2 and c2 on x86' + 'demo_board_2/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_2'.replace('/', os.sep) : 'a2 and c2 on x86', + 'demo_board_1/scripts/tests/twister/test_data/testsuites/tests/test_d/test_d.check_1'.replace('/', os.sep) : 'all test_d', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_d/test_d.check_1'.replace('/', os.sep) : 'all test_d', + 'demo_board_2/scripts/tests/twister/test_data/testsuites/tests/test_d/test_d.check_1'.replace('/', os.sep) : 'all test_d', + 'demo_board_2/scripts/tests/twister/test_data/testsuites/tests/test_c/test_c.check_2'.replace('/', os.sep) : 'a2 and c2 on x86' } QUARANTINE_PLATFORM = { - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_1' : 'all on board_3', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_2' : 'all on board_3', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_d/test_d.check_1' : 'all on board_3', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_b/test_b.check_1' : 'all on board_3', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_b/test_b.check_2' : 'all on board_3', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_c/test_c.check_1' : 'all on board_3', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_c/test_c.check_2' : 'all on board_3', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_e/test_e.check_1' : 'all on board_3', - 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_config/test_config.main' : 'all on board_3' + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_1'.replace('/', os.sep) : 'all on board_3', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_a/test_a.check_2'.replace('/', os.sep) : 'all on board_3', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_d/test_d.check_1'.replace('/', os.sep) : 'all on board_3', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_b/test_b.check_1'.replace('/', os.sep) : 'all on board_3', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_b/test_b.check_2'.replace('/', os.sep) : 'all on board_3', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_c/test_c.check_1'.replace('/', os.sep) : 'all on board_3', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_c/test_c.check_2'.replace('/', os.sep) : 'all on board_3', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_e/test_e.check_1'.replace('/', os.sep) : 'all on board_3', + 'demo_board_3/scripts/tests/twister/test_data/testsuites/tests/test_config/test_config.main'.replace('/', os.sep) : 'all on board_3' } QUARANTINE_MULTIFILES = { @@ -352,12 +354,9 @@ def test_quarantine_short(class_testplan, platforms_list, test_data, TESTDATA_PART4 = [ - (os.path.join('test_d', 'test_d.check_1'), ['dummy'], - None, 'Snippet not supported'), - (os.path.join('test_c', 'test_c.check_1'), ['cdc-acm-console'], - 0, None), - (os.path.join('test_d', 'test_d.check_1'), ['dummy', 'cdc-acm-console'], - 2, 'Snippet not supported'), + ('test_d/test_d.check_1', ['dummy'], None, 'Snippet not supported'), + ('test_c/test_c.check_1', ['cdc-acm-console'], 0, None), + ('test_d/test_d.check_1', ['dummy', 'cdc-acm-console'], 2, 'Snippet not supported'), ] @pytest.mark.parametrize( @@ -376,15 +375,13 @@ def test_required_snippets_short( ): """ Testing required_snippets function of TestPlan class in Twister """ plan = class_testplan - testpath = os.path.join('scripts', 'tests', 'twister', 'test_data', - 'testsuites', 'tests', testpath) - testsuite = class_testplan.testsuites.get(testpath) + testsuite_rel_path = f'scripts/tests/twister/test_data/testsuites/tests/{testpath}' + testsuite_rel_path = testsuite_rel_path.replace('/', os.sep) + testsuite = class_testplan.testsuites.get(testsuite_rel_path) plan.platforms = platforms_list plan.platform_names = [p.name for p in platforms_list] plan.testsuites = {testpath: testsuite} - print(plan.testsuites) - for _, testcase in plan.testsuites.items(): testcase.exclude_platform = [] testcase.required_snippets = required_snippets @@ -1125,7 +1122,7 @@ def test_testplan_add_configurations( - name: unit_testing """ p1e1_yamlfile = tmp_p1_dir / 'board.yml' - p1e1_yamlfile.write_text(p1e1_bs_yaml) + p1e1_yamlfile.write_text(p1e1_bs_yaml, encoding='utf-8') p1e1_yaml = """\ identifier: p1e1 @@ -1138,7 +1135,7 @@ def test_testplan_add_configurations( twister: False """ p1e1_yamlfile = tmp_p1_dir / 'p1e1.yaml' - p1e1_yamlfile.write_text(p1e1_yaml) + p1e1_yamlfile.write_text(p1e1_yaml, encoding='utf-8') p1e2_yaml = """\ identifier: p1e2 @@ -1150,7 +1147,7 @@ def test_testplan_add_configurations( - zephyr """ p1e2_yamlfile = tmp_p1_dir / 'p1e2.yaml' - p1e2_yamlfile.write_text(p1e2_yaml) + p1e2_yamlfile.write_text(p1e2_yaml, encoding='utf-8') tmp_p2_dir = tmp_arch1_dir / 'p2' tmp_p2_dir.mkdir() @@ -1168,7 +1165,7 @@ def test_testplan_add_configurations( - name: unit_testing """ p2_yamlfile = tmp_p2_dir / 'board.yml' - p2_yamlfile.write_text(p2_bs_yaml) + p2_yamlfile.write_text(p2_bs_yaml, encoding='utf-8') p2_yaml = """\ identifier: p2 @@ -1182,11 +1179,11 @@ def test_testplan_add_configurations( default: True """ p2_yamlfile = tmp_p2_dir / 'p2.yaml' - p2_yamlfile.write_text(p2_yaml) + p2_yamlfile.write_text(p2_yaml, encoding='utf-8') if create_duplicate: p2_yamlfile = tmp_p2_dir / 'p2-1.yaml' - p2_yamlfile.write_text(p2_yaml) + p2_yamlfile.write_text(p2_yaml, encoding='utf-8') p2_2_yaml = """\ testing: @@ -1200,7 +1197,7 @@ def test_testplan_add_configurations( - zephyr """ p2_2_yamlfile = tmp_p2_dir / 'p2-2.yaml' - p2_2_yamlfile.write_text(p2_2_yaml) + p2_2_yamlfile.write_text(p2_2_yaml, encoding='utf-8') tmp_arch2_dir = tmp_board_root_dir / 'arm' tmp_arch2_dir.mkdir() @@ -1217,7 +1214,7 @@ def test_testplan_add_configurations( - name: unit_testing """ p3_yamlfile = tmp_p3_dir / 'board.yml' - p3_yamlfile.write_text(p3_bs_yaml) + p3_yamlfile.write_text(p3_bs_yaml, encoding='utf-8') p3_yaml = """\ identifier: p3 @@ -1229,9 +1226,9 @@ def test_testplan_add_configurations( - zephyr """ p3_yamlfile = tmp_p3_dir / 'p3.yaml' - p3_yamlfile.write_text(p3_yaml) + p3_yamlfile.write_text(p3_yaml, encoding='utf-8') p3_yamlfile = tmp_p3_dir / 'p3_B.conf' - p3_yamlfile.write_text('') + p3_yamlfile.write_text('', encoding='utf-8') env = mock.Mock(board_roots=[tmp_board_root_dir]) @@ -1291,6 +1288,7 @@ def test_testplan_get_all_tests(): 'testsuite filter', 'testsuite filter, alt root'] ) def test_testplan_add_testsuites(tmp_path, testsuite_filter, use_alt_root, expected_suite_count): + testsuite_filter = [f.replace('/', os.sep) for f in testsuite_filter] # tmp_path # ├ tests <- test root # │ ├ good_test @@ -1557,7 +1555,7 @@ def get_platform(name): testplan.load_from_file('dummy.yaml', filter_platform) expected_instances = { - 'Platform 1/TestSuite 1': { + str(os.path.join('Platform 1', 'TestSuite 1')): { 'metrics': { 'handler_time': 60.0, 'used_ram': 4096, @@ -1575,7 +1573,7 @@ def get_platform(name): } } }, - 'Platform 1/TestSuite 2': { + str(os.path.join('Platform 1', 'TestSuite 2')): { 'metrics': { 'handler_time': 0, 'used_ram': 0, @@ -1586,7 +1584,7 @@ def get_platform(name): 'retries': 0, 'testcases': [] }, - 'Platform 1/TestSuite 3': { + str(os.path.join('Platform 1', 'TestSuite 3')): { 'metrics': { 'handler_time': 360.0, 'used_ram': 4096, @@ -1610,7 +1608,7 @@ def get_platform(name): } } }, - 'Platform 1/TestSuite 4': { + str(os.path.join('Platform 1', 'TestSuite 4')): { 'metrics': { 'handler_time': 360.0, 'used_ram': 4096, diff --git a/scripts/tests/twister/test_testsuite.py b/scripts/tests/twister/test_testsuite.py index e297b6b6d9cd6c8..d0e2278cc0a438c 100644 --- a/scripts/tests/twister/test_testsuite.py +++ b/scripts/tests/twister/test_testsuite.py @@ -326,13 +326,7 @@ def test_get_unique_exception(testsuite_root, workdir, name, exception): 'test_a', 'test_a.check_1' ), - os.path.join( - os.sep, - TEST_DATA_REL_PATH, - 'tests', - 'test_a', - 'test_a.check_1' - ), + os.path.join(os.sep, TEST_DATA_REL_PATH, 'tests', 'test_a', 'test_a.check_1') ), ( ZEPHYR_BASE, @@ -367,7 +361,14 @@ def test_get_unique_exception(testsuite_root, workdir, name, exception): @pytest.mark.parametrize( 'testsuite_root, suite_path, name, expected', - TESTDATA_5 + TESTDATA_5, + ids=[ + 'test base, single test folder, case absolute path', + 'zephyr base, zephyr base, case', + 'zephyr base, single test folder, case absolute path', + 'tests folder, tests folder, case relative path', + 'zephyr base, zephyr base, subcase' + ] ) def test_get_unique(testsuite_root, suite_path, name, expected): """ @@ -443,8 +444,9 @@ def test_get_search_area_boundary( TESTDATA_7 = [ - (True, [os.path.join('', 'home', 'user', 'dummy_path', 'dummy.c'), - os.path.join('', 'home', 'user', 'dummy_path', 'dummy.cpp')]), + (True, [os.path.join(os.sep, 'home', 'user', 'dummy_path', 'dummy.c'), + os.path.join(os.sep, 'home', 'user', 'dummy_path', 'dummy.cpp')] + ), (False, []) ] @@ -454,8 +456,11 @@ def test_get_search_area_boundary( ids=['valid', 'not a directory'] ) def test_find_c_files_in(isdir, expected): - old_dir = os.path.join('', 'home', 'user', 'dummy_base_dir') - new_path = os.path.join('', 'home', 'user', 'dummy_path') + if os.name == 'nt': + expected = ['\\\\?\\C:' + p for p in expected] + + old_dir = os.path.join(os.sep, 'home', 'user', 'dummy_base_dir') + new_path = os.path.join(os.sep, 'home', 'user', 'dummy_path') cur_dir = old_dir def mock_chdir(path, *args, **kwargs): @@ -516,9 +521,9 @@ def mock_glob(fmt, *args, **kwargs): mock.patch('os.getcwd', return_value=cur_dir), \ mock.patch('glob.glob', mock_glob), \ mock.patch('os.chdir', side_effect=mock_chdir) as chdir_mock: - filenames = find_c_files_in(new_path) + tfilenames = find_c_files_in(new_path) - assert sorted(filenames) == sorted(expected) + assert sorted([str(f) for f in tfilenames]) == sorted(expected) assert chdir_mock.call_args is None or \ chdir_mock.call_args == mock.call(old_dir) @@ -680,9 +685,13 @@ def mock_stat(filename, *args, **kwargs): TESTDATA_9 = [ - ('dummy/path', 'dummy/path/src', 'dummy/path/src'), - ('dummy/path', 'dummy/src', 'dummy/src'), - ('dummy/path', 'another/path', '') + ( + os.path.join('dummy', 'path'), + os.path.join('dummy', 'path', 'src'), + os.path.join('dummy', 'path', 'src') + ), + (os.path.join('dummy', 'path'), os.path.join('dummy', 'src'), os.path.join('dummy', 'src')), + (os.path.join('dummy', 'path'), os.path.join('another', 'path'), '') ] @@ -900,5 +909,4 @@ def test_testcase_dunders(): def test_get_no_detailed_test_id(testsuite_root, suite_path, name, expected): '''Test to check if the name without path is given for each testsuite''' suite = TestSuite(testsuite_root, suite_path, name, detailed_test_id=False) - print(suite.name) assert suite.name == expected