diff --git a/.bazelrc b/.bazelrc index b90e1ef5..a97c3019 100644 --- a/.bazelrc +++ b/.bazelrc @@ -72,6 +72,10 @@ build:x86_64-qnx --platforms=@score_bazel_platforms//:x86_64-qnx-sdp_8.0.0-posix build:x86_64-qnx --extra_toolchains=@score_qcc_x86_64_toolchain//:x86_64-qnx-sdp_8.0.0 build:x86_64-qnx --extra_toolchains=@score_qnx_x86_64_ifs_toolchain//:ifs-x86_64-qnx-sdp_8.0.0 build:x86_64-qnx --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_pc_nto_qnx800 +# Integration tests require direct device access (KVM, TAP) — bypass linux-sandbox +test:x86_64-qnx --strategy=TestRunner=local +# All QEMU instances share one tap0/IP — tests must run one at a time +test:x86_64-qnx --local_test_jobs=1 # TODO arm64 when rust support is there @@ -98,12 +102,16 @@ coverage --combined_report=lcov coverage --test_env=COVERAGE_GCOV_OPTIONS=-bcu coverage --features=coverage coverage --cache_test_results=no +# See -fprofile-update in the GCC manual (recommended for multithreaded applications): +# https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html +# Reasoning: https://github.com/eclipse-score/logging/issues/156 +coverage --cxxopt=-fprofile-update=atomic # ============================================================================== # Dynamic analysis (sanitizers) for Linux host builds/tests # ============================================================================== -# Debug symbols for sanitizer stack traces +# Debug symbols for sanitizer stack traces test:with_debug_symbols --cxxopt=-g1 test:with_debug_symbols --strip=never diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 75c6661e..7d7789fd 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -22,7 +22,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request_target' }} + cancel-in-progress: true jobs: approval: @@ -39,7 +39,14 @@ jobs: integration-tests-qnx: name: QNX Integration Tests (x86_64-qnx) needs: approval - if: ${{ !cancelled() }} + # Run for all events except 'labeled', which only triggers on the opt-in label + # to avoid rerunning this expensive job on unrelated label changes. + if: >- + ${{ !cancelled() + && (github.event_name != 'pull_request_target' + || github.event.action != 'labeled' + || github.event.label.name == 'integration_testing') }} + timeout-minutes: 45 runs-on: ${{ vars.runner_labels_ghub_standard_x64 && fromJSON(vars.runner_labels_ghub_standard_x64) || vars.REPO_RUNNER_LABELS && fromJSON(vars.REPO_RUNNER_LABELS) || 'ubuntu-latest' }} permissions: contents: read @@ -51,13 +58,13 @@ jobs: level: 4 - name: Checkout repository (Handle all events) - uses: actions/checkout@v4.2.2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.head_ref || github.event.pull_request.head.ref || github.ref }} repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - name: Setup Bazel with shared caching - uses: bazel-contrib/setup-bazel@0.18.0 + uses: bazel-contrib/setup-bazel@77a1d3d18379c7cb0a7e3b9fcaaa4d94f1029763 # 0.18.0 with: bazelisk-version: 1.26.0 disk-cache: true @@ -79,7 +86,7 @@ jobs: - name: Install qemu run: | sudo apt-get update - sudo apt-get install -y qemu-system + sudo apt-get install -y --no-install-recommends qemu-system-x86 - name: Enable KVM group permissons run: | @@ -89,33 +96,58 @@ jobs: - name: Setup tap0 network interface for QEMU run: | - sudo ip tuntap add dev tap0 mode tap user runner - sudo ip link set dev tap0 up + set -euo pipefail + + RUNNER_USER="${SUDO_USER:-$USER}" + if ! getent passwd "$RUNNER_USER" > /dev/null; then + RUNNER_USER="$(id -un)" + fi + + if [ ! -c /dev/net/tun ]; then + echo "Missing /dev/net/tun; TAP devices are unavailable on this runner." >&2 + exit 1 + fi + + if ! ip link show tap0 > /dev/null 2>&1; then + sudo ip tuntap add dev tap0 mode tap user "$RUNNER_USER" + fi + + sudo ip addr flush dev tap0 sudo ip addr add 169.254.21.88/16 dev tap0 + sudo ip link set dev tap0 up - - name: Allow unprivileged user namespaces + - name: Verify QEMU tap0 network preflight run: | - sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 + set -euo pipefail + test -c /dev/net/tun + ip link show tap0 + ip -4 addr show dev tap0 | grep -q "169.254.21.88/16" - name: Run integration tests run: | set -euo pipefail + : "${QNX_CREDENTIAL_HELPER:?QNX_CREDENTIAL_HELPER is not set (expected from setup-qnx-sdp)}" bazel test --config x86_64-qnx \ --credential_helper=*.qnx.com="${QNX_CREDENTIAL_HELPER}" \ - --lockfile_mode=error --test_tag_filters=integration --test_output=all -- \ + --lockfile_mode=error --test_tag_filters=integration --test_output=errors -- \ //score/test/component/... - name: Upload QNX ITF test logs - if: always() - uses: actions/upload-artifact@v6 + if: ${{ failure() || cancelled() }} + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: qnx-itf-test-logs path: | bazel-testlogs/**/test.log bazel-testlogs/**/test.outputs/** if-no-files-found: warn - retention-days: 7 + retention-days: 3 + + - name: Cleanup tap0 network interface + if: always() + run: | + sudo ip link delete tap0 2>/dev/null || true - name: Cleanup QNX license if: always() diff --git a/quality/integration_testing/environments/qnx8_qemu/init.build b/quality/integration_testing/environments/qnx8_qemu/init.build index 44428bc6..2fbb8fec 100644 --- a/quality/integration_testing/environments/qnx8_qemu/init.build +++ b/quality/integration_testing/environments/qnx8_qemu/init.build @@ -21,6 +21,7 @@ [+script] startup-script = { procmgr_symlink /dev/shmem /tmp + procmgr_symlink /tmp_ram/tmp_discovery /tmp_discovery display_msg Welcome to QNX OS 8.0 on x86_64 - score_logging component tests diff --git a/quality/integration_testing/environments/qnx8_qemu/startup.sh b/quality/integration_testing/environments/qnx8_qemu/startup.sh index 03343945..504133e1 100644 --- a/quality/integration_testing/environments/qnx8_qemu/startup.sh +++ b/quality/integration_testing/environments/qnx8_qemu/startup.sh @@ -58,6 +58,5 @@ fi echo "---> Adding /tmp_discovery folder" mkdir -p /tmp_ram/tmp_discovery -ln -s /tmp_ram/tmp_discovery /tmp_discovery /proc/boot/sshd -f /var/ssh/sshd_config diff --git a/score/test/component/datarouter/BUILD b/score/test/component/datarouter/BUILD index fe6c0212..5fe797f8 100644 --- a/score/test/component/datarouter/BUILD +++ b/score/test/component/datarouter/BUILD @@ -72,7 +72,6 @@ filegroup( py_logging_itf_test( name = "test_datarouter_filters", srcs = ["test_datarouter_filters.py"], - extra_oci_tars = ["//score/test/component/filters_app:filtertest_classid_pkg"], filesystem = "//score/test/component/filters_app:filtertest_filesystem", deps = ["@score_itf//score/itf/plugins/dlt"], ) diff --git a/score/test/component/datarouter/filetransfer_app.cpp b/score/test/component/datarouter/filetransfer_app.cpp index ab9c5732..870ebf68 100644 --- a/score/test/component/datarouter/filetransfer_app.cpp +++ b/score/test/component/datarouter/filetransfer_app.cpp @@ -15,7 +15,6 @@ #include "score/mw/log/logging.h" #include -#include #include #include #include @@ -57,11 +56,13 @@ int main() { std::stringstream filename; filename << "/tmp/filetransfer_test_" << i << ".txt"; - std::error_code ec; - std::filesystem::copy(original_file, filename.str(), ec); - if (ec) { - logger.LogError() << "Failed to copy file:" << ec.message(); - continue; + { + std::ifstream src(original_file, std::ios::binary); + std::ofstream dst(filename.str(), std::ios::binary | std::ios::trunc); + if (!src || !dst || !(dst << src.rdbuf())) { + logger.LogError() << "Failed to copy file:" << filename.str(); + continue; + } } file_transfer.TransferFile(filename.str(), false); diff --git a/score/test/component/filters_app/BUILD b/score/test/component/filters_app/BUILD index 65cc1fae..746bcb46 100644 --- a/score/test/component/filters_app/BUILD +++ b/score/test/component/filters_app/BUILD @@ -110,6 +110,7 @@ pkg_tar( visibility = ["//score/test/component:__subpackages__"], deps = [ ":filtertest_bin_pkg", + ":filtertest_classid_pkg", ":filtertest_config_pkg", ], ) diff --git a/score/test/component/logging_plugin.py b/score/test/component/logging_plugin.py index 8bc06356..8d94dc44 100644 --- a/score/test/component/logging_plugin.py +++ b/score/test/component/logging_plugin.py @@ -20,7 +20,7 @@ from contextlib import contextmanager import pytest -from score.itf.plugins.dlt.dlt_receive import Protocol +from score.itf.plugins.dlt.dlt_receive import DltReceive, Protocol from score.itf.plugins.dlt.dlt_window import DltLogRecord LOGGER = logging.getLogger(__name__) @@ -31,8 +31,9 @@ _DATAROUTER_READY_INTERVAL = 0.2 _QNX_DR_CMD = ( - "on -A nonroot,allow,pathspace -u 1000:1000 " - "/usr/bin/datarouter/datarouter --no_adaptive_runtime " + "cd /usr/bin/datarouter && " + "nohup on -A nonroot,allow,pathspace -u 1000:1000 " + "./datarouter --no_adaptive_runtime " "> /dev/null 2>&1 &" ) _QNX_DR_STOP_CMD = ( @@ -75,8 +76,21 @@ def _wait_for_datarouter(target, timeout=_DATAROUTER_READY_TIMEOUT): raise TimeoutError(f"Datarouter not ready within {timeout}s") +class _LocalDltReceiver: + """Exposes a local DLT file path via the same .dlt_file interface as DltReceiver.""" + + def __init__(self, local_path): + self.dlt_file = local_path + + def download_dlt(target, remote_path): - """Download a .dlt file from the target and return a DltLogRecord.""" + """Download a .dlt file from the target and return a DltLogRecord. + + If remote_path is already a local file (QNX HOST-side capture), returns it directly. + """ + if os.path.exists(remote_path): + LOGGER.info(f"Using local DLT file: {os.path.getsize(remote_path)} bytes") + return DltLogRecord(remote_path) local_dir = tempfile.mkdtemp(prefix="dlt_") local_path = os.path.join(local_dir, os.path.basename(remote_path)) target.download(remote_path, local_path) @@ -97,8 +111,8 @@ def docker_configuration(): def datarouter_on_target(target): """Start the datarouter on the target (Docker or QNX), yield, then stop it.""" if _is_qnx(target): - target.execute(_QNX_DR_CMD) try: + target.execute(_QNX_DR_CMD) _wait_for_datarouter(target) yield target finally: @@ -119,25 +133,31 @@ def datarouter_on_target(target): @pytest.fixture(scope="function") def dlt_capture(target, dlt_on_target, request): - """Start a DLT receiver. On QNX, binds to the host tap0 IP from dlt_config.""" + """Start a DLT receiver. On QNX, runs dlt-receive on the host; on Docker, on the target.""" @contextmanager def _start(protocol=Protocol.UDP, host_ip=None, multicast_ips=None): - if host_ip is None: - if _is_qnx(target): - try: - dlt_cfg = request.getfixturevalue("dlt_config") - host_ip = dlt_cfg.host_ip - except pytest.FixtureLookupError: - host_ip = target.get_ip() - else: - host_ip = target.get_ip() if multicast_ips is None: multicast_ips = DLT_MULTICAST_IPS - with dlt_on_target( - protocol, host_ip=host_ip, multicast_ips=multicast_ips - ) as receiver: - time.sleep(_DLT_RECEIVER_SETTLE_DELAY) - yield receiver + + if _is_qnx(target): + dlt_cfg = request.getfixturevalue("dlt_config") + effective_host_ip = host_ip or dlt_cfg.host_ip + with DltReceive( + protocol=protocol, + host_ip=effective_host_ip, + multicast_ips=multicast_ips, + binary_path=dlt_cfg.dlt_receive_path, + ) as receiver: + time.sleep(_DLT_RECEIVER_SETTLE_DELAY) + yield _LocalDltReceiver(receiver.file_name()) + else: + if host_ip is None: + host_ip = target.get_ip() + with dlt_on_target( + protocol, host_ip=host_ip, multicast_ips=multicast_ips + ) as receiver: + time.sleep(_DLT_RECEIVER_SETTLE_DELAY) + yield receiver return _start