Skip to content

Commit 9c02226

Browse files
committed
scripts: twister: Fix Unit Tests on Windows
Unit tests were failing on Windows, indicating that current Twister code is not truly multiplatform. Linux-specific code parts were changed into multiplatform ones. Paths were deemed the main culprit, so this commit introduces a new TPath, that aims to fix the Windows limitation of supporting only up to 260 char paths by default. Signed-off-by: Lukasz Mrugala <[email protected]>
1 parent f1087d2 commit 9c02226

23 files changed

+530
-272
lines changed

scripts/pylib/pytest-twister-harness/tests/device/hardware_adapter_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ def test_device_log_correct_error_handle(patched_popen, device: HardwareAdapter,
205205
assert 'flashing error' in file.readlines()
206206

207207

208+
@pytest.mark.skipif(os.name == 'nt', reason='PTY is not used on Windows.')
208209
@mock.patch('twister_harness.device.hardware_adapter.subprocess.Popen')
209210
@mock.patch('twister_harness.device.hardware_adapter.serial.Serial')
210211
def test_if_hardware_adapter_uses_serial_pty(
@@ -238,6 +239,7 @@ def test_if_hardware_adapter_uses_serial_pty(
238239
assert not device._serial_pty_proc
239240

240241

242+
@pytest.mark.skipif(os.name == 'nt', reason='PTY is not used on Windows.')
241243
def test_if_hardware_adapter_properly_send_data_to_subprocess(
242244
device: HardwareAdapter, shell_simulator_path: str
243245
) -> None:

scripts/pylib/pytest-twister-harness/tests/device/qemu_adapter_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def test_if_generate_command_creates_proper_command(patched_which, device: QemuA
3232
assert device.command == ['west', 'build', '-d', 'build_dir', '-t', 'run']
3333

3434

35+
@pytest.mark.skipif(os.name == 'nt', reason='QEMU FIFO fails on Windows.')
3536
def test_if_qemu_adapter_runs_without_errors(resources, device: QemuAdapter) -> None:
3637
fifo_file_path = str(device.device_config.build_dir / 'qemu-fifo')
3738
script_path = resources.joinpath('fifo_mock.py')
@@ -46,6 +47,7 @@ def test_if_qemu_adapter_runs_without_errors(resources, device: QemuAdapter) ->
4647
assert file_lines[-2:] == lines[-2:]
4748

4849

50+
@pytest.mark.skipif(os.name == 'nt', reason='QEMU FIFO fails on Windows.')
4951
def test_if_qemu_adapter_raise_exception_due_to_no_fifo_connection(device: QemuAdapter) -> None:
5052
device.base_timeout = 0.3
5153
device.command = ['sleep', '1']

scripts/pylib/twister/twisterlib/config_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def __init__(self, filename, schema):
9696
self.common = {}
9797

9898
def load(self):
99-
data = scl.yaml_load_verify(self.filename, self.schema)
99+
data = scl.yaml_load_verify(str(self.filename), self.schema)
100100
self.data = data
101101

102102
if 'tests' in self.data:

scripts/pylib/twister/twisterlib/coverage.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def run_command(self, cmd, coveragelog):
236236
"--ignore-errors", "mismatch,mismatch",
237237
]
238238

239+
cmd = [str(c) for c in cmd]
239240
cmd_str = " ".join(cmd)
240241
logger.debug(f"Running {cmd_str}...")
241242
return subprocess.call(cmd, stdout=coveragelog)
@@ -363,6 +364,7 @@ def _generate(self, outdir, coveragelog):
363364
"--gcov-executable", self.gcov_tool,
364365
"-e", "tests/*"]
365366
cmd += excludes + mode_options + ["--json", "-o", coveragefile, outdir]
367+
cmd = [str(c) for c in cmd]
366368
cmd_str = " ".join(cmd)
367369
logger.debug(f"Running {cmd_str}...")
368370
subprocess.call(cmd, stdout=coveragelog)

scripts/pylib/twister/twisterlib/environment.py

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
from collections.abc import Generator
1919
from datetime import datetime, timezone
2020
from importlib import metadata
21-
from pathlib import Path
2221

2322
import zephyr_module
2423
from twisterlib.constants import SUPPORTED_SIMS
2524
from twisterlib.coverage import supported_coverage_formats
2625
from twisterlib.error import TwisterRuntimeError
2726
from twisterlib.log_helper import log_command
27+
from twisterlib.twister_path import TPath
2828

2929
logger = logging.getLogger('twister')
3030
logger.setLevel(logging.DEBUG)
@@ -133,7 +133,7 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
133133
)
134134

135135
case_select.add_argument(
136-
"-T", "--testsuite-root", action="append", default=[], type = norm_path,
136+
"-T", "--testsuite-root", action="append", default=[], type = TPath,
137137
help="Base directory to recursively search for test cases. All "
138138
"testcase.yaml files under here will be processed. May be "
139139
"called multiple times. Defaults to the 'samples/' and "
@@ -248,7 +248,7 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
248248
and global timeout multiplier (this parameter)""")
249249

250250
test_xor_subtest.add_argument(
251-
"-s", "--test", "--scenario", action="append", type = norm_path,
251+
"-s", "--test", "--scenario", action="append", type = TPath,
252252
help="""Run only the specified test suite scenario. These are named by
253253
'path/relative/to/Zephyr/base/section.subsection_in_testcase_yaml',
254254
or just 'section.subsection' identifier. With '--testsuite-root' option
@@ -293,16 +293,19 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
293293

294294
# Start of individual args place them in alpha-beta order
295295

296-
board_root_list = [f"{ZEPHYR_BASE}/boards", f"{ZEPHYR_BASE}/subsys/testsuite/boards"]
296+
board_root_list = [
297+
TPath(f"{ZEPHYR_BASE}/boards"),
298+
TPath(f"{ZEPHYR_BASE}/subsys/testsuite/boards")
299+
]
297300

298301
modules = zephyr_module.parse_modules(ZEPHYR_BASE)
299302
for module in modules:
300303
board_root = module.meta.get("build", {}).get("settings", {}).get("board_root")
301304
if board_root:
302-
board_root_list.append(os.path.join(module.project, board_root, "boards"))
305+
board_root_list.append(TPath(os.path.join(module.project, board_root, "boards")))
303306

304307
parser.add_argument(
305-
"-A", "--board-root", action="append", default=board_root_list,
308+
"-A", "--board-root", action="append", default=board_root_list, type=TPath,
306309
help="""Directory to search for board configuration files. All .yaml
307310
files in the directory will be processed. The directory should have the same
308311
structure in the main Zephyr tree: boards/<vendor>/<board_name>/""")
@@ -349,7 +352,7 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
349352
"--cmake-only", action="store_true",
350353
help="Only run cmake, do not build or run.")
351354

352-
parser.add_argument("--coverage-basedir", default=ZEPHYR_BASE,
355+
parser.add_argument("--coverage-basedir", default=ZEPHYR_BASE, type=TPath,
353356
help="Base source directory for coverage report.")
354357

355358
parser.add_argument("--coverage-platform", action="append", default=[],
@@ -374,7 +377,8 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
374377
parser.add_argument(
375378
"--test-config",
376379
action="store",
377-
default=os.path.join(ZEPHYR_BASE, "tests", "test_config.yaml"),
380+
type=TPath,
381+
default=TPath(os.path.join(ZEPHYR_BASE, "tests", "test_config.yaml")),
378382
help="Path to file with plans and test configurations."
379383
)
380384

@@ -431,7 +435,7 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
431435
help="Do not filter based on toolchain, use the set "
432436
" toolchain unconditionally")
433437

434-
parser.add_argument("--gcov-tool", type=Path, default=None,
438+
parser.add_argument("--gcov-tool", type=TPath, default=None,
435439
help="Path to the gcov tool to use for code coverage "
436440
"reports")
437441

@@ -513,6 +517,7 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
513517
"-z", "--size",
514518
action="append",
515519
metavar='FILENAME',
520+
type=TPath,
516521
help="Ignore all other command line options and just produce a report to "
517522
"stdout with ROM/RAM section sizes on the specified binary images.")
518523

@@ -543,7 +548,7 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
543548
test_plan_report_xor.add_argument("--list-tags", action="store_true",
544549
help="List all tags occurring in selected tests.")
545550

546-
parser.add_argument("--log-file", metavar="FILENAME", action="store",
551+
parser.add_argument("--log-file", metavar="FILENAME", action="store", type=TPath,
547552
help="Specify a file where to save logs.")
548553

549554
parser.add_argument(
@@ -604,15 +609,15 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
604609
)
605610

606611
parser.add_argument(
607-
"-O", "--outdir",
608-
default=os.path.join(os.getcwd(), "twister-out"),
612+
"-O", "--outdir", type=TPath,
613+
default=TPath(os.path.join(os.getcwd(), "twister-out")),
609614
help="Output directory for logs and binaries. "
610615
"Default is 'twister-out' in the current directory. "
611616
"This directory will be cleaned unless '--no-clean' is set. "
612617
"The '--clobber-output' option controls what cleaning does.")
613618

614619
parser.add_argument(
615-
"-o", "--report-dir",
620+
"-o", "--report-dir", type=TPath,
616621
help="""Output reports containing results of the test run into the
617622
specified directory.
618623
The output will be both in JSON and JUNIT format
@@ -663,6 +668,7 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
663668
"--quarantine-list",
664669
action="append",
665670
metavar="FILENAME",
671+
type=TPath,
666672
help="Load list of test scenarios under quarantine. The entries in "
667673
"the file need to correspond to the test scenarios names as in "
668674
"corresponding tests .yaml files. These scenarios "
@@ -831,7 +837,7 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
831837
parser.add_argument("extra_test_args", nargs=argparse.REMAINDER,
832838
help="Additional args following a '--' are passed to the test binary")
833839

834-
parser.add_argument("--alt-config-root", action="append", default=[],
840+
parser.add_argument("--alt-config-root", action="append", default=[], type=TPath,
835841
help="Alternative test configuration root/s. When a test is found, "
836842
"Twister will check if a test configuration file exist in any of "
837843
"the alternative test configuration root folders. For example, "
@@ -878,8 +884,8 @@ def parse_arguments(
878884

879885
# check again and make sure we have something set
880886
if not options.testsuite_root:
881-
options.testsuite_root = [os.path.join(ZEPHYR_BASE, "tests"),
882-
os.path.join(ZEPHYR_BASE, "samples")]
887+
options.testsuite_root = [TPath(os.path.join(ZEPHYR_BASE, "tests")),
888+
TPath(os.path.join(ZEPHYR_BASE, "samples"))]
883889

884890
if options.last_metrics or options.compare_report:
885891
options.enable_size_report = True
@@ -1019,34 +1025,38 @@ def __init__(self, options : argparse.Namespace, default_options=None) -> None:
10191025

10201026
self.test_roots = options.testsuite_root
10211027

1022-
if not isinstance(options.board_root, list):
1023-
self.board_roots = [options.board_root]
1028+
if options:
1029+
if not isinstance(options.board_root, list):
1030+
self.board_roots = [self.options.board_root]
1031+
else:
1032+
self.board_roots = self.options.board_root
1033+
self.outdir = TPath(os.path.abspath(options.outdir))
10241034
else:
10251035
self.board_roots = options.board_root
10261036
self.outdir = os.path.abspath(options.outdir)
10271037

1028-
self.snippet_roots = [Path(ZEPHYR_BASE)]
1038+
self.snippet_roots = [TPath(ZEPHYR_BASE)]
10291039
modules = zephyr_module.parse_modules(ZEPHYR_BASE)
10301040
for module in modules:
10311041
snippet_root = module.meta.get("build", {}).get("settings", {}).get("snippet_root")
10321042
if snippet_root:
1033-
self.snippet_roots.append(Path(module.project) / snippet_root)
1043+
self.snippet_roots.append(TPath(module.project) / snippet_root)
10341044

10351045

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)]
1046+
self.soc_roots = [TPath(ZEPHYR_BASE), TPath(ZEPHYR_BASE) / 'subsys' / 'testsuite']
1047+
self.dts_roots = [TPath(ZEPHYR_BASE)]
1048+
self.arch_roots = [TPath(ZEPHYR_BASE)]
10391049

10401050
for module in modules:
10411051
soc_root = module.meta.get("build", {}).get("settings", {}).get("soc_root")
10421052
if soc_root:
1043-
self.soc_roots.append(Path(module.project) / Path(soc_root))
1053+
self.soc_roots.append(TPath(module.project) / TPath(soc_root))
10441054
dts_root = module.meta.get("build", {}).get("settings", {}).get("dts_root")
10451055
if dts_root:
1046-
self.dts_roots.append(Path(module.project) / Path(dts_root))
1056+
self.dts_roots.append(TPath(module.project) / TPath(dts_root))
10471057
arch_root = module.meta.get("build", {}).get("settings", {}).get("arch_root")
10481058
if arch_root:
1049-
self.arch_roots.append(Path(module.project) / Path(arch_root))
1059+
self.arch_roots.append(TPath(module.project) / TPath(arch_root))
10501060

10511061
self.hwm = None
10521062

@@ -1143,7 +1153,7 @@ def run_cmake_script(args=None):
11431153
return results
11441154

11451155
def get_toolchain(self):
1146-
toolchain_script = Path(ZEPHYR_BASE) / Path('cmake/verify-toolchain.cmake')
1156+
toolchain_script = TPath(ZEPHYR_BASE) / TPath('cmake/verify-toolchain.cmake')
11471157
result = self.run_cmake_script([toolchain_script, "FORMAT=json"])
11481158

11491159
try:

scripts/pylib/twister/twisterlib/handlers.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,19 @@ def terminate_process(proc):
5959
so we need to use try_kill_process_by_pid.
6060
"""
6161

62-
for child in psutil.Process(proc.pid).children(recursive=True):
62+
parent = psutil.Process(proc.pid)
63+
to_terminate = parent.children(recursive=True)
64+
to_terminate.append(parent)
65+
66+
for p in to_terminate:
67+
with contextlib.suppress(ProcessLookupError, psutil.NoSuchProcess):
68+
p.terminate()
69+
_, alive = psutil.wait_procs(to_terminate, timeout=1)
70+
71+
for p in alive:
6372
with contextlib.suppress(ProcessLookupError, psutil.NoSuchProcess):
64-
os.kill(child.pid, signal.SIGTERM)
65-
proc.terminate()
66-
# sleep for a while before attempting to kill
67-
time.sleep(0.5)
68-
proc.kill()
73+
p.kill()
74+
_, alive = psutil.wait_procs(to_terminate, timeout=1)
6975

7076

7177
class Handler:
@@ -203,8 +209,8 @@ def try_kill_process_by_pid(self):
203209
pid = int(pid_file.read())
204210
os.unlink(self.pid_fn)
205211
self.pid_fn = None # clear so we don't try to kill the binary twice
206-
with contextlib.suppress(ProcessLookupError, psutil.NoSuchProcess):
207-
os.kill(pid, signal.SIGKILL)
212+
p = psutil.Process(pid)
213+
terminate_process(p)
208214

209215
def _output_reader(self, proc):
210216
self.line = proc.stdout.readline()

scripts/pylib/twister/twisterlib/reports.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
import xml.etree.ElementTree as ET
1212
from datetime import datetime
1313
from enum import Enum
14-
from pathlib import Path, PosixPath
14+
from pathlib import PosixPath, WindowsPath
1515

1616
from colorama import Fore
1717
from twisterlib.statuses import TwisterStatus
18+
from twisterlib.twister_path import TPath
1819

1920
logger = logging.getLogger('twister')
2021
logger.setLevel(logging.DEBUG)
@@ -171,7 +172,7 @@ def xunit_report_suites(self, json_file, filename):
171172
runnable = suite.get('runnable', 0)
172173
duration += float(handler_time)
173174
ts_status = TwisterStatus(suite.get('status'))
174-
classname = Path(suite.get("name","")).name
175+
classname = TPath(suite.get("name","")).name
175176
for tc in suite.get("testcases", []):
176177
status = TwisterStatus(tc.get('status'))
177178
reason = tc.get('reason', suite.get('reason', 'Unknown'))
@@ -253,7 +254,7 @@ def xunit_report(self, json_file, filename, selected_platform=None, full_report=
253254
):
254255
continue
255256
if full_report:
256-
classname = Path(ts.get("name","")).name
257+
classname = TPath(ts.get("name","")).name
257258
for tc in ts.get("testcases", []):
258259
status = TwisterStatus(tc.get('status'))
259260
reason = tc.get('reason', ts.get('reason', 'Unknown'))
@@ -295,8 +296,14 @@ def json_report(self, filename, version="NA", platform=None, filters=None):
295296
report_options = self.env.non_default_options()
296297

297298
# Resolve known JSON serialization problems.
298-
for k,v in report_options.items():
299-
report_options[k] = str(v) if type(v) in [PosixPath] else v
299+
for k, v in report_options.items():
300+
pathlikes = [PosixPath, WindowsPath, TPath]
301+
value = v
302+
if type(v) in pathlikes:
303+
value = os.fspath(v)
304+
if type(v) in [list]:
305+
value = [os.fspath(x) if type(x) in pathlikes else x for x in v]
306+
report_options[k] = value
300307

301308
report = {}
302309
report["environment"] = {"os": os.name,
@@ -342,7 +349,7 @@ def json_report(self, filename, version="NA", platform=None, filters=None):
342349
"name": instance.testsuite.name,
343350
"arch": instance.platform.arch,
344351
"platform": instance.platform.name,
345-
"path": instance.testsuite.source_dir_rel
352+
"path": os.fspath(instance.testsuite.source_dir_rel)
346353
}
347354
if instance.run_id:
348355
suite['run_id'] = instance.run_id

scripts/pylib/twister/twisterlib/size_calc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def _check_is_xip(self) -> None:
205205
# Search for CONFIG_XIP in the ELF's list of symbols using NM and AWK.
206206
# GREP can not be used as it returns an error if the symbol is not
207207
# found.
208-
is_xip_command = "nm " + self.elf_filename + \
208+
is_xip_command = "nm " + str(self.elf_filename) + \
209209
" | awk '/CONFIG_XIP/ { print $3 }'"
210210
is_xip_output = subprocess.check_output(
211211
is_xip_command, shell=True, stderr=subprocess.STDOUT).decode(
@@ -221,7 +221,7 @@ def _check_is_xip(self) -> None:
221221

222222
def _get_info_elf_sections(self) -> None:
223223
"""Calculate RAM and ROM usage and information about issues by section"""
224-
objdump_command = "objdump -h " + self.elf_filename
224+
objdump_command = "objdump -h " + str(self.elf_filename)
225225
objdump_output = subprocess.check_output(
226226
objdump_command, shell=True).decode("utf-8").splitlines()
227227

scripts/pylib/twister/twisterlib/testinstance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def __init__(self, testsuite, platform, outdir):
7272
else:
7373
# if suite is not in zephyr,
7474
# keep only the part after ".." in reconstructed dir structure
75-
source_dir_rel = testsuite.source_dir_rel.rsplit(os.pardir+os.path.sep, 1)[-1]
75+
source_dir_rel = testsuite.source_dir_rel.get_rel_after_dots()
7676
self.build_dir = os.path.join(
7777
outdir,
7878
platform.normalized_name,

0 commit comments

Comments
 (0)