From 09690e03384565e8bc2a391a600aa4ba1ac14d26 Mon Sep 17 00:00:00 2001 From: sahithinukala Date: Tue, 17 Mar 2026 16:00:25 +0530 Subject: [PATCH] Add QNX unit testing infrastructure and update Python dependencies --- quality/qnx_unit_testing/BUILD | 122 +++++++++++ quality/qnx_unit_testing/cc_test_qnx.bzl | 195 ++++++++++++++++++ .../qnx_unit_testing/configure_persistency.sh | 165 +++++++++++++++ quality/qnx_unit_testing/environments/BUILD | 14 ++ .../environments/run_as_exec.bzl | 104 ++++++++++ .../init_x86_64_cc_test.build.template | 75 +++++++ .../qnx_unit_testing/process_test_results.py | 30 +++ quality/qnx_unit_testing/run_qemu.sh | 52 +++++ quality/qnx_unit_testing/run_qemu_shell.sh | 58 ++++++ quality/qnx_unit_testing/run_test.sh | 53 +++++ quality/qnx_unit_testing/startup.sh | 60 ++++++ quality/qnx_unit_testing/tools.build | 54 +++++ requirements.in | 4 + requirements_lock.txt | 24 +++ 14 files changed, 1010 insertions(+) create mode 100644 quality/qnx_unit_testing/BUILD create mode 100644 quality/qnx_unit_testing/cc_test_qnx.bzl create mode 100755 quality/qnx_unit_testing/configure_persistency.sh create mode 100644 quality/qnx_unit_testing/environments/BUILD create mode 100644 quality/qnx_unit_testing/environments/run_as_exec.bzl create mode 100644 quality/qnx_unit_testing/init_x86_64_cc_test.build.template create mode 100755 quality/qnx_unit_testing/process_test_results.py create mode 100755 quality/qnx_unit_testing/run_qemu.sh create mode 100755 quality/qnx_unit_testing/run_qemu_shell.sh create mode 100755 quality/qnx_unit_testing/run_test.sh create mode 100755 quality/qnx_unit_testing/startup.sh create mode 100644 quality/qnx_unit_testing/tools.build diff --git a/quality/qnx_unit_testing/BUILD b/quality/qnx_unit_testing/BUILD new file mode 100644 index 000000000..7f5d0acff --- /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_communication_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 000000000..fcef539ad --- /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 000000000..9775621bc --- /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 000000000..ace952e5e --- /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 000000000..c829deb3d --- /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 000000000..67aab5c99 --- /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 000000000..48c4fbe82 --- /dev/null +++ b/quality/qnx_unit_testing/process_test_results.py @@ -0,0 +1,30 @@ +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 000000000..9b7e6f444 --- /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 000000000..d14784d1b --- /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 000000000..18af37ae1 --- /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 000000000..a390d2973 --- /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 000000000..7113ec42c --- /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 index 6bfba48de..cf093ffdc 100644 --- a/requirements.in +++ b/requirements.in @@ -16,3 +16,7 @@ # Python dependencies for traceability tools -r ./third_party/traceability/requirements.txt + +# Direct dependencies for QNX unit testing and Bazel Python rules +pyfatfs +fs diff --git a/requirements_lock.txt b/requirements_lock.txt index 29e55decd..e1250ea69 100644 --- a/requirements_lock.txt +++ b/requirements_lock.txt @@ -12,6 +12,10 @@ alabaster==1.0.0 \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b # via sphinx +appdirs==1.4.4 \ + --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ + --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 + # via fs babel==2.18.0 \ --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 @@ -164,6 +168,12 @@ docutils==0.22.4 \ # pydata-sphinx-theme # sphinx # sphinx-rtd-theme +fs==2.4.16 \ + --hash=sha256:660064febbccda264ae0b6bace80a8d1be9e089e0a5eb2427b7d517f9a91545c \ + --hash=sha256:ae97c7d51213f4b70b6a958292530289090de3a7e15841e108fbe144f069d313 + # via + # -r requirements.in + # pyfatfs idna==3.11 \ --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 @@ -309,6 +319,10 @@ pydot==4.0.1 \ --hash=sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6 \ --hash=sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5 # via -r ./third_party/traceability/requirements.txt +pyfatfs==1.1.0 \ + --hash=sha256:7401fd39d9e92f531b2abea5b5d1d2c19d5461f4ba9ce8819b1ba35aea73c65d \ + --hash=sha256:9725ccd0a4da1c09c27358abbf10f08c043ac84210af576803e087f51a2b30e0 + # via -r requirements.in pygments==2.19.2 \ --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b @@ -412,6 +426,10 @@ roman-numerals==4.1.0 \ --hash=sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2 \ --hash=sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7 # via sphinx +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via fs snowballstemmer==3.0.1 \ --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 @@ -474,3 +492,9 @@ urllib3==2.6.3 \ # via # docker # requests + +# The following packages are considered to be unsafe in a requirements file: +setuptools==82.0.1 \ + --hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \ + --hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb + # via fs