From 09642a1d789028b8622b261f838b0b5329dac751 Mon Sep 17 00:00:00 2001 From: Dragan Bjedov Date: Thu, 28 Aug 2025 16:45:07 +0200 Subject: [PATCH 1/5] Added checking ping, ssh and sftp before executing tests --- itf/plugins/base/utils/exec_utils.py | 56 ++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/itf/plugins/base/utils/exec_utils.py b/itf/plugins/base/utils/exec_utils.py index 0ff7ecd..87e72eb 100644 --- a/itf/plugins/base/utils/exec_utils.py +++ b/itf/plugins/base/utils/exec_utils.py @@ -13,12 +13,62 @@ # pylint: disable=unused-argument +import logging + +from itf.plugins.base.target.base_target import Target +from itf.plugins.com.ssh import execute_command + + +logger = logging.getLogger(__name__) + def pre_tests_phase(target, ip_address, test_config, request): - # Will be implemented later - pass + __check_ping(target=target, check_timeout=60) + __check_ssh_is_up(target=target, ext_ip=test_config.os.value.ssh_uses_ext_ip, check_timeout=10, check_n_retries=5) + __check_sftp_is_up(target=target, ext_ip=test_config.os.value.ssh_uses_ext_ip) + # TODO Add more checks in pre_tests_phase def post_tests_phase(target, test_config): - # Will be implemented later + # TODO post_tests_phase will be implemented later pass + + +def __check_ping(target: Target, ext_ip: bool = False, check_timeout: int = 180): + """Checks whether the target can be pinged. + + :param Target target: Target to ping. + :param boolext_ip: Use external IP address. Default: False. + :param int check_timeout: How long to wait for check to succeed. Default: 180. + :raises AssertionError: If the target cannot be pinged within the specified time-frame. + """ + result = target.sut.ping(timeout=check_timeout, ext_ip=ext_ip) + assert result, f"{target.sut.type} is not pingable within expected time frame" + logger.info("Check target ping: OK") + + +def __check_ssh_is_up(target: Target, ext_ip: bool = False, check_timeout: int = 15, check_n_retries: int = 5): + """Check whether the target can be reached via SSH. + + :param Target target: Target to reach via SSH. + :param bool ext_ip: Use external IP address. Default: False. + :param int check_timeout: How long to wait for check to succeed. Default: 15. + :param int check_n_retries: How many times to re-try the check. Default: 5. + :raises AssertionError: If the SSH command fails within the specified time-frame. + """ + with target.sut.ssh(timeout=check_timeout, n_retries=check_n_retries, retry_interval=2, ext_ip=ext_ip) as ssh: + result = execute_command(ssh, "echo Qnx_S-core!") + assert result == 0, "Running SSH command on the target failed" + logger.info("Check target ssh: OK") + + +def __check_sftp_is_up(target: Target, ext_ip: bool = False): + """Check whether the target can be reached via SFTP. + + :param Target target: Target to reach via SFTP. + :param bool ext_ip: Use external IP address. Default: False. + """ + with target.sut.sftp(ext_ip=ext_ip) as sftp: + result = sftp.list_dirs_and_files("/") + assert result, "Running SFTP command on the target failed" + logger.info("Check target sftp: OK") From a96daf06940f273c50a602a00cee6d188eb49906 Mon Sep 17 00:00:00 2001 From: Dragan Bjedov Date: Fri, 29 Aug 2025 11:49:43 +0200 Subject: [PATCH 2/5] Added starting Qemu automatically - Qemu is started if `--qemu_image` is provided - Qemu RAM size and number of CPUs are mandatory parameters in config file --- .bazelrc | 5 + .gitignore | 8 + MODULE.bazel | 7 + README.md | 2 +- bazel/py_itf_test.bzl | 1 + config/target_config.json | 8 +- examples/MODULE.bazel | 14 ++ itf/plugins/base/base_plugin.py | 5 + itf/plugins/base/target/BUILD | 1 + itf/plugins/base/target/config/config.py | 2 + .../target/config/performance_processor.py | 26 +++ itf/plugins/base/target/qemu_target.py | 5 +- itf/plugins/qemu/BUILD | 27 +++ itf/plugins/qemu/__init__.py | 12 ++ itf/plugins/qemu/qemu.py | 163 ++++++++++++++++++ itf/plugins/qemu/qemu_process.py | 62 +++++++ requirements.in | 1 + requirements_lock.txt | 32 ++++ scripts/BUILD | 21 +++ scripts/run_under_qemu.sh | 75 ++++++++ 20 files changed, 473 insertions(+), 4 deletions(-) create mode 100644 itf/plugins/qemu/BUILD create mode 100644 itf/plugins/qemu/__init__.py create mode 100644 itf/plugins/qemu/qemu.py create mode 100644 itf/plugins/qemu/qemu_process.py create mode 100644 scripts/BUILD create mode 100755 scripts/run_under_qemu.sh diff --git a/.bazelrc b/.bazelrc index d4da110..0ba1267 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,3 +1,8 @@ common --registry=https://bcr.bazel.build test --test_output=errors + + +build:qemu-integration --run_under=//scripts:run_under_qemu +build:qemu-integration --test_arg="--qemu" +build:qemu-integration --test_arg="--os=qnx" diff --git a/.gitignore b/.gitignore index bd47a7d..65e53b2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,11 @@ MODULE.bazel.lock .ruff_cache bazel-* + +# VS Code +.vscode + +# Python cache/venv folders +*__pycache__* +.venv/ +*.egg-info/ diff --git a/MODULE.bazel b/MODULE.bazel index b8723e8..6cd5bc6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -72,6 +72,13 @@ git_override( ############################################################################### bazel_dep(name = "rules_cc", version = "0.1.1") +############################################################################### +# +# Shell dependency +# +############################################################################### +bazel_dep(name = "rules_shell", version = "0.6.0") + ################################################################################ # # Load DLT dependencies diff --git a/README.md b/README.md index 2117415..741500d 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Steps: ``` $ bazel test //test:test_ssh_bridge_network --test_output=streamed ``` - * Note: If it fails check IP Address of started Qemu with `ifconfig` and update IP addresses in `itf/config/target_config.json` for `S_CORE_ECU_QEMU_BRIDGE_NETWORK` + * Note: If it fails, check `IP address set to:` in logs of started Qemu and update IP addresses in `itf/config/target_config.json` for `S_CORE_ECU_QEMU_BRIDGE_NETWORK` * Run ssh test with qemu started with port forwarding * Start Qemu with bridge network from `reference_integration/qnx_qemu` folder: ``` diff --git a/bazel/py_itf_test.bzl b/bazel/py_itf_test.bzl index bd1235c..fa7ed99 100644 --- a/bazel/py_itf_test.bzl +++ b/bazel/py_itf_test.bzl @@ -40,6 +40,7 @@ def py_itf_test(name, srcs, args = [], data = [], plugins = [], **kwargs): requirement("pytest"), requirement("paramiko"), requirement("typing-extensions"), + requirement("netifaces"), "@score_itf//:itf", ], data = [ diff --git a/config/target_config.json b/config/target_config.json index c23a65b..e11d43e 100644 --- a/config/target_config.json +++ b/config/target_config.json @@ -13,7 +13,9 @@ "data_router_config": { "vlan_address": "127.0.0.1", "multicast_addresses": [] - } + }, + "qemu_num_cores": 2, + "qemu_ram_size": "1G" }, "safety_processor": { "name": "S_CORE_ECU_QEMU_BRIDGE_NETWORK_SC", @@ -39,7 +41,9 @@ "data_router_config": { "vlan_address": "127.0.0.1", "multicast_addresses": [] - } + }, + "qemu_num_cores": 2, + "qemu_ram_size": "1G" }, "safety_processor": { "name": "CORE_ECU_QEMU_PORT_FORWARDING_SC", diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index d1e990c..121dafa 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -45,6 +45,14 @@ register_toolchains("@llvm_toolchain//:all") ############################################################################### bazel_dep(name = "rules_cc", version = "0.1.1") + +############################################################################### +# +# Shell dependency +# +############################################################################### +bazel_dep(name = "rules_shell", version = "0.6.0") + ############################################################################### # # Container dependencies @@ -79,6 +87,12 @@ bazel_dep(name = "rules_pkg", version = "1.0.1") # ############################################################################### bazel_dep(name = "googletest", version = "1.15.0") + +############################################################################### +# +# ITF dependency +# +############################################################################### bazel_dep(name = "score_itf", version = "0.1") local_path_override( module_name = "score_itf", diff --git a/itf/plugins/base/base_plugin.py b/itf/plugins/base/base_plugin.py index 45c8b34..7262f8b 100644 --- a/itf/plugins/base/base_plugin.py +++ b/itf/plugins/base/base_plugin.py @@ -52,6 +52,8 @@ def pytest_addoption(parser): help="Operating System to run", ) parser.addoption("--qemu", action="store_true", help="Run tests with QEMU image") + parser.addoption("--qemu_image", action="store", help="Path to a QEMU image") + parser.addoption("--qvp", action="store_true", help="Run tests with QVP") parser.addoption("--hw", action="store_true", help="Run tests against connected HW") @@ -115,6 +117,7 @@ def __make_test_config(config): ecu=target_ecu_argparse(config.getoption("ecu")), os=config.getoption("os"), qemu=config.getoption("qemu"), + qemu_image=config.getoption("qemu_image"), qvp=config.getoption("qvp"), hw=config.getoption("hw"), ) @@ -122,4 +125,6 @@ def __make_test_config(config): def __make_target_config(test_config): target_config = test_config.ecu.sut + if test_config.qemu_image: + target_config.qemu_image_path = test_config.qemu_image return target_config diff --git a/itf/plugins/base/target/BUILD b/itf/plugins/base/target/BUILD index cf70f03..de76ce6 100644 --- a/itf/plugins/base/target/BUILD +++ b/itf/plugins/base/target/BUILD @@ -28,5 +28,6 @@ py_library( "//itf/plugins/base/target/config", "//itf/plugins/base/target/processors", "//itf/plugins/dlt", + "//itf/plugins/qemu", ], ) diff --git a/itf/plugins/base/target/config/config.py b/itf/plugins/base/target/config/config.py index e08ca47..eb73585 100644 --- a/itf/plugins/base/target/config/config.py +++ b/itf/plugins/base/target/config/config.py @@ -50,6 +50,8 @@ def load_configuration(config_file: str): network_interfaces=perf_config["network_interfaces"], ecu_name=perf_config["ecu_name"], data_router_config=perf_config["data_router_config"], + qemu_num_cores=perf_config["qemu_num_cores"], + qemu_ram_size=perf_config["qemu_ram_size"], params=perf_config.get("params", {}), ) PERFORMANCE_PROCESSORS[perf_config["name"]] = performance_processor diff --git a/itf/plugins/base/target/config/performance_processor.py b/itf/plugins/base/target/config/performance_processor.py index ba53c4a..fc8fc54 100644 --- a/itf/plugins/base/target/config/performance_processor.py +++ b/itf/plugins/base/target/config/performance_processor.py @@ -27,6 +27,8 @@ def __init__( network_interfaces: list = [], ecu_name: str = None, data_router_config: dict = None, + qemu_num_cores: int = 2, + qemu_ram_size: str = "1G", params: dict = None, ): """Initialize the PerformanceProcessor class. @@ -42,6 +44,8 @@ def __init__( :param str ecu_name: The ECU name for the processor. :param dict data_router_configs: Configuration for the data router with keys "vlan_address" and "multicast_addresses". + :param int qemu_num_cores: The number of CPU cores for QEMU. + :param str qemu_ram_size: The amount of RAM for QEMU. :param dict params: Additional parameters for the processor. """ super().__init__( @@ -57,6 +61,9 @@ def __init__( self.__network_interfaces = network_interfaces self.__ecu_name = ecu_name self.__data_router_config = data_router_config + self.__qemu_num_cores = qemu_num_cores + self.__qemu_ram_size = qemu_ram_size + self.__qemu_image_path = None @property def ext_ip_address(self): @@ -74,6 +81,22 @@ def network_interfaces(self): def data_router_config(self): return self.__data_router_config + @property + def qemu_num_cores(self): + return self.__qemu_num_cores + + @property + def qemu_ram_size(self): + return self.__qemu_ram_size + + @property + def qemu_image_path(self): + return self.__qemu_image_path + + @qemu_image_path.setter + def qemu_image_path(self, value): + self.__qemu_image_path = value + def update(self, processor): """Update the current processor with another processor's parameters. @@ -84,6 +107,9 @@ def update(self, processor): self.__network_interfaces = processor.network_interfaces self.__ecu_name = processor.ecu_name self.__data_router_config = processor.data_router_config + self.__qemu_num_cores = processor.num_cores + self.__qemu_ram_size = processor.ram_size + self.__qemu_image_path = processor.qemu_image_path def __eq__(self, other): if isinstance(other, PerformanceProcessor): diff --git a/itf/plugins/base/target/qemu_target.py b/itf/plugins/base/target/qemu_target.py index b40d552..894eb92 100644 --- a/itf/plugins/base/target/qemu_target.py +++ b/itf/plugins/base/target/qemu_target.py @@ -17,6 +17,7 @@ from itf.plugins.base.target.config.ecu import Ecu from itf.plugins.base.target.processors.qemu_processor import TargetProcessorQemu from itf.plugins.dlt.dlt_receive import DltReceive, Protocol +from itf.plugins.qemu.qemu_process import QemuProcess as Qemu class TargetQemu(Target): @@ -37,7 +38,9 @@ def qemu_target(target_config, test_config): Currently, only ITF tests against an already running Qemu instance is supported. """ - with nullcontext() as qemu_process: + with Qemu( + target_config.qemu_image_path, None, target_config.qemu_ram_size, target_config.qemu_num_cores + ) if target_config.qemu_image_path else nullcontext() as qemu_process: with DltReceive( target_ip=target_config.ip_address, protocol=Protocol.UDP, diff --git a/itf/plugins/qemu/BUILD b/itf/plugins/qemu/BUILD new file mode 100644 index 0000000..159dcd6 --- /dev/null +++ b/itf/plugins/qemu/BUILD @@ -0,0 +1,27 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "qemu", + srcs = [ + "__init__.py", + "qemu.py", + "qemu_process.py", + ], + imports = ["."], + visibility = ["//visibility:public"], + deps = [ + "//itf/plugins/utils/process", + ], +) diff --git a/itf/plugins/qemu/__init__.py b/itf/plugins/qemu/__init__.py new file mode 100644 index 0000000..6bdeed2 --- /dev/null +++ b/itf/plugins/qemu/__init__.py @@ -0,0 +1,12 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* diff --git a/itf/plugins/qemu/qemu.py b/itf/plugins/qemu/qemu.py new file mode 100644 index 0000000..6caae39 --- /dev/null +++ b/itf/plugins/qemu/qemu.py @@ -0,0 +1,163 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +import os +import shlex +import sys +import subprocess +import netifaces +import logging + +logger = logging.getLogger(__name__) + + +class Qemu: + """ + This class shall be used to start an qemu instance based on pre-configured Qemu parameters. + """ + + def __init__( + self, + path_to_image, + path_to_bootloader=None, + ram="1G", + cores="2", + cpu="Cascadelake-Server-v5", + host_first_network_device_ip_address="160.48.199.77", + host_second_network_device_ip_address="192.168.1.99", + ): + """Create a QEMU instance with the specified parameters. + + :param str path_to_image: The path to the Qemu image file. + :param str path_to_bootloader: The path to the Qemu bootloader file. + :param str ram: The amount of RAM to allocate to the QEMU instance. + :param str cores: The number of CPU cores to allocate to the QEMU instance. + :param str cpu: The CPU model to emulate. + Default is Cascadelake-Server-v5 used to emulate modern Intel CPU features. + For older Ubuntu versions change that to host in case of errors. + :param str host_first_network_device_ip_address: The IP address of the first network device on the host. + :param str host_second_network_device_ip_address: The IP address of the second network device on the host. + """ + self.__qemu_path = "/usr/bin/qemu-system-x86_64" + + self.__first_network_device_name = "unknown" + self.__second_network_device_name = "unknown" + self.__first_network_adapter_mac = "52:54:11:22:33:01" + self.__second_network_adapter_mac = "52:54:11:22:33:02" + self.__first_network_device_ip_address = host_first_network_device_ip_address + self.__second_network_device_ip_address = host_second_network_device_ip_address + + self.__path_to_image = path_to_image + self.__path_to_bootloader = path_to_bootloader + self.__ram = ram + self.__cores = cores + self.__cpu = cpu + + self.__check_qemu_is_installed() + self.__find_available_kvm_support() + self.__check_kvm_readable_when_necessary() + self.__find_tap_devices() + + self._subprocess = None + + def __enter__(self): + return self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + + def start(self, subprocess_params=None): + logger.debug(self.__build_qemu_command()) + subprocess_args = {"args": shlex.split(self.__build_qemu_command())} + if subprocess_params: + subprocess_args.update(subprocess_params) + self._subprocess = subprocess.Popen(**subprocess_args) + return self._subprocess + + def stop(self): + if self._subprocess.poll() is None: + self._subprocess.terminate() + self._subprocess.wait(2) + if self._subprocess.poll() is None: + self._subprocess.kill() + self._subprocess.wait(2) + ret = self._subprocess.returncode + if ret != 0: + raise Exception(f"QEMU process returned: {ret}") + + def __check_qemu_is_installed(self): + if not os.path.isfile(self.__qemu_path): + logger.fatal(f"Qemu is not installed under {self.__qemu_path}") + sys.exit(-1) + + def __find_available_kvm_support(self): + self._accelerator_support = "kvm" + with open("/proc/cpuinfo") as cpuinfo: + cpu_options = str(cpuinfo.read()) + if "vmx" not in cpu_options and "svm" not in cpu_options: + logger.error("No virtual capability on machine. We're using standard TCG accel on QEMU") + self._accelerator_support = "tcg" + + if not os.path.exists("/dev/kvm"): + logger.error("No KVM available. We're using standard TCG accel on QEMU") + self._accelerator_support = "tcg" + + def __check_kvm_readable_when_necessary(self): + if self._accelerator_support == "kvm": + if not os.access("/dev/kvm", os.R_OK): + logger.fatal( + "You dont have access rights to /dev/kvm. Consider adding yourself to kvm group. Aborting." + ) + sys.exit(-1) + + def __find_tap_devices(self): + for interface in netifaces.interfaces(): + try: + interface_address = netifaces.ifaddresses(interface)[netifaces.AF_INET][0]["addr"] + if interface_address == self.__first_network_device_ip_address: + self.__first_network_device_name = "tap0" + if interface_address == self.__second_network_device_ip_address: + self.__second_network_device_name = interface + except KeyError: + pass + + if "unknown" in (self.__first_network_device_name, self.__second_network_device_name): + logger.fatal("Could not find correct tap devices. Please setup network for Qemu first!") + sys.exit(-1) + + def __build_qemu_command(self): + return ( + f"{self.__qemu_path}" + " --enable-kvm" # Use hardware virtualization for better performance + f" -smp {self.__cores},maxcpus={self.__cores},cores={self.__cores}" + f" -cpu {self.__cpu}" # Specify CPU to emulate + f" -m {self.__ram}" # Specify RAM size + f" -kernel {self.__path_to_image}" # Specify kernel image + " -nographic" # Disable graphical display (console-only) + " -serial mon:stdio" # Redirect serial output to console + " -object rng-random,filename=/dev/urandom,id=rng0" # Provide hardware random number generation + f" {self.__first_network_adapter()}" + f" {self.__second_network_adapter()}" + " -device virtio-rng-pci,rng=rng0" # Provide hardware random number generation + ) + + def __first_network_adapter(self): + return ( + f" -netdev tap,id=t1,ifname={self.__first_network_device_name},script=no,downscript=no" + f" -device virtio-net-pci,netdev=t1,id=nic1,mac={self.__first_network_adapter_mac},guest_csum=off" + ) + + def __second_network_adapter(self): + return ( + f" -netdev tap,id=t2,ifname={self.__second_network_device_name},script=no,downscript=no" + f" -device virtio-net-pci,netdev=t2,id=nic2,mac={self.__second_network_adapter_mac},guest_csum=off" + ) diff --git a/itf/plugins/qemu/qemu_process.py b/itf/plugins/qemu/qemu_process.py new file mode 100644 index 0000000..e28b3f0 --- /dev/null +++ b/itf/plugins/qemu/qemu_process.py @@ -0,0 +1,62 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +import logging +import subprocess + +from itf.plugins.utils.process.console import PipeConsole +from itf.plugins.qemu.qemu import Qemu + +logger = logging.getLogger(__name__) + + +class QemuProcess: + def __init__(self, path_to_qemu_image, path_to_bootloader, available_ram, available_cores): + self._path_to_qemu_image = path_to_qemu_image + self._path_to_bootloader = path_to_bootloader + self._available_ram = available_ram + self._available_cores = available_cores + self._qemu = Qemu( + self._path_to_qemu_image, self._path_to_bootloader, self._available_ram, self._available_cores + ) + self._console = None + + def __enter__(self): + return self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + + def start(self): + logger.info("Starting Qemu...") + logger.info(f"Using QEMU image: {self._path_to_qemu_image}") + subprocess_params = { + "stdin": subprocess.PIPE, + "stdout": subprocess.PIPE, + "stderr": subprocess.STDOUT, + } + # pylint: disable=too-many-function-args + qemu_subprocess = self._qemu.start(subprocess_params) + self._console = PipeConsole("QEMU", qemu_subprocess) + return self + + def stop(self): + logger.info("Stopping Qemu...") + self._qemu.stop() + + def restart(self): + self.stop() + self.start() + + @property + def console(self): + return self._console diff --git a/requirements.in b/requirements.in index 80d9b30..2018064 100644 --- a/requirements.in +++ b/requirements.in @@ -4,3 +4,4 @@ docker==7.1.0 pytest==8.4.1 paramiko==3.5.1 typing-extensions==4.14.1 +netifaces==0.11.0 diff --git a/requirements_lock.txt b/requirements_lock.txt index 2163210..950787c 100644 --- a/requirements_lock.txt +++ b/requirements_lock.txt @@ -279,6 +279,38 @@ iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 # via pytest +netifaces==0.11.0 \ + --hash=sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32 \ + --hash=sha256:08e3f102a59f9eaef70948340aeb6c89bd09734e0dca0f3b82720305729f63ea \ + --hash=sha256:0f6133ac02521270d9f7c490f0c8c60638ff4aec8338efeff10a1b51506abe85 \ + --hash=sha256:18917fbbdcb2d4f897153c5ddbb56b31fa6dd7c3fa9608b7e3c3a663df8206b5 \ + --hash=sha256:2479bb4bb50968089a7c045f24d120f37026d7e802ec134c4490eae994c729b5 \ + --hash=sha256:2650beee182fed66617e18474b943e72e52f10a24dc8cac1db36c41ee9c041b7 \ + --hash=sha256:28f4bf3a1361ab3ed93c5ef360c8b7d4a4ae060176a3529e72e5e4ffc4afd8b0 \ + --hash=sha256:3ecb3f37c31d5d51d2a4d935cfa81c9bc956687c6f5237021b36d6fdc2815b2c \ + --hash=sha256:469fc61034f3daf095e02f9f1bbac07927b826c76b745207287bc594884cfd05 \ + --hash=sha256:48324183af7f1bc44f5f197f3dad54a809ad1ef0c78baee2c88f16a5de02c4c9 \ + --hash=sha256:50721858c935a76b83dd0dd1ab472cad0a3ef540a1408057624604002fcfb45b \ + --hash=sha256:54ff6624eb95b8a07e79aa8817288659af174e954cca24cdb0daeeddfc03c4ff \ + --hash=sha256:5be83986100ed1fdfa78f11ccff9e4757297735ac17391b95e17e74335c2047d \ + --hash=sha256:5f9ca13babe4d845e400921973f6165a4c2f9f3379c7abfc7478160e25d196a4 \ + --hash=sha256:73ff21559675150d31deea8f1f8d7e9a9a7e4688732a94d71327082f517fc6b4 \ + --hash=sha256:7dbb71ea26d304e78ccccf6faccef71bb27ea35e259fb883cfd7fd7b4f17ecb1 \ + --hash=sha256:815eafdf8b8f2e61370afc6add6194bd5a7252ae44c667e96c4c1ecf418811e4 \ + --hash=sha256:841aa21110a20dc1621e3dd9f922c64ca64dd1eb213c47267a2c324d823f6c8f \ + --hash=sha256:84e4d2e6973eccc52778735befc01638498781ce0e39aa2044ccfd2385c03246 \ + --hash=sha256:8f7da24eab0d4184715d96208b38d373fd15c37b0dafb74756c638bd619ba150 \ + --hash=sha256:96c0fe9696398253f93482c84814f0e7290eee0bfec11563bd07d80d701280c3 \ + --hash=sha256:aab1dbfdc55086c789f0eb37affccf47b895b98d490738b81f3b2360100426be \ + --hash=sha256:c03fb2d4ef4e393f2e6ffc6376410a22a3544f164b336b3a355226653e5efd89 \ + --hash=sha256:c37a1ca83825bc6f54dddf5277e9c65dec2f1b4d0ba44b8fd42bc30c91aa6ea1 \ + --hash=sha256:c92ff9ac7c2282009fe0dcb67ee3cd17978cffbe0c8f4b471c00fe4325c9b4d4 \ + --hash=sha256:c9a3a47cd3aaeb71e93e681d9816c56406ed755b9442e981b07e3618fb71d2ac \ + --hash=sha256:cb925e1ca024d6f9b4f9b01d83215fd00fe69d095d0255ff3f64bffda74025c8 \ + --hash=sha256:d07b01c51b0b6ceb0f09fc48ec58debd99d2c8430b09e56651addeaf5de48048 \ + --hash=sha256:e76c7f351e0444721e85f975ae92718e21c1f361bda946d60a214061de1f00a1 \ + --hash=sha256:eb4813b77d5df99903af4757ce980a98c4d702bbcb81f32a0b305a1537bdf0b1 + # via -r requirements.in packaging==24.2 \ --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f diff --git a/scripts/BUILD b/scripts/BUILD new file mode 100644 index 0000000..e56e35b --- /dev/null +++ b/scripts/BUILD @@ -0,0 +1,21 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") + +sh_binary( + name = "run_under_qemu", + srcs = [ + "run_under_qemu.sh", + ], + visibility = ["//visibility:public"], +) diff --git a/scripts/run_under_qemu.sh b/scripts/run_under_qemu.sh new file mode 100755 index 0000000..4f8625a --- /dev/null +++ b/scripts/run_under_qemu.sh @@ -0,0 +1,75 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +#!/bin/bash +set -euo pipefail + +# Check if TEST_UNDECLARED_OUTPUTS_DIR is set +CMD_UNDECLARED_OUTPUTS_DIR="true" +if [[ -n "${TEST_UNDECLARED_OUTPUTS_DIR:-}" ]]; then + CMD_UNDECLARED_OUTPUTS_DIR="export TEST_UNDECLARED_OUTPUTS_DIR=$TEST_UNDECLARED_OUTPUTS_DIR" +else + TEST_UNDECLARED_OUTPUTS_DIR="/tmp" +fi + +# Create a temp directory to be mounted as /var/run inside the unshared user namespace +TMP_VAR_RUN_DIR=${TEST_TMPDIR-$(mktemp -d)}/tmp_var_run +mkdir -p "${TMP_VAR_RUN_DIR}" +CMD_CREATE_VAR_DIR="mount --bind $TMP_VAR_RUN_DIR /var/run" + +# CMD_BRIDGE_NETWORK="ip link add name virbr0 type bridge && +# ip link set virbr0 up && +# ip addr add 192.168.122.1/24 dev virbr0" + +# CMD_ENABLE_IP_FORWARDING="echo 1 | tee /proc/sys/net/ipv4/ip_forward" + +# Creates a new tap device using tunctl. Afterwards it configures the new network interface with +# the correct ip-address and the correct routing information. +CMD_TAP0_INTERFACE="ip tuntap add mode tap tap0 && +ip addr add 169.254.21.88/16 broadcast 160.48.199.255 dev tap0 && +ip link set dev tap0 up && +ip link add link tap0 name tap0.73 type vlan id 73 && +ip addr add 160.48.199.77/25 broadcast 160.48.199.255 dev tap0.73 && +ip link set dev tap0.73 up && +ip link set tap0.73 multicast on && +ip route add 231.255.42.99 dev tap0.73 && +ip route add 232.255.42.99 dev tap0.73 && +ip route add 233.255.42.99 dev tap0.73 && +ip route add 234.255.42.99 dev tap0.73 && +ip route add 235.255.42.99 dev tap0.73 && +ip route add 236.255.42.99 dev tap0.73 && +ip route add 237.255.42.99 dev tap0.73 && +ip route add 239.255.42.99 dev tap0.73 && +ip link add link tap0 name tap0.105 type vlan id 105 && +ip addr add 160.48.249.142/27 broadcast 160.48.249.255 dev tap0.105 && +ip link set dev tap0.105 up && +ip link set tap0.105 multicast on && +ip route add 224.0.0.0/4 dev tap0 && +ip route append 224.0.0.0/4 dev tap0.73 && +ip route append 224.0.0.0/4 dev tap0.105" + +CMD_TAP1_INTERFACE="ip tuntap add mode tap tap1 && +ip addr add 192.168.1.99/24 broadcast 160.48.199.255 dev tap1 && +ip link set dev tap1 up && +ip route add to 192.168.1.99 dev tap1" + +# Allow non-root user to bind to port 500 +# CMD_UNPRIVILEGED_PORT_START_500="echo 500 | tee /proc/sys/net/ipv4/ip_unprivileged_port_start" + +# Run the concatenated commands in an unnamed network namespace for isolation +unshare -m -U -n --map-root-user /bin/bash -c \ + "${CMD_UNDECLARED_OUTPUTS_DIR} && + ${CMD_CREATE_VAR_DIR} && + ${CMD_TAP0_INTERFACE} && + ${CMD_TAP1_INTERFACE} && + ${*}" From a6e1339495f37428473a6ca82451481eeaff17f9 Mon Sep 17 00:00:00 2001 From: Lukasz Tekieli Date: Mon, 15 Sep 2025 12:38:52 +0200 Subject: [PATCH 3/5] Fix toolchain usage --- bazel/py_itf_test.bzl | 40 +++++-- bazel/rules/as_host/BUILD | 12 -- .../{as_host/rule.bzl => build_as_host.bzl} | 0 bazel/rules/run_as_exec.bzl | 104 ++++++++++++++++++ itf/plugins/dlt/BUILD | 2 +- 5 files changed, 133 insertions(+), 25 deletions(-) delete mode 100644 bazel/rules/as_host/BUILD rename bazel/rules/{as_host/rule.bzl => build_as_host.bzl} (100%) create mode 100644 bazel/rules/run_as_exec.bzl diff --git a/bazel/py_itf_test.bzl b/bazel/py_itf_test.bzl index fa7ed99..c891d4a 100644 --- a/bazel/py_itf_test.bzl +++ b/bazel/py_itf_test.bzl @@ -14,27 +14,30 @@ load("@itf_pip//:requirements.bzl", "requirement") load("@rules_python//python:defs.bzl", "py_test") +load("@score_itf//bazel/rules:run_as_exec.bzl", "test_as_exec") def py_itf_test(name, srcs, args = [], data = [], plugins = [], **kwargs): + """Bazel macro for running ITF tests. + + Args: + name: Name of the test target. + srcs: List of source files for the test. + args: Additional arguments to pass to ITF. + data: Data files needed for the test. + plugins: List of pytest plugins to enable. + **kwargs: Additional keyword arguments passed to py_test. + """ pytest_bootstrap = Label("@score_itf//:main.py") pytest_ini = Label("@score_itf//:pytest.ini") plugins = ["-p %s" % plugin for plugin in plugins] py_test( - name = name, + name = "_" + name, srcs = [ pytest_bootstrap, ] + srcs, main = pytest_bootstrap, - args = args + - ["-c $(location %s)" % pytest_ini] + - [ - "-p no:cacheprovider", - "--show-capture=no", - ] + - plugins + - ["$(location %s)" % x for x in srcs], deps = [ requirement("docker"), requirement("pytest"), @@ -43,9 +46,22 @@ def py_itf_test(name, srcs, args = [], data = [], plugins = [], **kwargs): requirement("netifaces"), "@score_itf//:itf", ], - data = [ - pytest_ini, - ] + data, + tags = ["manual"], + ) + + test_as_exec( + name = name, + executable = "_" + name, + data_as_exec = [pytest_ini] + srcs, + data = data, + args = args + + ["-c $(location %s)" % pytest_ini] + + [ + "-p no:cacheprovider", + "--show-capture=no", + ] + + plugins + + ["$(location %s)" % x for x in srcs], env = { "PYTHONDONOTWRITEBYTECODE": "1", }, diff --git a/bazel/rules/as_host/BUILD b/bazel/rules/as_host/BUILD deleted file mode 100644 index 6bdeed2..0000000 --- a/bazel/rules/as_host/BUILD +++ /dev/null @@ -1,12 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* diff --git a/bazel/rules/as_host/rule.bzl b/bazel/rules/build_as_host.bzl similarity index 100% rename from bazel/rules/as_host/rule.bzl rename to bazel/rules/build_as_host.bzl diff --git a/bazel/rules/run_as_exec.bzl b/bazel/rules/run_as_exec.bzl new file mode 100644 index 0000000..c829deb --- /dev/null +++ b/bazel/rules/run_as_exec.bzl @@ -0,0 +1,104 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +""" +Runs an executable on execution platform, with runtime deps built with either +target or execution platform configuration. + +Executable rule outputs a symlink to the actual executable, fully leveraging +it's implementation (e.g. reuse a py_binary) and remaining somewhat OS +independent. + +NOTE: This has one caveat, that the symlink is "built" with target +configuration. So do not depend on targets output by this rule unless +you're qualified personnel. +""" + +def run_as_exec(**kwargs): + """This macro remaps the arguments for clarity: + + deps -> data_as_exec: Runtime deps built with execution platform configuration. + """ + data_as_exec = kwargs.pop("data_as_exec", None) + _as_exec_run( + deps = data_as_exec, + **kwargs + ) + +def test_as_exec(**kwargs): + """This macro remaps the arguments for clarity: + + deps -> data_as_exec: Runtime deps built with execution platform configuration. + """ + data_as_exec = kwargs.pop("data_as_exec", None) + _as_exec_test( + deps = data_as_exec, + **kwargs + ) + +_RULE_ATTRS = { + # In order for args expansion to work in bazel for an executable rule + # the attributes must be one of: "srcs", "deps", "data" or "tools". + # See Bazel's LocationExpander implementation, these attribute names + # are hardcoded. + "data": attr.label_list( + allow_files = True, + cfg = "target", + ), + "deps": attr.label_list( + allow_files = True, + cfg = "exec", + ), + "env": attr.string_dict(), + "executable": attr.label( + allow_files = True, + cfg = "exec", + executable = True, + mandatory = True, + ), +} + +def _executable_as_exec_impl(ctx): + link = ctx.actions.declare_file(ctx.attr.name) + ctx.actions.symlink( + output = link, + target_file = ctx.executable.executable, + is_executable = True, + ) + + return [ + DefaultInfo( + executable = link, + runfiles = ctx.runfiles( + files = ctx.files.data + ctx.files.deps + ctx.files.executable, + transitive_files = depset( + transitive = [ctx.attr.executable.default_runfiles.files] + + [dataf.default_runfiles.files for dataf in ctx.attr.data] + + [dataf.data_runfiles.files for dataf in ctx.attr.data], + ), + ), + ), + RunEnvironmentInfo(environment = ctx.attr.env), + ] + +_as_exec_run = rule( + implementation = _executable_as_exec_impl, + attrs = _RULE_ATTRS, + executable = True, +) + +_as_exec_test = rule( + implementation = _executable_as_exec_impl, + attrs = _RULE_ATTRS, + test = True, +) diff --git a/itf/plugins/dlt/BUILD b/itf/plugins/dlt/BUILD index e7199b1..6e0983c 100644 --- a/itf/plugins/dlt/BUILD +++ b/itf/plugins/dlt/BUILD @@ -10,7 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("//bazel/rules/as_host:rule.bzl", "as_host") +load("//bazel/rules:build_as_host.bzl", "as_host") load("@rules_python//python:defs.bzl", "py_library") as_host( From d67902e3ccf44929529d6331bd7e9ae77b0f0c01 Mon Sep 17 00:00:00 2001 From: Dragan Bjedov Date: Tue, 16 Sep 2025 14:09:25 +0200 Subject: [PATCH 4/5] Provide dlt-receive path as argument --- bazel/py_itf_test.bzl | 10 +++++++++- examples/MODULE.bazel | 1 - itf/plugins/base/base_plugin.py | 7 +++++++ itf/plugins/base/target/hw_target.py | 2 +- itf/plugins/base/target/qemu_target.py | 2 +- itf/plugins/base/target/qvp_target.py | 2 +- itf/plugins/docker.py | 5 +++++ itf/plugins/utils/utils.py | 16 ++++++++++++++-- 8 files changed, 38 insertions(+), 7 deletions(-) diff --git a/bazel/py_itf_test.bzl b/bazel/py_itf_test.bzl index c891d4a..d5536b9 100644 --- a/bazel/py_itf_test.bzl +++ b/bazel/py_itf_test.bzl @@ -29,6 +29,14 @@ def py_itf_test(name, srcs, args = [], data = [], plugins = [], **kwargs): """ pytest_bootstrap = Label("@score_itf//:main.py") pytest_ini = Label("@score_itf//:pytest.ini") + dlt_receive = Label("@score_itf//itf/plugins/dlt:dlt-receive_as_host") + dlt_library = Label("@score_itf//itf/plugins/dlt:libdlt_as_host.so") + + data_as_exec = [pytest_ini] + srcs + + if "itf.plugins.base.base_plugin" in plugins: + data_as_exec += [dlt_receive, dlt_library] + args.append("--dlt_receive_path=$(location %s)" % dlt_receive) plugins = ["-p %s" % plugin for plugin in plugins] @@ -52,7 +60,7 @@ def py_itf_test(name, srcs, args = [], data = [], plugins = [], **kwargs): test_as_exec( name = name, executable = "_" + name, - data_as_exec = [pytest_ini] + srcs, + data_as_exec = data_as_exec, data = data, args = args + ["-c $(location %s)" % pytest_ini] + diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 121dafa..692e6bc 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -45,7 +45,6 @@ register_toolchains("@llvm_toolchain//:all") ############################################################################### bazel_dep(name = "rules_cc", version = "0.1.1") - ############################################################################### # # Shell dependency diff --git a/itf/plugins/base/base_plugin.py b/itf/plugins/base/base_plugin.py index 7262f8b..006f547 100644 --- a/itf/plugins/base/base_plugin.py +++ b/itf/plugins/base/base_plugin.py @@ -35,6 +35,12 @@ def pytest_addoption(parser): default="config/target_config.json", help="Path to json file with target configurations.", ) + # Internally provided in py_itf_test macro + parser.addoption( + "--dlt_receive_path", + action="store", + help="Path to dlt-receive binary", + ) parser.addoption( "--ecu", action="store", @@ -120,6 +126,7 @@ def __make_test_config(config): qemu_image=config.getoption("qemu_image"), qvp=config.getoption("qvp"), hw=config.getoption("hw"), + dlt_receive_path=config.getoption("dlt_receive_path"), ) diff --git a/itf/plugins/base/target/hw_target.py b/itf/plugins/base/target/hw_target.py index 5140bd4..b00cbd5 100644 --- a/itf/plugins/base/target/hw_target.py +++ b/itf/plugins/base/target/hw_target.py @@ -33,7 +33,7 @@ def hw_target(target_config, test_config): target_ip=target_config.ip_address, protocol=Protocol.UDP, data_router_config=target_config.data_router_config, - binary_path="./itf/plugins/dlt/dlt-receive", + binary_path=test_config.dlt_receive_path, ): target = Target(test_config.ecu, test_config.os, diagnostic_ip) target.register_processors() diff --git a/itf/plugins/base/target/qemu_target.py b/itf/plugins/base/target/qemu_target.py index 894eb92..cc24872 100644 --- a/itf/plugins/base/target/qemu_target.py +++ b/itf/plugins/base/target/qemu_target.py @@ -45,7 +45,7 @@ def qemu_target(target_config, test_config): target_ip=target_config.ip_address, protocol=Protocol.UDP, data_router_config=target_config.data_router_config, - binary_path="./itf/plugins/dlt/dlt-receive", + binary_path=test_config.dlt_receive_path, ): target = TargetQemu(test_config.ecu, test_config.os) target.register_processors(qemu_process) diff --git a/itf/plugins/base/target/qvp_target.py b/itf/plugins/base/target/qvp_target.py index 8b4b3ca..595ca8d 100644 --- a/itf/plugins/base/target/qvp_target.py +++ b/itf/plugins/base/target/qvp_target.py @@ -46,7 +46,7 @@ def qvp_target(target_config, test_config): target_ip=target_config.ip_address, protocol=Protocol.UDP, data_router_config=target_config.data_router_config, - binary_path="./itf/plugins/dlt/dlt-receive", + binary_path=test_config.dlt_receive_path, ): target = TargetQvp(test_config.ecu, test_config.os) target.register_processors(qvp_process) diff --git a/itf/plugins/docker.py b/itf/plugins/docker.py index f14d36f..88492b8 100644 --- a/itf/plugins/docker.py +++ b/itf/plugins/docker.py @@ -32,6 +32,11 @@ def pytest_addoption(parser): required=False, help="Docker image bootstrap command, that will be executed before referencing the container.", ) + parser.addoption( + "--dlt_receive_path", + action="store", + help="Path to dlt-receive binary", + ) @pytest.fixture() diff --git a/itf/plugins/utils/utils.py b/itf/plugins/utils/utils.py index a263f81..bdd13a5 100644 --- a/itf/plugins/utils/utils.py +++ b/itf/plugins/utils/utils.py @@ -10,11 +10,23 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +import logging +import os + + CONSOLE_WIDTH = 80 +logger = logging.getLogger(__name__) + +def padder(string: str, length: int = CONSOLE_WIDTH) -> str: + """Pad a string with dashes to fit in a given length. -def padder(string, length=CONSOLE_WIDTH): + :param str string: The string to pad. + :param int length: The total length of the padded string, defaults to CONSOLE_WIDTH. + :return: The padded string. + :rtype: str + """ str_len = len(string) left = round((length - 2 - str_len) / 2) right = length - 2 - str_len - left - return f'{left*"-"} {string} {right*"-"}' + return f"{left * '-'} {string} {right * '-'}" From f0ad8f6eccba12803e14e9d1eebc09482b9acf25 Mon Sep 17 00:00:00 2001 From: Dragan Bjedov Date: Tue, 16 Sep 2025 17:25:32 +0200 Subject: [PATCH 5/5] Save ITF results in JUnit xml file itf-results.xml --- MODULE.bazel | 2 +- main.py | 3 +++ pytest.ini | 9 ++++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 6cd5bc6..b8a1d1d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -12,7 +12,7 @@ # ******************************************************************************* module( name = "score_itf", - version = "0.1", + version = "0.1.0", compatibility_level = 0, ) diff --git a/main.py b/main.py index acc803b..797dcd7 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,10 @@ +import os import sys import pytest +import itf.plugins.utils.bazel as bazel if __name__ == "__main__": args = sys.argv[1:] + args += [f"--junitxml={os.path.join(bazel.get_output_dir(), 'itf-results.xml')}"] sys.exit(pytest.main(args)) diff --git a/pytest.ini b/pytest.ini index 0fabec0..4b3637b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,12 +1,15 @@ [pytest] -log_cli=True -log_cli_level=Debug +log_cli = True +log_cli_level = Debug log_cli_format = [%(asctime)s.%(msecs)03d] [%(levelname)-3s] [%(name)s] %(message)s log_cli_date_format = %Y-%m-%d %H:%M:%S log_format = [%(asctime)s.%(msecs)03d] [%(levelname)-3s] [%(name)s] %(message)s log_date_format = %Y-%m-%d %H:%M:%S -log_file_level=Debug +log_file_level = Debug log_file_format = [%(asctime)s.%(msecs)03d] [%(levelname)-3s] [%(name)s] %(message)s log_file_date_format = %Y-%m-%d %H:%M:%S + +junit_suite_name = ITF +junit_duration_report = call