diff --git a/MODULE.bazel b/MODULE.bazel index a278ee1d..5807b520 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -65,6 +65,8 @@ bazel_dep(name = "boost.container", version = "1.87.0") bazel_dep(name = "boost.interprocess", version = "1.87.0") bazel_dep(name = "download_utils", version = "1.2.2") bazel_dep(name = "rules_cc", version = "0.2.16") +bazel_dep(name = "rules_pkg", version = "1.1.0") +bazel_dep(name = "score_toolchains_qnx", version = "0.0.7") ## Custom Module Loading @@ -93,6 +95,14 @@ python.toolchain( python_version = PYTHON_VERSION, ) +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True) +pip.parse( + hub_name = "score_baselibs_pip", + python_version = PYTHON_VERSION, + requirements_lock = "//:requirements_lock.txt", +) +use_repo(pip, "score_baselibs_pip") + bazel_dep(name = "aspect_rules_py", version = "1.6.3", dev_dependency = True) bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2", dev_dependency = True) bazel_dep(name = "aspect_rules_lint", version = "1.5.3", dev_dependency = True) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 19644ff5..f3662fee 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -62,10 +62,13 @@ "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", - "https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc", + "https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e", + "https://bcr.bazel.build/modules/bazel_features/1.34.0/source.json": "dfa5c4b01110313153b484a735764d247fee5624bbab63d25289e43b151a657a", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0/source.json": "895f21909c6fba01d7c17914bb6c8e135982275a1b18cdaa4e62272217ef1751", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", @@ -125,6 +128,8 @@ "https://bcr.bazel.build/modules/download_utils/1.0.1/MODULE.bazel": "f1d0afade59e37de978506d6bbf08d7fe5f94964e86944aaf58efcead827b41b", "https://bcr.bazel.build/modules/download_utils/1.2.2/MODULE.bazel": "7d185ec9dd3c5ee277f269e3a8e5f09b9de4cb7ba34d06b93dce9bf41c1279f8", "https://bcr.bazel.build/modules/download_utils/1.2.2/source.json": "c88be2bc48c98371d35665b805f307a647c98c83327345c918d9088822d77928", + "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", + "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2", "https://bcr.bazel.build/modules/gazelle/0.27.0/MODULE.bazel": "3446abd608295de6d90b4a8a118ed64a9ce11dcb3dda2dc3290a22056bd20996", "https://bcr.bazel.build/modules/gazelle/0.30.0/MODULE.bazel": "f888a1effe338491f35f0e0e85003b47bb9d8295ccba73c37e07702d8d31c65b", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", @@ -299,7 +304,8 @@ "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", "https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", - "https://bcr.bazel.build/modules/tar.bzl/0.2.1/source.json": "600ac6ff61744667a439e7b814ae59c1f29632c3984fccf8000c64c9db8d7bb6", + "https://bcr.bazel.build/modules/tar.bzl/0.7.0/MODULE.bazel": "cc1acd85da33c80e430b65219a620d54d114628df24a618c3a5fa0b65e988da9", + "https://bcr.bazel.build/modules/tar.bzl/0.7.0/source.json": "9becb80306f42d4810bfa16379fb48aad0b01ce5342bc12fe47dcd6af3ac4d7a", "https://bcr.bazel.build/modules/toolchain_utils/1.0.2/MODULE.bazel": "9b8be503a4fcfd3b8b952525bff0869177a5234d5c35dc3e566b9f5ca2f755a1", "https://bcr.bazel.build/modules/toolchain_utils/1.0.2/source.json": "88769ec576dddacafd8cca4631812cf8eead89f10a29d9405d9f7a553de6bf87", "https://bcr.bazel.build/modules/toolchains_llvm/1.4.0/MODULE.bazel": "05239402b7374293359c2f22806f420b75aa5d6f4b15a2eaa809a2c214d58b31", @@ -367,9 +373,11 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.27.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.28.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.30.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.34.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.4.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.9.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.9.1/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_lib/3.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_skylib/1.0.3/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_skylib/1.1.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_skylib/1.2.0/MODULE.bazel": "not found", @@ -407,6 +415,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/buildozer/7.1.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/download_utils/1.0.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/download_utils/1.2.2/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/gazelle/0.27.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/gazelle/0.30.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/google_benchmark/1.8.2/MODULE.bazel": "not found", @@ -545,6 +554,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_cpp_toolchains/0.2.2/MODULE.bazel": "343a1892b1d5c616e0b4cbecfb5e548fa69328d22bb4fd5862bdd3cfa902142b", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_cpp_toolchains/0.2.2/source.json": "624c1addd22fff7fc894d0571d35c8e47cc2d3ff9e75b15b8fb1cff021391a30", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/0.0.2/MODULE.bazel": "32f0cbc08bb1c60279448d666aead6b5a000374a8a67f08822b258bf00a6a183", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/0.0.3/MODULE.bazel": "14c96e378c08705a46abe0799d6236fe3095c342c34f83f8d1b3f6046ce00651", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/0.0.4/MODULE.bazel": "57dce05e4eb4ac25250d67e47caef99f4cbaedf38083de26aa53979ee6a3ae4b", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/0.0.4/source.json": "28caa2c12a50a72ea75494113d0e4bdb559a9324543c930665f5968b87fce134", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_tools_cc/0.1.0/MODULE.bazel": "e859eef20dc2f08a58586197cc59f721c640dd8d49bdc820a584d577d33d82a6", @@ -553,6 +563,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_tools_python/0.1.3/source.json": "f87907b20372ca2c4ad15719cba03423577ed1ea8392c7373eb37f73499decce", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_cr_checker/0.2.2/MODULE.bazel": "dc36d9c35543db918c3fb5b93a8e684431f56c7c784cf2a1b90f35802a373c98", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_cr_checker/0.3.1/MODULE.bazel": "f49e037d7fbc0b2a8b2734fc6b47334e8cc8589ca7a5aa0f3ccca85cc5f79fac", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_cr_checker/0.3.1/source.json": "ad038d99c0e2a59cca3a7fa1aac6d87cd0d752314b65b52a91451ab0bd0f7171", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_dash_license_checker/0.1.1/MODULE.bazel": "76681dbd2d45b5c540869a2337174086c56c54953aab1d02cd878b59d31d13a5", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/0.2.4/MODULE.bazel": "ea4801e96c87e2b8650a0fa9e5fed9b8bdbef05c1bc3e30003ba527d5af60a43", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/0.2.6/MODULE.bazel": "1af2963e91c6472555e222f0aba3dc2f5492d04598298209a361978ee3e321e3", @@ -579,8 +590,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.3.1/MODULE.bazel": "29666e38fbc76eddd6676e594f225e474d130dce9c3a9d224e59ae7a499c4575", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.3.2/MODULE.bazel": "a32390ef217cef9a811408b0a1c5aeed1398c377aa846f5d5416d7b95b4e4366", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.4.2/MODULE.bazel": "7593d62baf500c4e40bf0758f2515b5df016e177b889d11ca964818821ebe505", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.4.3/MODULE.bazel": "eb8243299a6ae3663618db5b4718e2c0d3c93f91a44994641e70106c400b23fb", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.4.3/source.json": "6dfc6ded50c6e278a8e619837f5cd323c2d740d22f913a5c06cf17b5e9d9eea0", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.4.4/MODULE.bazel": "faf38c6a91becd6df4c7ede661353f239b38789f0011c19eb9d6b850e97abfe9", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.4.4/source.json": "ce591847cd2f2bf2deb951da28ee7360574611f688611d34981b46fd533bfd94", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_python_basics/0.3.0/MODULE.bazel": "785ddd5295213e36c31ab86bdc34f29c0f7d1b72e9abd931bb08f42c0e48e2e9", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_python_basics/0.3.1/MODULE.bazel": "99c491109937542e61df090222666a8613ef946fa7bb2b2d5ba648b2baba03ad", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_python_basics/0.3.2/MODULE.bazel": "f25490f64035a0e3a0d53ad9cb6164e8325ce6cf2a7ee68c6ae153840cb2497e", @@ -588,6 +599,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rust_policies/0.0.2/MODULE.bazel": "ade2bad4a331b02d9b7e7d9842e8de8c6fded6186486e02c4f7db5cd4b71d34d", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rust_policies/0.0.2/source.json": "fbcbc738e652b0c68d5d28dd1db09f2e643dc111f5739b2f6af7ec56c2e88043", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_starpls_lsp/0.1.0/MODULE.bazel": "b2f8c4c8d8e851706255ff9002b448bff6e040b8f0c6adedbde2a09375aa16cc", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_toolchains_qnx/0.0.7/MODULE.bazel": "2ae1df18803227f4edce7dfa1529ace724f60d9f6d048e20cd3aea1ae1429ba9", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_toolchains_qnx/0.0.7/source.json": "31864e9f2009cab9ebb50c1afe66f65c74a905e955defe76eb1f4a5ccf12f3bd", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_tooling/1.0.2/MODULE.bazel": "e70f396375b9d612b4f41ebceff7f18f68ab423b14625c138a354cc01bc62a10", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_tooling/1.1.0/MODULE.bazel": "5a04a5ce3512eb742a036600fba58b465f427e2e193db8e88857132e4a4eb513", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_tooling/1.1.0/source.json": "3ed8a7328d0edc1fe655f981893c646559f3f89a306d18e036309f6bc9846c98", @@ -601,6 +614,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/stardoc/0.7.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/stardoc/0.7.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/tar.bzl/0.2.1/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/tar.bzl/0.7.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/toolchain_utils/1.0.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/toolchains_llvm/1.4.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/toolchains_protoc/0.2.1/MODULE.bazel": "not found", diff --git a/quality/qnx_unit_testing/BUILD b/quality/qnx_unit_testing/BUILD new file mode 100644 index 00000000..03b3bbff --- /dev/null +++ b/quality/qnx_unit_testing/BUILD @@ -0,0 +1,122 @@ +# ******************************************************************************* +# 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("@bazel_skylib//rules:expand_template.bzl", "expand_template") +load("@score_baselibs_pip//:requirements.bzl", "requirement") +load("@score_toolchains_qnx//rules/fs:ifs.bzl", "qnx_ifs") +load("//quality/qnx_unit_testing/environments:run_as_exec.bzl", "run_as_exec") + +# Export Bazel macros and shell scripts for QNX unit testing +exports_files( + [ + "cc_test_qnx.bzl", + "py_unittest_qnx_test.bzl", + "run_qemu_shell.sh", + "run_qemu.sh", + "configure_persistency.sh", + "run_test.sh", + "startup.sh", + "tools.build", + "init_x86_64_cc_test.build.template", + ], + visibility = ["//visibility:public"], +) + +# Create empty FAT32 filesystem image for test results (10MB) +genrule( + name = "test_results", + outs = ["test_results.img"], + cmd = """ + # Create 10MB empty file for FAT32 test result exchange + dd if=/dev/zero of=$@ bs=1M count=10 2>/dev/null + """, + tags = ["manual"], + visibility = ["//visibility:public"], +) + +# Create empty QNX6 filesystem image for persistent storage (10MB) +genrule( + name = "persistent", + outs = ["persistent.img"], + cmd = """ + # Create 10MB empty file for QNX6 persistent storage + dd if=/dev/zero of=$@ bs=1M count=10 2>/dev/null + """, + tags = ["manual"], + visibility = ["//visibility:public"], +) + +# Generate IFS build file for C++ unit tests (runs test binary) +expand_template( + name = "init_build_cc_test", + out = "init_cc_test.build", + substitutions = { + "{RUN_BINARY}": "run_test.sh", + }, + template = ":init_x86_64_cc_test.build.template", + visibility = ["//visibility:public"], +) + +# Generate IFS build file for interactive shell (debugging) +expand_template( + name = "init_build_shell", + out = "init_shell.build", + substitutions = { + "{RUN_BINARY}": "[+session] /bin/sh &", + }, + template = ":init_x86_64_cc_test.build.template", + visibility = ["//visibility:public"], +) + +# QNX bootable image for interactive shell (debugging) +qnx_ifs( + name = "init_shell", + testonly = True, + srcs = [ + ":configure_persistency.sh", + ":run_test.sh", + ":startup.sh", + ":tools.build", + ], + build_file = ":init_build_shell", + ext_repo_maping = { + "CONFIGURE_PERSISTENCY_SCRIPT": "$(location :configure_persistency.sh)", + "RUN_TEST_SCRIPT": "$(location :run_test.sh)", + "STARTUP_SCRIPT": "$(location :startup.sh)", + "TOOLS_BUILDFILE": "$(location :tools.build)", + }, + tags = ["manual"], + target_compatible_with = ["@platforms//os:qnx"], + visibility = ["//visibility:public"], +) + +# Python script to parse test results from FAT32 image +py_binary( + name = "process_test_results_bin", + srcs = ["process_test_results.py"], + main = "process_test_results.py", + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [ + requirement("pyfatfs"), + requirement("fs"), + ], +) + +# Wrap py_binary with run_as_exec to force execution platform (Linux) +# even when building with --config=qnx_x86_64 (which sets target platform to QNX) +run_as_exec( + name = "process_test_results", + executable = ":process_test_results_bin", + visibility = ["//visibility:public"], +) diff --git a/quality/qnx_unit_testing/cc_test_qnx.bzl b/quality/qnx_unit_testing/cc_test_qnx.bzl new file mode 100644 index 00000000..fcef539a --- /dev/null +++ b/quality/qnx_unit_testing/cc_test_qnx.bzl @@ -0,0 +1,195 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +QNX C++ Unit Test Macro for SCORE Integration Testing Framework + +This macro wraps cc_test targets to run them on QNX 8.0 in QEMU using the +SCORE integration testing framework's QEMURunner infrastructure. + +It creates a bootable QNX image containing the test binary and executes it +using Python-based QEMU management (replacing the old shell-based approach). + +## Architecture: + +1. **qnx_ifs target**: Creates QNX bootable .ifs image + - Marked with target_compatible_with = QNX + - Built with QNX cross-compilation toolchain (--config=qnx_x86_64) + +2. **py_test wrapper**: Runs on Linux host + - Uses QEMURunner from quality/integration_testing/environments/qnx8_qemu/ + - Launches QEMU, executes tests, parses results from FAT32 image + - No target_compatible_with (runs on execution platform) + +This consolidates QEMU logic into a single Python-based implementation, +eliminating the previous duplication between shell scripts and Python. +""" + +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("@score_toolchains_qnx//rules/fs:ifs.bzl", "qnx_ifs") + +def _get_test_and_data_impl(ctx): + """Extract test binary and data files (excluding .so libraries)""" + data_files = [] + for file in ctx.attr.src[DefaultInfo].data_runfiles.files.to_list(): + if not file.basename.endswith(".so"): + data_files.append(file) + return [DefaultInfo(files = ctx.attr.src[DefaultInfo].files, runfiles = ctx.runfiles(files = data_files))] + +_get_test_and_data = rule( + implementation = _get_test_and_data_impl, + attrs = { + "src": attr.label(providers = [CcInfo]), + }, +) + +def cc_test_qnx(name, cc_test, excluded_tests_filter = None, app_tar = None): + """Compile and run C++ unit tests on QNX 8.0 in QEMU + + This macro uses the SCORE integration testing framework's QEMURunner + infrastructure to execute C++ tests in a QNX virtual machine. + + Args: + name: Test name + cc_test: cc_test target to execute in QNX + excluded_tests_filter: list of GTest filters to exclude from execution. + Examples: + ["FooTest.Test1"] - do not run Test1 from test suite FooTest + ["FooTest.*"] - do not run any test from test suite FooTest + ["*FooTest.*"] - runs all non-FooTest tests + app_tar: optional pkg_tar target containing application binaries to include + in the QNX image. Similar to integration_test's filesystem parameter. + The tar will be extracted to /app directory in the QNX filesystem. + """ + excluded_tests_filter = excluded_tests_filter if excluded_tests_filter else [] + + # Convert filter list to GTest format: "-FooTest.Test1:BarTest.*" + excluded_tests_filter_str = "-" + for test_filter in excluded_tests_filter: + excluded_tests_filter_str = excluded_tests_filter_str + (test_filter + ":\\") + + native.genrule( + name = "{}_excluded_tests_filter".format(name), + cmd_bash = """ + echo {} > $(@) + """.format(excluded_tests_filter_str), + testonly = True, + tags = ["manual"], + outs = ["{}_excluded_tests_filter.txt".format(name)], + ) + + _get_test_and_data( + name = "%s_test_and_data" % name, + src = cc_test, + testonly = True, + tags = ["manual"], + ) + + pkg_files( + name = "%s_test_and_runfiles" % name, + srcs = [ + ":{}_excluded_tests_filter".format(name), + ":{}_test_and_data".format(name), + ], + include_runfiles = True, + prefix = "/tests", + testonly = True, + tags = ["manual"], + attributes = pkg_attributes(mode = "0755"), + ) + + pkg_tar( + name = "%s_pkgtar" % name, + srcs = [ + "%s_test_and_runfiles" % name, + ], + testonly = True, + tags = ["manual"], + symlinks = { + "/tests/cc_test_qnx": native.package_relative_label(cc_test).name, + "/tests/cc_test_qnx_filters.txt": "{}_excluded_tests_filter.txt".format(name), + }, + modes = { + "/tests/cc_test_qnx": "0777", + "/tests/cc_test_qnx_filters.txt": "0777", + }, + ) + + # Create QNX bootable image with test package + # Uses tars parameter (like integration_test) instead of DUI + # Optionally includes app_tar if provided (similar to integration_test's filesystem) + qnx_ifs( + name = "%s_test_img" % name, + out = "{}_test.ifs".format(name), + build_file = "//quality/qnx_unit_testing:init_build_cc_test", + tars = {"TESTS": ":%s_pkgtar" % name} | ({"APP": app_tar} if app_tar else {}), + testonly = True, + target_compatible_with = [ + "@platforms//os:qnx", + ], + tags = [ + "manual", + ], + ) + + # Shell test wrapper (temporary - keeps old sh_test approach for compatibility) + # TODO: Migrate to py_test + QEMURunner once platform constraints are resolved + native.sh_test( + name = name, + srcs = ["//quality/qnx_unit_testing:run_qemu.sh"], + args = [ + "$(location //quality/qnx_unit_testing:init_shell)", + "$(location //quality/qnx_unit_testing:persistent)", + "$(location //quality/qnx_unit_testing:process_test_results)", + "$(location //quality/qnx_unit_testing:test_results)", + "$(location :%s_test_img)" % name, + ], + data = [ + ":%s_test_img" % name, + "//quality/qnx_unit_testing:init_shell", + "//quality/qnx_unit_testing:persistent", + "//quality/qnx_unit_testing:process_test_results", + "//quality/qnx_unit_testing:test_results", + ], + timeout = "short", + size = "medium", + # Note: sh_test runs on Linux host (launches QEMU), so no target_compatible_with + # The QNX IFS image (qnx_ifs) has proper platform constraints + tags = [ + "cpu:2", + "manual", + "qnx_unit_test", + ], + ) + + # Shell access for debugging (kept for compatibility) + native.sh_binary( + name = "%s_shell" % name, + srcs = ["//quality/qnx_unit_testing:run_qemu_shell.sh"], + args = [ + "$(location //quality/qnx_unit_testing:init_shell)", + "$(location //quality/qnx_unit_testing:persistent)", + "$(location //quality/qnx_unit_testing:test_results)", + "$(location :%s_test_img)" % name, + ], + data = [ + ":%s_test_img" % name, + "//quality/qnx_unit_testing:init_shell", + "//quality/qnx_unit_testing:persistent", + "//quality/qnx_unit_testing:test_results", + ], + testonly = True, + # Note: sh_binary runs on Linux host (launches QEMU), so no target_compatible_with + tags = ["manual"], + ) diff --git a/quality/qnx_unit_testing/configure_persistency.sh b/quality/qnx_unit_testing/configure_persistency.sh new file mode 100755 index 00000000..9775621b --- /dev/null +++ b/quality/qnx_unit_testing/configure_persistency.sh @@ -0,0 +1,165 @@ +#!/bin/sh + +set -eu + +PERSISTENT_PARTITION="/dev/hd1" +PERSISTENT_PARTITION_MOUNT_POINT="/qnx6fs_persistent" +PERSISTENT_ENCRYPTION_MOUNT_POINT=/persistent +QEMU=1 + +ENC_DOMAIN=3 +ENC_TYPE=1 + +BackupEncrypted="true" + +mount_qnx6fs_with_crypto() { + echo "CONFIGURE_PERSISTENCY: mount_qnx6fs_with_crypto()" + if mount | grep "$PERSISTENT_PARTITION_MOUNT_POINT"; then + echo "CONFIGURE_PERSISTENCY: $PERSISTENT_PARTITION_MOUNT_POINT already mounted, returning" + else + echo "CONFIGURE_PERSISTENCY: $PERSISTENT_PARTITION_MOUNT_POINT not mounted, hence mounting" + mount -t qnx6 -ocrypto=openssl "$PERSISTENT_PARTITION" "$PERSISTENT_PARTITION_MOUNT_POINT" + fi +} + +create_links(){ + echo "CONFIGURE_PERSISTENCY: create_links()" + #create needed folders + mkdir -p $PERSISTENT_PARTITION_MOUNT_POINT/persistent + if [ ! -h $PERSISTENT_ENCRYPTION_MOUNT_POINT ]; then + ln -s -P $PERSISTENT_PARTITION_MOUNT_POINT/persistent $PERSISTENT_ENCRYPTION_MOUNT_POINT + fi +} + +format_qnx6_then_mount() { + echo "CONFIGURE_PERSISTENCY: format_qnx6_then_mount(): check if PER already mounted, if so unmount it before formatting!" + if mount | grep "$PERSISTENT_PARTITION_MOUNT_POINT"; then + echo "CONFIGURE_PERSISTENCY: $PERSISTENT_PARTITION_MOUNT_POINT already mounted, need to umount before formatting!" + umount "$PERSISTENT_PARTITION_MOUNT_POINT" + fi + + echo "CONFIGURE_PERSISTENCY: start formatting qnx6fs" + if mkqnx6fs "$PERSISTENT_PARTITION" -q; then + echo "CONFIGURE_PERSISTENCY: formatting done" + if [ -h $PERSISTENT_ENCRYPTION_MOUNT_POINT ]; then + echo "CONFIGURE_PERSISTENCY: remove $PERSISTENT_ENCRYPTION_MOUNT_POINT" + rm $PERSISTENT_ENCRYPTION_MOUNT_POINT + fi + if ! mount_qnx6fs_with_crypto; then + echo "CONFIGURE_PERSISTENCY: mounting failed, returning" + return 1 + fi + fi + echo "CONFIGURE_PERSISTENCY: $PERSISTENT_PARTITION formatted and mounted now" +} + +validate_encryption_status() { + echo "CONFIGURE_PERSISTENCY: validate_encryption_status()" + + #mount in order to check if it is already encrypted + if ! mount_qnx6fs_with_crypto; then + echo "CONFIGURE_PERSISTENCY: mounting failed, returning" + return 1 + fi + + #check the encryption status after the partition mounting + query=$(fsencrypt -v -p "$PERSISTENT_PARTITION_MOUNT_POINT" -c query-all) + domain_number=$(echo $query | awk -F' ' '{print $1}') + domain_status=$(echo $query | awk -F' ' '{print $2}') + echo "CONFIGURE_PERSISTENCY: domain_number: $domain_number" + echo "CONFIGURE_PERSISTENCY: domain_status: $domain_status" + if [[ -n "$domain_number" && -n "$domain_status" && "$domain_number" == "$ENC_DOMAIN" && ("$domain_status" == "UNLOCKED" || "$domain_status" == "LOCKED") ]]; then + echo "CONFIGURE_PERSISTENCY: $PERSISTENT_PARTITION_MOUNT_POINT already encrypted" + return 0 + else + echo "CONFIGURE_PERSISTENCY: $PERSISTENT_PARTITION_MOUNT_POINT not yet encrypted" + return 1 + fi +} + +encrypt_persistent() { + echo "CONFIGURE_PERSISTENCY: encrypt_persistent()" + + create_links + + key="B8428D25261B7E04F1311571BA0EA3EAEDA50ABC9AEE91B1355EF8065B75B227B8428D25261B7E04F1311571BA0EA3EAEDA50ABC9AEE91B1355EF8065B75B227" #hard code key for Qemu + + echo "CONFIGURE_PERSISTENCY: Creating the encryption domain $ENC_DOMAIN" + keybase64="$(echo -n $key | openssl base64 -A)" + + if fsencrypt -p "$PERSISTENT_PARTITION_MOUNT_POINT" -d $ENC_DOMAIN -c create -t $ENC_TYPE -k "#$keybase64"; then + echo "CONFIGURE_PERSISTENCY: Encryption domain $ENC_DOMAIN has been created" + else + echo "CONFIGURE_PERSISTENCY: Something went wrong during encryption domain creation" >&2 + exit 1 + fi + + echo "CONFIGURE_PERSISTENCY: Setting the encryption domain $ENC_DOMAIN" + if fsencrypt -p "$PERSISTENT_PARTITION_MOUNT_POINT/persistent" -d $ENC_DOMAIN -c set; then + echo "CONFIGURE_PERSISTENCY: Encryption domain $ENC_DOMAIN has successfully been set" + else + echo "CONFIGURE_PERSISTENCY: Something went wrong during seting the encryption domain" >&2 + exit 1 + fi + + echo "CONFIGURE_PERSISTENCY: encrypt_persistent() done" +} + +unlock_persistent() { + echo "CONFIGURE_PERSISTENCY: unlock_persistent()" + + create_links + + #unlock the persistent if it is locked + query=$(fsencrypt -v -p "$PERSISTENT_PARTITION_MOUNT_POINT" -c query-all) + domain_number=$(echo $query | awk -F' ' '{print $1}') + domain_status=$(echo $query | awk -F' ' '{print $2}') + echo "CONFIGURE_PERSISTENCY: domain_number: $domain_number" + echo "CONFIGURE_PERSISTENCY: domain_status: $domain_status" + if [ -n "$domain_number" ] && [ -n "$domain_status" ] && [ "$domain_number" == "$ENC_DOMAIN" ] && [ "$domain_status" == "LOCKED" ] && [ "$domain_status" != "UNLOCKED" ]; then + echo "CONFIGURE_PERSISTENCY: Unlocking the encryption domain $ENC_DOMAIN" + + key="B8428D25261B7E04F1311571BA0EA3EAEDA50ABC9AEE91B1355EF8065B75B227B8428D25261B7E04F1311571BA0EA3EAEDA50ABC9AEE91B1355EF8065B75B227" #hard code some random key for Qemu + echo "CONFIGURE_PERSISTENCY: key available, start unlocking encryption domain" + + echo "CONFIGURE_PERSISTENCY: Unlocking the encryption domain $ENC_DOMAIN" + keybase64="$(echo -n $key | openssl base64 -A)" + + if fsencrypt -p "$PERSISTENT_PARTITION_MOUNT_POINT" -d $ENC_DOMAIN -c unlock -t $ENC_TYPE -k "#$keybase64"; then + echo "CONFIGURE_PERSISTENCY: Encryption domain $ENC_DOMAIN has successfully been unlocked" + else + echo "CONFIGURE_PERSISTENCY: Something went wrong during unlocking the encryption domain" >&2 + return 1 + fi + + fi + echo "CONFIGURE_PERSISTENCY: return from unlock_persistent()" + return 0 +} + +format_mount_encrypt_restore(){ + echo "CONFIGURE_PERSISTENCY: format_mount_encrypt_restore()" + format_qnx6_then_mount + encrypt_persistent +} + +main() { + if validate_encryption_status; then + echo "CONFIGURE_PERSISTENCY: persistent encrypted path" #persistent encrypted + if ! mount_qnx6fs_with_crypto; then + format_mount_encrypt_restore + else + if ! unlock_persistent; then + echo "CONFIGURE_PERSISTENCY: could not unlock persistent" + format_mount_encrypt_restore + fi + fi + else + echo "CONFIGURE_PERSISTENCY: persistent unencrypted path" #persistent unencrypted (also migration from Linux) + format_mount_encrypt_restore + fi + + echo "CONFIGURE_PERSISTENCY: Persistency configured successfully!" +} + +main diff --git a/quality/qnx_unit_testing/environments/BUILD b/quality/qnx_unit_testing/environments/BUILD new file mode 100644 index 00000000..ace952e5 --- /dev/null +++ b/quality/qnx_unit_testing/environments/BUILD @@ -0,0 +1,14 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +exports_files(["run_as_exec.bzl"]) diff --git a/quality/qnx_unit_testing/environments/run_as_exec.bzl b/quality/qnx_unit_testing/environments/run_as_exec.bzl new file mode 100644 index 00000000..c829deb3 --- /dev/null +++ b/quality/qnx_unit_testing/environments/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/quality/qnx_unit_testing/init_x86_64_cc_test.build.template b/quality/qnx_unit_testing/init_x86_64_cc_test.build.template new file mode 100644 index 00000000..67aab5c9 --- /dev/null +++ b/quality/qnx_unit_testing/init_x86_64_cc_test.build.template @@ -0,0 +1,75 @@ +############################################################################### +# +# QNX 8.0 Unit Test Image for x86_64 +# +############################################################################### + +[-optional +autolink] + +[image=0x3600000] +[virtual=x86_64,multiboot] boot = { + startup-x86 -v -D8250..115200 + PATH=/proc/boot + LD_LIBRARY_PATH=/proc/boot + procnto-smp-instr +} + +[+script] startup-script = { + procmgr_symlink /dev/shmem /tmp + + display_msg Welcome to QNX OS 8.0 on x86_64 for SCORE Unit Tests + + # These env variables get inherited by all programs which follow + SYSNAME=nto + TERM=qansi + + devc-ser8250 & + waitfor /dev/ser1 + reopen /dev/ser1 + + startup.sh + + {RUN_BINARY} +} + +# Instead of [+include], inline the essential tools directly +# dynamic loader +/usr/lib/ldqnx-64.so.2=ldqnx-64.so.2 + +# shared libraries (QNX 8.0 x86_64 - minimal set) +cam-disk.so +fs-qnx6.so +io-blk.so +libc++.so.2 +libc.so +libc.so.6 +libcam.so.2 +libgcc_s.so.1 +libm.so +libsecpol.so.1 +libz.so + +# essential tools +devc-ser8250 +ksh +pipe +slogger2 +toybox + +# links +[type=link] /bin/sh=/proc/boot/ksh +[type=link] cat=toybox +[type=link] echo=toybox +[type=link] ls=toybox + +# Custom scripts +startup.sh=${STARTUP_SCRIPT} +run_test.sh=${RUN_TEST_SCRIPT} +configure_persistency.sh=${CONFIGURE_PERSISTENCY_SCRIPT} + +# Include test package filesystem +/=${TESTS} + +# Include application binaries (if provided via app_tar parameter) +# This line is optional - mkifs will ignore it if ${APP} is not defined in tars dictionary +[-optional] /app=${APP} diff --git a/quality/qnx_unit_testing/process_test_results.py b/quality/qnx_unit_testing/process_test_results.py new file mode 100755 index 00000000..96c344eb --- /dev/null +++ b/quality/qnx_unit_testing/process_test_results.py @@ -0,0 +1,32 @@ +import os +import sys +import tarfile +from fs import open_fs + + +def check_tests_result() -> int: + shared_img_path = sys.argv[1] + shared_img = open_fs("fat://" + shared_img_path + "?preserve_case=true") + + output_xml = os.environ.get("XML_OUTPUT_FILE") + output_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + coverage = os.environ.get("COVERAGE", "0") == "1" + + if shared_img.exists("test.xml"): + with open(output_xml, "wb") as file: + shared_img.download("test.xml", file) + + if coverage: + coverage_archive = output_dir + "/coverage.tar.gz" + if shared_img.exists("coverage.tar.gz"): + with open(coverage_archive, "wb") as file: + shared_img.download("coverage.tar.gz", file) + + with tarfile.open(coverage_archive) as tf: + tf.extractall(output_dir) + + return int(shared_img.readtext("returncode.log")) + + +if __name__ == "__main__": + sys.exit(check_tests_result()) diff --git a/quality/qnx_unit_testing/run_qemu.sh b/quality/qnx_unit_testing/run_qemu.sh new file mode 100755 index 00000000..9b7e6f44 --- /dev/null +++ b/quality/qnx_unit_testing/run_qemu.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +set -euo pipefail + +IFS_IMAGE=$1 +PERSISTENT_IMAGE=$2 +PROCESS_TESTS_RESULTS=$3 +TEST_RESULT_IMAGE=$4 +TEST_IMAGE=$5 + +writable_img=$(mktemp) +cp "${TEST_RESULT_IMAGE}" "${writable_img}" + +writable_img_persistent=$(mktemp) +cp "${PERSISTENT_IMAGE}" "${writable_img_persistent}" + +ACCEL="-cpu qemu64,+cx16,+lahf-lm,+popcnt,+pni,+sse4.1,+sse4.2,+ssse3,+avx,+avx2,+bmi1,+bmi2,+f16c,+fma,+movbe" + +DISABLE_KVM="${DISABLE_KVM:-0}" + +if [[ -e /dev/kvm && -r /dev/kvm ]]; then + echo "KVM supported!" + + if [[ "${DISABLE_KVM}" == 0 ]]; then + ACCEL="-enable-kvm -cpu host,-xsave" + else + echo "KVM explicitly disabled!" + fi +fi + +qemu-system-x86_64 \ + -smp 2 \ + -m 2G \ + ${ACCEL} \ + -nographic \ + -kernel "${IFS_IMAGE}" \ + -serial mon:stdio \ + -no-reboot \ + -object rng-random,filename=/dev/urandom,id=rng0 \ + -device virtio-rng-pci,rng=rng0 \ + -drive file="${writable_img}",if=virtio,format=raw,index=0,media=disk \ + -drive file="${writable_img_persistent}",if=virtio,format=raw,index=1,media=disk \ + -drive file="${TEST_IMAGE}",if=virtio,format=raw,index=2,media=disk,readonly=on \ + 2>&1 | sed 's/[^[:print:]]//g' | sed 's/\r//' + +${PROCESS_TESTS_RESULTS} "${writable_img}" +return_code=$? + +rm -rf "${writable_img}" +rm -rf "${writable_img_persistent}" + +exit "${return_code}" diff --git a/quality/qnx_unit_testing/run_qemu_shell.sh b/quality/qnx_unit_testing/run_qemu_shell.sh new file mode 100755 index 00000000..d14784d1 --- /dev/null +++ b/quality/qnx_unit_testing/run_qemu_shell.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +set -euo pipefail + +IFS_IMAGE=$1 +PERSISTENT_IMAGE=$2 +TEST_RESULT_IMAGE=$3 +TEST_IMAGE=$4 + +writable_img=$(mktemp) +cp "${TEST_RESULT_IMAGE}" "${writable_img}" + +writable_img_persistent=$(mktemp) +cp "${PERSISTENT_IMAGE}" "${writable_img_persistent}" + +DEBUG_PORT="" +if [ $# -eq 6 ]; then + if [ "$5" == "--debug-port" ]; then + DEBUG_PORT="$6" + else + echo "ERROR: Unknown argument '$5'" + exit 1 + fi +fi + +NETWORK="" +if [ ! -z "${DEBUG_PORT}" ]; then + echo "WARNING: Debugging enabled on port ${DEBUG_PORT}" + NETWORK="-netdev user,id=net0,hostfwd=tcp:127.0.0.1:${DEBUG_PORT}-10.0.2.15:38080 -device e1000,netdev=net0" +fi + +ACCEL="-cpu qemu64,+cx16,+lahf-lm,+popcnt,+pni,+sse4.1,+sse4.2,+ssse3,+avx,+avx2,+bmi1,+bmi2,+f16c,+fma,+movbe" + +DISABLE_KVM="${DISABLE_KVM:-0}" + +if [[ -e /dev/kvm && -r /dev/kvm ]]; then + echo "KVM supported!" + + if [[ "${DISABLE_KVM}" == 0 ]]; then + ACCEL="-enable-kvm -cpu host,-xsave" + else + echo "KVM explicitly disabled!" + fi +fi + +qemu-system-x86_64 \ + -smp 2 \ + -m 2G \ + ${ACCEL} \ + -nographic \ + -kernel "${IFS_IMAGE}" \ + -serial mon:stdio \ + -object rng-random,filename=/dev/urandom,id=rng0 \ + -device virtio-rng-pci,rng=rng0 \ + ${NETWORK} \ + -drive file="${writable_img}",if=virtio,format=raw,index=0,media=disk \ + -drive file="${writable_img_persistent}",if=virtio,format=raw,index=1,media=disk \ + -drive file="${TEST_IMAGE}",if=virtio,format=raw,index=2,media=disk,readonly=on diff --git a/quality/qnx_unit_testing/run_test.sh b/quality/qnx_unit_testing/run_test.sh new file mode 100755 index 00000000..18af37ae --- /dev/null +++ b/quality/qnx_unit_testing/run_test.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +export GCOV_PREFIX=/persistent/coverage +export GCOV_PREFIX_STRIP=3 + +mkdir /persistent/unit_tests + +ROOT_DIR="/opt/tests/cc_test_qnx.runfiles/" + +if [ -d "$ROOT_DIR" ]; then + if [ -d "/opt/tests/cc_test_qnx.runfiles/safe_posix_platform" ]; then + ROOT_DIR=/opt/tests/cc_test_qnx.runfiles/safe_posix_platform + elif [ -d "/opt/tests/cc_test_qnx.runfiles/_main" ]; then + ROOT_DIR=/opt/tests/cc_test_qnx.runfiles/_main + elif [ -d "/opt/tests/cc_test_qnx.runfiles/ddad" ]; then + ROOT_DIR=/opt/tests/cc_test_qnx.runfiles/ddad + fi + find ${ROOT_DIR} -maxdepth 1 -mindepth 1 -type d -exec cp -R '{}' /persistent/unit_tests/ \; +fi + +export GTEST_FILTER="$(cat /opt/tests/cc_test_qnx_filters.txt)" + +cd /persistent/unit_tests +ln -s -f /opt/tests/cc_test_qnx cc_test_qnx +/persistent/unit_tests/cc_test_qnx --gtest_output=xml:/persistent/test.xml + +echo "$?" > /persistent/returncode.log + +cp -fR /persistent/returncode.log /shared/returncode.log + +if [ -e "/persistent/test.xml" ]; then + cp -fR /persistent/test.xml /shared/test.xml +fi + +# Wait for all test processes to finish +echo "Waiting for all test processes to finish..." +while pidin -F '%a %b %n' | grep cc_test_qnx > /dev/null 2>&1; do true; done +echo "Test processes finished" + +if [ -d "/persistent/coverage" ]; then + echo "Creating coverage archive..." + time tar czf /shared/coverage.tar.gz -C /persistent/ coverage + echo "Coverage archive created!" +fi + +sync + +cd / + +umount /shared +umount /qnx6fs_persistent + +shutdown diff --git a/quality/qnx_unit_testing/startup.sh b/quality/qnx_unit_testing/startup.sh new file mode 100755 index 00000000..a390d297 --- /dev/null +++ b/quality/qnx_unit_testing/startup.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +slogger2 +waitfor /dev/slog + +pci-server --config=/proc/boot/pci_server.cfg +waitfor /dev/pci + +pipe +waitfor /dev/pipe + +random -t -p +waitfor /dev/random + +fsevmgr +waitfor /dev/fsnotify + +devb-virtio + +waitfor /dev/hd0 +while ! mount -t dos -o case /dev/hd0 /shared; do + echo "Failed to mount /shared. Retrying..." +done + +waitfor /dev/hd1 +while ! configure_persistency.sh; do + echo "failed to configure persistency. retrying..." +done + +waitfor /dev/hd2 +while ! mount_ifs -f /dev/hd2 -m /opt; do + echo "Failed to mount /opt. Retrying..." +done + +devb-ram ram capacity=1 blk ramdisk=10m,cache=512k,vnode=256 +waitfor /dev/ram0 + +mkqnx6fs -q /dev/ram0 + +mount -t qnx6 -o mntperms=777,noexec /dev/ram0 /tmp_discovery + +io-pkt-v6-hc -d e1000 name=eth +waitfor /dev/socket +if_up -p -r 200 -m 10 eth0 + +ifconfig eth0 10.0.2.15 up +if_up -l -r 200 -m 10 eth0 + +mount -Tio-pkt /proc/boot/lsm-pf-v6.so +waitfor /dev/pf +waitfor /dev/bpf + +mq +waitfor /dev/mq + +mqueue +waitfor /dev/mqueue + +devc-pty -n 32 & +pdebug 38080 diff --git a/quality/qnx_unit_testing/tools.build b/quality/qnx_unit_testing/tools.build new file mode 100644 index 00000000..7113ec42 --- /dev/null +++ b/quality/qnx_unit_testing/tools.build @@ -0,0 +1,54 @@ +# dynamic loader +/usr/lib/ldqnx-64.so.2=ldqnx-64.so.2 + +# shared libraries (QNX 8.0 x86_64) +cam-disk.so +fs-qnx6.so +io-blk.so +libc++.so.2 +libc.so +libc.so.6 +libcam.so.2 +libgcc_s.so.1 +libm.so +libqcrypto.so.1.0 +libqh.so +libregex.so.1 +libslog2.so.1 +libslog2parse.so.1 +libz.so + +# tools (absolute minimum for unit testing) +devc-ser8250 +devb-ram +ksh +mount +pipe +slogger2 +toybox + +# links +[type=link] /bin/sh=/proc/boot/ksh +[type=link] /bin/ls=/proc/boot/ls +[type=link] cat=toybox +[type=link] chmod=toybox +[type=link] cp=toybox +[type=link] dd=toybox +[type=link] echo=toybox +[type=link] grep=toybox +[type=link] ls=toybox +[type=link] rm=toybox + +# Custom scripts +startup.sh=${STARTUP_SCRIPT} +run_test.sh=${RUN_TEST_SCRIPT} +configure_persistency.sh=${CONFIGURE_PERSISTENCY_SCRIPT} + +# Minimal user/group configuration +/etc/passwd = { +root:x:0:0:Superuser:/root:/bin/sh +} + +/etc/group = { +root:x:0:root +} diff --git a/requirements.in b/requirements.in new file mode 100644 index 00000000..a4fc2147 --- /dev/null +++ b/requirements.in @@ -0,0 +1,3 @@ +# Direct dependencies for QNX unit testing +pyfatfs +fs diff --git a/requirements_lock.txt b/requirements_lock.txt new file mode 100644 index 00000000..33f08958 --- /dev/null +++ b/requirements_lock.txt @@ -0,0 +1,18 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# bazel run //:pip_requirements.update +# +fs==2.4.16 \ + --hash=sha256:660064febbccda264ae0b6bace80a8d1be9e089e0a5eb2427b7d517f9a91545c \ + --hash=sha256:ae97c7d51213f4b70b6a958292530289090de3a7e15841e108fbe144f069d313 + # via pyfatfs +pyfatfs==1.1.0 \ + --hash=sha256:7401fd39d9e92f531b2abea5b5d1d2c19d5461f4ba9ce8819b1ba35aea73c65d \ + --hash=sha256:9725ccd0a4da1c09c27358abbf10f08c043ac84210af576803e087f51a2b30e0 + # via -r requirements.in +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via fs diff --git a/third_party/itf/cc_test_qnx.bzl b/third_party/itf/cc_test_qnx.bzl new file mode 100644 index 00000000..fcef539a --- /dev/null +++ b/third_party/itf/cc_test_qnx.bzl @@ -0,0 +1,195 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +QNX C++ Unit Test Macro for SCORE Integration Testing Framework + +This macro wraps cc_test targets to run them on QNX 8.0 in QEMU using the +SCORE integration testing framework's QEMURunner infrastructure. + +It creates a bootable QNX image containing the test binary and executes it +using Python-based QEMU management (replacing the old shell-based approach). + +## Architecture: + +1. **qnx_ifs target**: Creates QNX bootable .ifs image + - Marked with target_compatible_with = QNX + - Built with QNX cross-compilation toolchain (--config=qnx_x86_64) + +2. **py_test wrapper**: Runs on Linux host + - Uses QEMURunner from quality/integration_testing/environments/qnx8_qemu/ + - Launches QEMU, executes tests, parses results from FAT32 image + - No target_compatible_with (runs on execution platform) + +This consolidates QEMU logic into a single Python-based implementation, +eliminating the previous duplication between shell scripts and Python. +""" + +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("@score_toolchains_qnx//rules/fs:ifs.bzl", "qnx_ifs") + +def _get_test_and_data_impl(ctx): + """Extract test binary and data files (excluding .so libraries)""" + data_files = [] + for file in ctx.attr.src[DefaultInfo].data_runfiles.files.to_list(): + if not file.basename.endswith(".so"): + data_files.append(file) + return [DefaultInfo(files = ctx.attr.src[DefaultInfo].files, runfiles = ctx.runfiles(files = data_files))] + +_get_test_and_data = rule( + implementation = _get_test_and_data_impl, + attrs = { + "src": attr.label(providers = [CcInfo]), + }, +) + +def cc_test_qnx(name, cc_test, excluded_tests_filter = None, app_tar = None): + """Compile and run C++ unit tests on QNX 8.0 in QEMU + + This macro uses the SCORE integration testing framework's QEMURunner + infrastructure to execute C++ tests in a QNX virtual machine. + + Args: + name: Test name + cc_test: cc_test target to execute in QNX + excluded_tests_filter: list of GTest filters to exclude from execution. + Examples: + ["FooTest.Test1"] - do not run Test1 from test suite FooTest + ["FooTest.*"] - do not run any test from test suite FooTest + ["*FooTest.*"] - runs all non-FooTest tests + app_tar: optional pkg_tar target containing application binaries to include + in the QNX image. Similar to integration_test's filesystem parameter. + The tar will be extracted to /app directory in the QNX filesystem. + """ + excluded_tests_filter = excluded_tests_filter if excluded_tests_filter else [] + + # Convert filter list to GTest format: "-FooTest.Test1:BarTest.*" + excluded_tests_filter_str = "-" + for test_filter in excluded_tests_filter: + excluded_tests_filter_str = excluded_tests_filter_str + (test_filter + ":\\") + + native.genrule( + name = "{}_excluded_tests_filter".format(name), + cmd_bash = """ + echo {} > $(@) + """.format(excluded_tests_filter_str), + testonly = True, + tags = ["manual"], + outs = ["{}_excluded_tests_filter.txt".format(name)], + ) + + _get_test_and_data( + name = "%s_test_and_data" % name, + src = cc_test, + testonly = True, + tags = ["manual"], + ) + + pkg_files( + name = "%s_test_and_runfiles" % name, + srcs = [ + ":{}_excluded_tests_filter".format(name), + ":{}_test_and_data".format(name), + ], + include_runfiles = True, + prefix = "/tests", + testonly = True, + tags = ["manual"], + attributes = pkg_attributes(mode = "0755"), + ) + + pkg_tar( + name = "%s_pkgtar" % name, + srcs = [ + "%s_test_and_runfiles" % name, + ], + testonly = True, + tags = ["manual"], + symlinks = { + "/tests/cc_test_qnx": native.package_relative_label(cc_test).name, + "/tests/cc_test_qnx_filters.txt": "{}_excluded_tests_filter.txt".format(name), + }, + modes = { + "/tests/cc_test_qnx": "0777", + "/tests/cc_test_qnx_filters.txt": "0777", + }, + ) + + # Create QNX bootable image with test package + # Uses tars parameter (like integration_test) instead of DUI + # Optionally includes app_tar if provided (similar to integration_test's filesystem) + qnx_ifs( + name = "%s_test_img" % name, + out = "{}_test.ifs".format(name), + build_file = "//quality/qnx_unit_testing:init_build_cc_test", + tars = {"TESTS": ":%s_pkgtar" % name} | ({"APP": app_tar} if app_tar else {}), + testonly = True, + target_compatible_with = [ + "@platforms//os:qnx", + ], + tags = [ + "manual", + ], + ) + + # Shell test wrapper (temporary - keeps old sh_test approach for compatibility) + # TODO: Migrate to py_test + QEMURunner once platform constraints are resolved + native.sh_test( + name = name, + srcs = ["//quality/qnx_unit_testing:run_qemu.sh"], + args = [ + "$(location //quality/qnx_unit_testing:init_shell)", + "$(location //quality/qnx_unit_testing:persistent)", + "$(location //quality/qnx_unit_testing:process_test_results)", + "$(location //quality/qnx_unit_testing:test_results)", + "$(location :%s_test_img)" % name, + ], + data = [ + ":%s_test_img" % name, + "//quality/qnx_unit_testing:init_shell", + "//quality/qnx_unit_testing:persistent", + "//quality/qnx_unit_testing:process_test_results", + "//quality/qnx_unit_testing:test_results", + ], + timeout = "short", + size = "medium", + # Note: sh_test runs on Linux host (launches QEMU), so no target_compatible_with + # The QNX IFS image (qnx_ifs) has proper platform constraints + tags = [ + "cpu:2", + "manual", + "qnx_unit_test", + ], + ) + + # Shell access for debugging (kept for compatibility) + native.sh_binary( + name = "%s_shell" % name, + srcs = ["//quality/qnx_unit_testing:run_qemu_shell.sh"], + args = [ + "$(location //quality/qnx_unit_testing:init_shell)", + "$(location //quality/qnx_unit_testing:persistent)", + "$(location //quality/qnx_unit_testing:test_results)", + "$(location :%s_test_img)" % name, + ], + data = [ + ":%s_test_img" % name, + "//quality/qnx_unit_testing:init_shell", + "//quality/qnx_unit_testing:persistent", + "//quality/qnx_unit_testing:test_results", + ], + testonly = True, + # Note: sh_binary runs on Linux host (launches QEMU), so no target_compatible_with + tags = ["manual"], + ) diff --git a/third_party/itf/py_unittest_qnx_test.bzl b/third_party/itf/py_unittest_qnx_test.bzl index e601d23c..f2305153 100644 --- a/third_party/itf/py_unittest_qnx_test.bzl +++ b/third_party/itf/py_unittest_qnx_test.bzl @@ -11,5 +11,46 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -def py_unittest_qnx_test(**kwargs): - pass +"""Convenience macro for creating QNX test suites.""" + +load("//third_party/itf:cc_test_qnx.bzl", "cc_test_qnx") + +def py_unittest_qnx_test( + name, + test_cases = [], + test_suites = [], + visibility = None, + tags = [], + excluded_tests_filter = None, + **kwargs): + """Creates a test suite of QNX tests from cc_test targets. + + Args: + name: Name of the test_suite to create + test_cases: List of cc_test targets to wrap with cc_test_qnx + test_suites: Additional test_suite targets to include + visibility: Visibility for the generated test_suite + tags: Tags to apply to the test_suite + **kwargs: Additional arguments passed to test_suite + """ + qnx_test_targets = [] + + for test_case in test_cases: + # Generate a unique name from the full label to avoid conflicts + # e.g. "@score_baselibs//score/os/utils/acl:unit_test" -> "score_baselibs__score_os_utils_acl__unit_test_qnx" + unique_name = test_case.replace("@", "").replace("//", "__").replace("/", "_").replace(":", "__") + qnx_target_name = unique_name + "_qnx" + + cc_test_qnx( + name = qnx_target_name, + cc_test = test_case, + ) + + qnx_test_targets.append(":" + qnx_target_name) + + native.test_suite( + name = name, + tests = qnx_test_targets + test_suites, + visibility = visibility, + tags = tags, + )