From e11fa32a7f7bd64bd4525f8a7fb66a6ed32fd158 Mon Sep 17 00:00:00 2001 From: Chris Langhans Date: Mon, 27 Apr 2026 14:46:38 +0200 Subject: [PATCH 1/4] feat(deps): add pytest-mock for unit test mocking Co-authored-by: $(git config user.name) <$(git config user.email)> --- requirements.in | 1 + requirements_lock.txt | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/requirements.in b/requirements.in index 46151fa..9b0b94c 100644 --- a/requirements.in +++ b/requirements.in @@ -5,3 +5,4 @@ pytest==9.0.2 paramiko==4.0.0 typing-extensions==4.15.0 pydantic==2.10.6 +pytest-mock==3.14.0 diff --git a/requirements_lock.txt b/requirements_lock.txt index ec52977..91336cb 100644 --- a/requirements_lock.txt +++ b/requirements_lock.txt @@ -505,6 +505,12 @@ pynacl==1.6.2 \ pytest==9.0.2 \ --hash=sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b \ --hash=sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11 + # via + # -r requirements.in + # pytest-mock +pytest-mock==3.14.0 \ + --hash=sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f \ + --hash=sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0 # via -r requirements.in requests==2.32.5 \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ From 4b9f86f9ffc00ae5bb4c6e4cb810e343fb898ee7 Mon Sep 17 00:00:00 2001 From: Chris Langhans Date: Mon, 27 Apr 2026 14:47:32 +0200 Subject: [PATCH 2/4] feat(coverage): enable Bazel native coverage for unit tests --- MODULE.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/MODULE.bazel b/MODULE.bazel index 185499f..6aa673d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -29,6 +29,7 @@ python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.toolchain( is_default = True, python_version = PYTHON_VERSION, + configure_coverage_tool = True, ) ############################################################################### From 5290c8ecaa698a8f47c4e309e768905dc105fe46 Mon Sep 17 00:00:00 2001 From: Chris Langhans Date: Mon, 27 Apr 2026 14:48:28 +0200 Subject: [PATCH 3/4] feat(test): add py_itf_unittest rule and restructure test directory - Introduce py_itf_unittest macro: lightweight py_test wrapper with no ITF plugin infrastructure, pytest-mock included by default - Export py_itf_unittest via defs.bzl alongside py_itf_test - Move existing tests to test/integration/ - Add test/unit/ with test_ping (mocker) and test_qemu_config_schema (pure Pydantic model validation, no file I/O) - Add integration-level test_qemu_config_schema (load_configuration with real JSON files) and test_attribute_plugin - Set coverage instrumentation filter to //score/itf[/:] --- .bazelrc | 3 + bazel/py_itf_unittest.bzl | 51 ++++++++++++ defs.bzl | 2 + test/{ => integration}/BUILD | 51 +++++------- test/{ => integration}/conftest.py | 0 test/{ => integration}/test_async_exec.py | 0 .../test_attribute_plugin.py | 0 test/{ => integration}/test_dlt.py | 0 test/{ => integration}/test_docker.py | 0 test/{ => integration}/test_qemu.py | 0 .../test_qemu_config_schema.py | 0 .../test_rules_are_working_correctly.py | 0 test/{ => integration}/test_ssh.py | 0 test/test_ping.py | 35 -------- test/unit/BUILD | 32 ++++++++ test/unit/test_ping.py | 34 ++++++++ test/unit/test_qemu_config_schema.py | 80 +++++++++++++++++++ 17 files changed, 221 insertions(+), 67 deletions(-) create mode 100644 bazel/py_itf_unittest.bzl rename test/{ => integration}/BUILD (94%) rename test/{ => integration}/conftest.py (100%) rename test/{ => integration}/test_async_exec.py (100%) rename test/{ => integration}/test_attribute_plugin.py (100%) rename test/{ => integration}/test_dlt.py (100%) rename test/{ => integration}/test_docker.py (100%) rename test/{ => integration}/test_qemu.py (100%) rename test/{ => integration}/test_qemu_config_schema.py (100%) rename test/{ => integration}/test_rules_are_working_correctly.py (100%) rename test/{ => integration}/test_ssh.py (100%) delete mode 100644 test/test_ping.py create mode 100644 test/unit/BUILD create mode 100644 test/unit/test_ping.py create mode 100644 test/unit/test_qemu_config_schema.py diff --git a/.bazelrc b/.bazelrc index a86da27..2c65f0f 100644 --- a/.bazelrc +++ b/.bazelrc @@ -3,6 +3,9 @@ common --registry=https://bcr.bazel.build test --test_output=errors +coverage --combined_report=lcov +coverage --instrumentation_filter="//score/itf[/:]" + build:x86_64-qnx --incompatible_strict_action_env build:x86_64-qnx --platforms=@score_bazel_platforms//:x86_64-qnx8_0 build:x86_64-qnx --sandbox_writable_path=/var/tmp diff --git a/bazel/py_itf_unittest.bzl b/bazel/py_itf_unittest.bzl new file mode 100644 index 0000000..5542480 --- /dev/null +++ b/bazel/py_itf_unittest.bzl @@ -0,0 +1,51 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 +# ******************************************************************************* + +"""Lightweight macro for running unit tests via pytest without ITF plugin infrastructure.""" + +load("@rules_python//python:defs.bzl", "py_test") + +def py_itf_unittest(name, srcs, deps = [], data = [], env = {}, pytest_config = None, **kwargs): + """Thin py_test wrapper for unit tests that do not need ITF plugin machinery. + + Unlike py_itf_test, this macro creates a direct py_test with no launcher + script or plugin infrastructure, so Bazel coverage works out of the box. + + Args: + name: Target name. + srcs: Python test source files. + deps: Additional Python dependencies. + data: Data files available at runtime. + env: Environment variables for the test. + pytest_config: Optional pytest config file. Defaults to @score_itf//:pytest.ini. + **kwargs: Forwarded to py_test (e.g. size, timeout, tags). + """ + pytest_bootstrap = Label("@score_itf//:main.py") + if not pytest_config: + pytest_config = Label("@score_itf//:pytest.ini") + + py_test( + name = name, + srcs = [pytest_bootstrap] + srcs, + main = pytest_bootstrap, + args = [ + "-c $(location %s)" % pytest_config, + "-p no:cacheprovider", + "--show-capture=no", + "--junitxml=$$XML_OUTPUT_FILE", + ] + ["$(location %s)" % x for x in srcs], + deps = ["@score_itf//:itf", "@itf_pip//pytest_mock"] + deps, + data = [pytest_config] + data, + env = {"PYTHONDONOTWRITEBYTECODE": "1"} | env, + **kwargs + ) diff --git a/defs.bzl b/defs.bzl index c319dc4..6a4d164 100644 --- a/defs.bzl +++ b/defs.bzl @@ -13,5 +13,7 @@ """ITF public Bazel interface""" load("@score_itf//bazel:py_itf_test.bzl", local_py_itf_test = "py_itf_test") +load("@score_itf//bazel:py_itf_unittest.bzl", local_py_itf_unittest = "py_itf_unittest") py_itf_test = local_py_itf_test +py_itf_unittest = local_py_itf_unittest diff --git a/test/BUILD b/test/integration/BUILD similarity index 94% rename from test/BUILD rename to test/integration/BUILD index 5b3c40f..fcd7559 100644 --- a/test/BUILD +++ b/test/integration/BUILD @@ -12,6 +12,25 @@ # ******************************************************************************* load("//:defs.bzl", "py_itf_test") +py_itf_test( + name = "test_attribute_plugin", + srcs = ["test_attribute_plugin.py"], + deps = ["//score/itf/plugins:attribute_plugin"], +) + +py_itf_test( + name = "test_qemu_config_schema", + srcs = ["test_qemu_config_schema.py"], + data = [ + "//test/resources:qemu_bridge_config", + "//test/resources:qemu_port_forwarding_config", + ], + deps = [ + "//score/itf/plugins/qemu", + "@rules_python//python/runfiles", + ], +) + py_itf_test( name = "test_rules_are_working_correctly", srcs = [ @@ -183,35 +202,3 @@ test_suite( ":test_ssh_configurable", ], ) - -py_itf_test( - name = "test_ping", - srcs = [ - "test_ping.py", - ], -) - -py_itf_test( - name = "test_qemu_config_schema", - srcs = [ - "test_qemu_config_schema.py", - ], - data = [ - "//test/resources:qemu_bridge_config", - "//test/resources:qemu_port_forwarding_config", - ], - deps = [ - "//score/itf/plugins/qemu", - "@rules_python//python/runfiles", - ], -) - -py_itf_test( - name = "test_attribute_plugin", - srcs = [ - "test_attribute_plugin.py", - ], - plugins = [ - "//score/itf/plugins:attribute_plugin", - ], -) diff --git a/test/conftest.py b/test/integration/conftest.py similarity index 100% rename from test/conftest.py rename to test/integration/conftest.py diff --git a/test/test_async_exec.py b/test/integration/test_async_exec.py similarity index 100% rename from test/test_async_exec.py rename to test/integration/test_async_exec.py diff --git a/test/test_attribute_plugin.py b/test/integration/test_attribute_plugin.py similarity index 100% rename from test/test_attribute_plugin.py rename to test/integration/test_attribute_plugin.py diff --git a/test/test_dlt.py b/test/integration/test_dlt.py similarity index 100% rename from test/test_dlt.py rename to test/integration/test_dlt.py diff --git a/test/test_docker.py b/test/integration/test_docker.py similarity index 100% rename from test/test_docker.py rename to test/integration/test_docker.py diff --git a/test/test_qemu.py b/test/integration/test_qemu.py similarity index 100% rename from test/test_qemu.py rename to test/integration/test_qemu.py diff --git a/test/test_qemu_config_schema.py b/test/integration/test_qemu_config_schema.py similarity index 100% rename from test/test_qemu_config_schema.py rename to test/integration/test_qemu_config_schema.py diff --git a/test/test_rules_are_working_correctly.py b/test/integration/test_rules_are_working_correctly.py similarity index 100% rename from test/test_rules_are_working_correctly.py rename to test/integration/test_rules_are_working_correctly.py diff --git a/test/test_ssh.py b/test/integration/test_ssh.py similarity index 100% rename from test/test_ssh.py rename to test/integration/test_ssh.py diff --git a/test/test_ping.py b/test/test_ping.py deleted file mode 100644 index 598f997..0000000 --- a/test/test_ping.py +++ /dev/null @@ -1,35 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 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 pytest -from unittest.mock import patch - -from score.itf.core.com.ping import ping - - -def test_ping_raises_when_ping_utility_is_missing(): - with patch("score.itf.core.com.ping.shutil.which", return_value=None): - with pytest.raises(RuntimeError, match="'ping' utility is not installed"): - ping("127.0.0.1") - - -def test_ping_returns_true_when_host_is_reachable(): - with patch("score.itf.core.com.ping.shutil.which", return_value="/usr/bin/ping"): - with patch("score.itf.core.com.ping.os.system", return_value=0): - assert ping("127.0.0.1") is True - - -def test_ping_returns_false_when_host_is_unreachable(): - with patch("score.itf.core.com.ping.shutil.which", return_value="/usr/bin/ping"): - with patch("score.itf.core.com.ping.os.system", return_value=1): - assert ping("192.0.2.1") is False diff --git a/test/unit/BUILD b/test/unit/BUILD new file mode 100644 index 0000000..68c32da --- /dev/null +++ b/test/unit/BUILD @@ -0,0 +1,32 @@ +# ******************************************************************************* +# Copyright (c) 2026 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("//:defs.bzl", "py_itf_unittest") + +py_itf_unittest( + name = "test_ping", + srcs = ["test_ping.py"], +) + +py_itf_unittest( + name = "test_qemu_config_schema", + srcs = ["test_qemu_config_schema.py"], + deps = ["//score/itf/plugins/qemu:config"], +) + +test_suite( + name = "unit", + tests = [ + ":test_ping", + ":test_qemu_config_schema", + ], +) diff --git a/test/unit/test_ping.py b/test/unit/test_ping.py new file mode 100644 index 0000000..b5fc782 --- /dev/null +++ b/test/unit/test_ping.py @@ -0,0 +1,34 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 pytest + +from score.itf.core.com.ping import ping + + +def test_ping_raises_when_ping_utility_is_missing(mocker): + mocker.patch("score.itf.core.com.ping.shutil.which", return_value=None) + with pytest.raises(RuntimeError, match="'ping' utility is not installed"): + ping("127.0.0.1") + + +def test_ping_returns_true_when_host_is_reachable(mocker): + mocker.patch("score.itf.core.com.ping.shutil.which", return_value="/usr/bin/ping") + mocker.patch("score.itf.core.com.ping.os.system", return_value=0) + assert ping("127.0.0.1") is True + + +def test_ping_returns_false_when_host_is_unreachable(mocker): + mocker.patch("score.itf.core.com.ping.shutil.which", return_value="/usr/bin/ping") + mocker.patch("score.itf.core.com.ping.os.system", return_value=1) + assert ping("192.0.2.1") is False diff --git a/test/unit/test_qemu_config_schema.py b/test/unit/test_qemu_config_schema.py new file mode 100644 index 0000000..772a6ed --- /dev/null +++ b/test/unit/test_qemu_config_schema.py @@ -0,0 +1,80 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 pytest + +from score.itf.plugins.qemu.config import QemuConfigModel + + +_VALID_BRIDGE_CONFIG = { + "networks": [{"name": "tap0", "ip_address": "169.254.158.190", "gateway": "169.254.21.88"}], + "ssh_port": 22, + "qemu_num_cores": 2, + "qemu_ram_size": "1G", +} + +_VALID_PORT_FORWARDING_CONFIG = { + "networks": [{"name": "lo", "ip_address": "127.0.0.1", "gateway": "127.0.0.1"}], + "ssh_port": 2222, + "qemu_num_cores": 2, + "qemu_ram_size": "1G", + "port_forwarding": [{"host_port": 2222, "guest_port": 22}], +} + + +def test_valid_bridge_config(): + QemuConfigModel.model_validate(_VALID_BRIDGE_CONFIG) + + +def test_valid_port_forwarding_config(): + QemuConfigModel.model_validate(_VALID_PORT_FORWARDING_CONFIG) + + +def test_missing_networks_is_rejected(): + config = {**_VALID_BRIDGE_CONFIG} + del config["networks"] + with pytest.raises(Exception): + QemuConfigModel.model_validate(config) + + +def test_empty_networks_is_rejected(): + config = {**_VALID_BRIDGE_CONFIG, "networks": []} + with pytest.raises(Exception): + QemuConfigModel.model_validate(config) + + +def test_invalid_ip_address_is_rejected(): + config = { + **_VALID_BRIDGE_CONFIG, + "networks": [{"name": "tap0", "ip_address": "not-an-ip", "gateway": "169.254.21.88"}], + } + with pytest.raises(Exception): + QemuConfigModel.model_validate(config) + + +def test_invalid_ssh_port_is_rejected(): + config = {**_VALID_BRIDGE_CONFIG, "ssh_port": 0} + with pytest.raises(Exception): + QemuConfigModel.model_validate(config) + + +def test_invalid_ram_size_is_rejected(): + config = {**_VALID_BRIDGE_CONFIG, "qemu_ram_size": "1GB"} + with pytest.raises(Exception): + QemuConfigModel.model_validate(config) + + +def test_unknown_keys_are_rejected(): + config = {**_VALID_BRIDGE_CONFIG, "unknown_key": "value"} + with pytest.raises(Exception): + QemuConfigModel.model_validate(config) From fb0b3a6fc9e6a0a20cb17007607a939431355cce Mon Sep 17 00:00:00 2001 From: Chris Langhans Date: Mon, 27 Apr 2026 14:49:02 +0200 Subject: [PATCH 4/4] refactor(qemu): extract :config as separate Bazel target Split //score/itf/plugins/qemu into :config (config.py + pydantic only) and :qemu (rest of plugin, depends on :config). Unit tests depend on :config to avoid pulling in qemu infrastructure as coverage denominator. --- score/itf/plugins/qemu/BUILD | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/score/itf/plugins/qemu/BUILD b/score/itf/plugins/qemu/BUILD index 2f4df1c..52b42f0 100644 --- a/score/itf/plugins/qemu/BUILD +++ b/score/itf/plugins/qemu/BUILD @@ -14,12 +14,19 @@ load("@itf_pip//:requirements.bzl", "requirement") load("@rules_python//python:defs.bzl", "py_library") +py_library( + name = "config", + srcs = ["config.py"], + imports = ["."], + visibility = ["//visibility:public"], + deps = [requirement("pydantic")], +) + py_library( name = "qemu", srcs = [ "__init__.py", "checks.py", - "config.py", "qemu.py", "qemu_process.py", "qemu_target.py", @@ -27,8 +34,8 @@ py_library( imports = ["."], visibility = ["//visibility:public"], deps = [ + ":config", "//score/itf/core/process", "//score/itf/core/utils", - requirement("pydantic"), ], )